diff --git a/README.md b/README.md
index 870e5ceb..edd93dcd 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,8 @@ our [migration guide](https://react-hookz.github.io/web/?path=/docs/migrating-fr
— Like `useSafeState` but its state setter is throttled.
- [**`useValidator`**](https://react-hookz.github.io/web/?path=/docs/state-usevalidator--example)
— Performs validation when any of provided dependencies has changed.
+ - [**`useGetSet`**](https://react-hookz.github.io/web/?path=/docs/state-usegetset--example)
+ - React state hook that returns state getter function instead of raw state itself, this prevents subtle bugs when state is used in nested functions.
- #### Navigator
diff --git a/src/__docs__/migrating-from-react-use.story.mdx b/src/__docs__/migrating-from-react-use.story.mdx
index b8344915..197d178e 100644
--- a/src/__docs__/migrating-from-react-use.story.mdx
+++ b/src/__docs__/migrating-from-react-use.story.mdx
@@ -557,7 +557,7 @@ Not implemented yet
#### useGetSet
-Not implemented yet
+See [useGetSet](/docs/state-useGetSet--example)
#### useGetSetState
diff --git a/src/index.ts b/src/index.ts
index 4cf2f65e..028f767a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -26,6 +26,7 @@ export { useMap } from './useMap/useMap';
export { useMediatedState } from './useMediatedState/useMediatedState';
export { usePrevious } from './usePrevious/usePrevious';
export { useSafeState } from './useSafeState/useSafeState';
+export { useGetSet } from './useGetSet/useGetSet';
export { useSet } from './useSet/useSet';
export { useToggle } from './useToggle/useToggle';
export { useThrottledState } from './useThrottledState/useThrottledState';
diff --git a/src/useGetSet/__docs__/example.stories.tsx b/src/useGetSet/__docs__/example.stories.tsx
new file mode 100644
index 00000000..5c8f89c8
--- /dev/null
+++ b/src/useGetSet/__docs__/example.stories.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import { useGetSet } from '../..';
+
+export const Example: React.FC = () => {
+ const [get, set] = useGetSet(0);
+ const onClick = () => {
+ setTimeout(() => {
+ set(get() + 1);
+ }, 1000);
+ };
+ return ;
+};
diff --git a/src/useGetSet/__docs__/story.mdx b/src/useGetSet/__docs__/story.mdx
new file mode 100644
index 00000000..e2218066
--- /dev/null
+++ b/src/useGetSet/__docs__/story.mdx
@@ -0,0 +1,38 @@
+import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
+import { Example } from './example.stories';
+import { ImportPath } from '../../storybookUtil/ImportPath';
+
+
+
+# useGetSet
+
+React state hook that returns state getter function instead of raw state itself, this prevents subtle bugs when state is used in nested functions.
+
+#### Example
+
+
+
+## Reference
+
+```ts
+export function useGetSet(
+ initialState: IInitialState
+): [get: () => S, set: (nextState: INextState) => void];
+```
+
+#### Importing
+
+
+
+#### Arguments
+
+- _**initialState**_ _`IInitialState`_ - initial state or initial state setter as for `useState`
+
+#### Return
+
+Returns array alike `useState` does, but instead return a state value it return a getter function.
+
+- _**[0]**_ _`S`_ - getter of current state
+- _**[1]**_ _`(nextState?: INewState) => void`_ - state setter as for `useState`.
diff --git a/src/useGetSet/__tests__/dom.ts b/src/useGetSet/__tests__/dom.ts
new file mode 100644
index 00000000..84fdeb7f
--- /dev/null
+++ b/src/useGetSet/__tests__/dom.ts
@@ -0,0 +1,62 @@
+import { renderHook, act } from '@testing-library/react-hooks/dom';
+import { useGetSet } from '../..';
+
+const setUp = (initialValue: any) => renderHook(() => useGetSet(initialValue));
+
+beforeEach(() => {
+ jest.useFakeTimers();
+});
+
+describe('useGetSet', () => {
+ it('should be defined', () => {
+ expect(useGetSet).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = setUp('foo');
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should init getter and setter', () => {
+ const { result } = setUp('foo');
+ const [get, set] = result.current;
+ expect(get).toBeInstanceOf(Function);
+ expect(set).toBeInstanceOf(Function);
+ });
+
+ it('should get current value', () => {
+ const { result } = setUp('foo');
+ const [get] = result.current;
+ expect(get()).toBe('foo');
+ });
+
+ it('should set new value', () => {
+ const { result } = setUp('foo');
+ const [get, set] = result.current;
+ act(() => set('bar'));
+ const currentValue = get();
+ expect(currentValue).toBe('bar');
+ });
+
+ it('should get and set expected values when used in nested functions', () => {
+ const { result } = setUp(0);
+ const [get, set] = result.current;
+ const onClick = jest.fn(() => {
+ setTimeout(() => {
+ set(get() + 1);
+ }, 1000);
+ });
+
+ // simulate 3 clicks
+ onClick();
+ onClick();
+ onClick();
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ expect(get()).toBe(3);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ });
+});
diff --git a/src/useGetSet/__tests__/ssr.ts b/src/useGetSet/__tests__/ssr.ts
new file mode 100644
index 00000000..107819c0
--- /dev/null
+++ b/src/useGetSet/__tests__/ssr.ts
@@ -0,0 +1,62 @@
+import { renderHook, act } from '@testing-library/react-hooks/server';
+import { useGetSet } from '../..';
+
+const setUp = (initialValue: any) => renderHook(() => useGetSet(initialValue));
+
+beforeEach(() => {
+ jest.useFakeTimers();
+});
+
+describe('useGetSet', () => {
+ it('should be defined', () => {
+ expect(useGetSet).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = setUp('foo');
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should init getter and setter', () => {
+ const { result } = setUp('foo');
+ const [get, set] = result.current;
+ expect(get).toBeInstanceOf(Function);
+ expect(set).toBeInstanceOf(Function);
+ });
+
+ it('should get current value', () => {
+ const { result } = setUp('foo');
+ const [get] = result.current;
+ expect(get()).toBe('foo');
+ });
+
+ it('should set new value', () => {
+ const { result } = setUp('foo');
+ const [get, set] = result.current;
+ act(() => set('bar'));
+ const currentValue = get();
+ expect(currentValue).toBe('bar');
+ });
+
+ it('should get and set expected values when used in nested functions', () => {
+ const { result } = setUp(0);
+ const [get, set] = result.current;
+ const onClick = jest.fn(() => {
+ setTimeout(() => {
+ set(get() + 1);
+ }, 1000);
+ });
+
+ // simulate 3 clicks
+ onClick();
+ onClick();
+ onClick();
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ expect(get()).toBe(3);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ });
+});
diff --git a/src/useGetSet/useGetSet.ts b/src/useGetSet/useGetSet.ts
new file mode 100644
index 00000000..e198dc78
--- /dev/null
+++ b/src/useGetSet/useGetSet.ts
@@ -0,0 +1,13 @@
+import { useCallback, Dispatch, SetStateAction } from 'react';
+import { IInitialState } from '../util/resolveHookState';
+import { useSafeState, useSyncedRef } from '../index';
+
+export function useGetSet(
+ initialState: IInitialState
+): [get: () => S, set: Dispatch>] {
+ const [state, setState] = useSafeState(initialState);
+ const stateRef = useSyncedRef(state);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return [useCallback(() => stateRef.current, []), setState];
+}