diff --git a/README.md b/README.md
index 0ca53903..427f250a 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,8 @@ Coming from `react-use`? Check out our
— Tracks a numeric value and offers functions for manipulating it.
- [**`useDebouncedState`**](https://react-hookz.github.io/web/?path=/docs/state-usedebouncedstate--example)
— Like `useSafeState` but its state setter is debounced.
+ - [**`useFunctionalState`**](https://react-hookz.github.io/web/?path=/docs/state-usefunctionalstate--page)
+ — Like `useState` but instead of raw state, state getter returned.
- [**`useList`**](https://react-hookz.github.io/web/?path=/docs/state-uselist--example)
— Tracks a list and offers functions for manipulating it.
- [**`useMap`**](https://react-hookz.github.io/web/?path=/docs/state-usemap--example) — Tracks the
diff --git a/src/index.ts b/src/index.ts
index 877e665e..32597b10 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -27,6 +27,7 @@ export { useIntervalEffect } from './useIntervalEffect/useIntervalEffect';
// State
export { useDebouncedState } from './useDebouncedState/useDebouncedState';
+export { useFunctionalState } from './useFunctionalState/useFunctionalState';
export { useList } from './useList/useList';
export { useMap } from './useMap/useMap';
export { useMediatedState } from './useMediatedState/useMediatedState';
@@ -58,6 +59,7 @@ export { useVibrate } from './useVibrate/useVibrate';
// Miscellaneous
export { useSyncedRef } from './useSyncedRef/useSyncedRef';
+export { useHookableRef, HookableRefHandler } from './useHookableRef/useHookableRef';
// SideEffect
export { useLocalStorageValue } from './useLocalStorageValue/useLocalStorageValue';
@@ -109,5 +111,3 @@ export { useWindowSize, WindowSize } from './useWindowSize/useWindowSize';
export { truthyAndArrayPredicate, truthyOrArrayPredicate } from './util/const';
export { IEffectCallback, IEffectHook } from './util/misc';
-
-export { useHookableRef } from './useHookableRef/useHookableRef';
diff --git a/src/useFunctionalState/__docs__/story.mdx b/src/useFunctionalState/__docs__/story.mdx
new file mode 100644
index 00000000..bc09611a
--- /dev/null
+++ b/src/useFunctionalState/__docs__/story.mdx
@@ -0,0 +1,19 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+
+
+
+# useFunctionalState
+
+Like `useState` but instead of raw state, state getter returned. `useSafeState` is used underneath.
+
+## Reference
+
+```ts
+export function useFunctionalState(
+ initialState: S | (() => S)
+): [() => S, React.Dispatch>];
+export function useFunctionalState(): [
+ () => S | undefined,
+ React.Dispatch>
+];
+```
diff --git a/src/useFunctionalState/__tests__/dom.ts b/src/useFunctionalState/__tests__/dom.ts
new file mode 100644
index 00000000..d592c333
--- /dev/null
+++ b/src/useFunctionalState/__tests__/dom.ts
@@ -0,0 +1,31 @@
+import { act, renderHook } from '@testing-library/react-hooks/dom';
+import { useFunctionalState } from '../..';
+
+describe('useFunctionalState', () => {
+ it('should be defined', () => {
+ expect(useFunctionalState).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useFunctionalState());
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should return proper values', () => {
+ const { result } = renderHook(() => useFunctionalState(1));
+ expect(result.current[1]).toBeInstanceOf(Function);
+ expect(result.current[0]).toBeInstanceOf(Function);
+ });
+
+ it('should return state getter', () => {
+ const { result } = renderHook(() => useFunctionalState(1));
+
+ expect(result.current[0]()).toBe(1);
+
+ act(() => {
+ result.current[1](2);
+ });
+
+ expect(result.current[0]()).toBe(2);
+ });
+});
diff --git a/src/useFunctionalState/__tests__/ssr.ts b/src/useFunctionalState/__tests__/ssr.ts
new file mode 100644
index 00000000..c19e65c1
--- /dev/null
+++ b/src/useFunctionalState/__tests__/ssr.ts
@@ -0,0 +1,19 @@
+import { renderHook } from '@testing-library/react-hooks/server';
+import { useFunctionalState } from '../..';
+
+describe('useFunctionalState', () => {
+ it('should be defined', () => {
+ expect(useFunctionalState).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useFunctionalState());
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should return proper values', () => {
+ const { result } = renderHook(() => useFunctionalState(1));
+ expect(result.current[1]).toBeInstanceOf(Function);
+ expect(result.current[0]).toBeInstanceOf(Function);
+ });
+});
diff --git a/src/useFunctionalState/useFunctionalState.ts b/src/useFunctionalState/useFunctionalState.ts
new file mode 100644
index 00000000..c289f71f
--- /dev/null
+++ b/src/useFunctionalState/useFunctionalState.ts
@@ -0,0 +1,25 @@
+import { Dispatch, SetStateAction, useCallback } from 'react';
+import { useSafeState } from '../useSafeState/useSafeState';
+import { useSyncedRef } from '../useSyncedRef/useSyncedRef';
+
+export function useFunctionalState(
+ initialState: S | (() => S)
+): [() => S, Dispatch>];
+export function useFunctionalState(): [
+ () => S | undefined,
+ Dispatch>
+];
+
+/**
+ * Like `useState` but instead of raw state, state getter returned. `useSafeState` is
+ * used underneath.
+ */
+export function useFunctionalState(
+ initialState?: S | (() => S)
+): [() => S | undefined, Dispatch>] {
+ const [state, setState] = useSafeState(initialState);
+ const stateRef = useSyncedRef(state);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return [useCallback(() => stateRef.current, []), setState];
+}
diff --git a/src/useSafeState/__docs__/story.mdx b/src/useSafeState/__docs__/story.mdx
index 954381b3..211c8807 100644
--- a/src/useSafeState/__docs__/story.mdx
+++ b/src/useSafeState/__docs__/story.mdx
@@ -16,8 +16,8 @@ action if component is unmounted, otherwise it is the same hook as common `useSt
#### Example
-Sadly we can't provide an example since this documentation built in `production` mode and warning
-are only shown in `development` mode.
+Sadly we can't provide an example since this documentation built in `production` mode and
+warnings are only shown in `development` mode.
## Reference
diff --git a/src/useSafeState/__tests__/dom.ts b/src/useSafeState/__tests__/dom.ts
index 7dd9dee8..79ddcf73 100644
--- a/src/useSafeState/__tests__/dom.ts
+++ b/src/useSafeState/__tests__/dom.ts
@@ -11,7 +11,7 @@ describe('useSafeState', () => {
expect(result.error).toBeUndefined();
});
- it('should not call', () => {
+ it('should not cause state change after component unmount', () => {
const consoleSpy = jest.spyOn(console, 'error');
consoleSpy.mockImplementationOnce(() => {});