diff --git a/README.md b/README.md
index d7938ce6..7019a6a1 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@
- [`useSpring`](./docs/useSpring.md) — interpolates number over time according to spring dynamics.
- [`useTimeout`](./docs/useTimeout.md) — returns true after a timeout.
- [`useTween`](./docs/useTween.md) — re-renders component, while tweening a number from 0 to 1. [![][img-demo]](https://codesandbox.io/s/52990wwzyl)
+ - [`useUpdate`](./docs/useUpdate.md) — returns a callback, which re-renders component when called.
- [**Side-effects**](./docs/Side-effects.md)
@@ -71,6 +72,7 @@
- [**State**](./docs/State.md)
+ - [`useGetSet`](./docs/useGetSet.md) — returns state getter `get()` instead of raw state.
- [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`.
- [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
- [`useToggle`](./docs/useToggle.md) — tracks state of a boolean.
diff --git a/docs/useGetSet.md b/docs/useGetSet.md
new file mode 100644
index 00000000..bda5cd40
--- /dev/null
+++ b/docs/useGetSet.md
@@ -0,0 +1,46 @@
+# `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.
+
+
+## Usage
+
+Below example uses `useGetSet` to increment a number after 1 second
+on each click.
+
+```jsx
+import {useGetSet} from 'react-use';
+
+const Demo = () => {
+ const [get, set] = useGetSet(0);
+ const onClick = () => {
+ setTimeout(() => {
+ set(get() + 1)
+ }, 1_000);
+ };
+
+ return (
+ Clicked: {get()}
+ );
+};
+```
+
+If you would do this example in a naive way using regular `useState`
+hook, the counter would not increment correctly if you click fast multiple times.
+
+```jsx
+const DemoWrong = () => {
+ const [cnt, set] = useState(0);
+ const onClick = () => {
+ setTimeout(() => {
+ set(cnt + 1)
+ }, 1_000);
+ };
+
+ return (
+ Clicked: {cnt}
+ );
+};
+```
diff --git a/docs/useUpdate.md b/docs/useUpdate.md
new file mode 100644
index 00000000..9f9549ef
--- /dev/null
+++ b/docs/useUpdate.md
@@ -0,0 +1,21 @@
+# `useUpdate`
+
+React utility hook that returns a function that forces component
+to re-render when called.
+
+
+## Usage
+
+```jsx
+import {useUpdate} from 'react-use';
+
+const Demo = () => {
+ const update = useUpdate();
+ return (
+ <>
+
Time: {Date.now()}
+ Update
+ >
+ );
+};
+```
diff --git a/src/__stories__/useGetSet.story.tsx b/src/__stories__/useGetSet.story.tsx
new file mode 100644
index 00000000..540a4c9c
--- /dev/null
+++ b/src/__stories__/useGetSet.story.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import {storiesOf} from '@storybook/react';
+import {useGetSet} from '..';
+import {useState} from '../react';
+import ShowDocs from '../util/ShowDocs';
+
+const Demo = () => {
+ const [get, set] = useGetSet(0);
+ const onClick = () => {
+ setTimeout(() => {
+ set(get() + 1)
+ }, 1_000);
+ };
+
+ return (
+ Clicked: {get()}
+ );
+};
+
+const DemoWrong = () => {
+ const [cnt, set] = useState(0);
+ const onClick = () => {
+ setTimeout(() => {
+ set(cnt + 1)
+ }, 1_000);
+ };
+
+ return (
+ Clicked: {cnt}
+ );
+};
+
+storiesOf('useGetSet', module)
+ .add('Docs', () => )
+ .add('Demo', () =>
+
+ )
+ .add('DemoWrong', () =>
+
+ )
diff --git a/src/__stories__/useUpdate.story.tsx b/src/__stories__/useUpdate.story.tsx
new file mode 100644
index 00000000..b445f3c2
--- /dev/null
+++ b/src/__stories__/useUpdate.story.tsx
@@ -0,0 +1,21 @@
+import {storiesOf} from '@storybook/react';
+import * as React from 'react';
+import {useUpdate} from '..';
+import ShowDocs from '../util/ShowDocs';
+
+const Demo = () => {
+ const update = useUpdate();
+ return (
+ <>
+ Time: {Date.now()}
+ Update
+ >
+ );
+};
+
+storiesOf('useUpdate', module)
+ .add('Docs', () => )
+ .add('Demo', () =>
+
+ )
+
diff --git a/src/index.ts b/src/index.ts
index 3b63f020..974acbcb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,6 +5,7 @@ import useCounter from './useCounter';
import useCss from './useCss';
import useFavicon from './useFavicon';
import useGeolocation from './useGeolocation';
+import useGetSet from './useGetSet';
import useHover from './useHover';
import useIdle from './useIdle';
import useLifecycles from './useLifecycles';
@@ -30,6 +31,7 @@ import useTitle from './useTitle';
import useToggle from './useToggle';
import useTween from './useTween';
import useUnmount from './useUnmount';
+import useUpdate from './useUpdate';
import useWindowSize from './useWindowSize';
export {
@@ -40,6 +42,7 @@ export {
useCss,
useFavicon,
useGeolocation,
+ useGetSet,
useHover,
useIdle,
useLifecycles,
@@ -65,5 +68,6 @@ export {
useToggle,
useTween,
useUnmount,
+ useUpdate,
useWindowSize,
};
diff --git a/src/useGetSet.ts b/src/useGetSet.ts
new file mode 100644
index 00000000..6e76bbb3
--- /dev/null
+++ b/src/useGetSet.ts
@@ -0,0 +1,17 @@
+import {useRef} from './react';
+import useUpdate from './useUpdate';
+
+const useGetSet = (initialValue: T): [() => T, (value: T) => void] => {
+ const update = useUpdate();
+ let state = useRef(initialValue);
+
+ const get = () => state.current;
+ const set = (value: T) => {
+ state.current = value;
+ update();
+ };
+
+ return [get, set];
+};
+
+export default useGetSet;
diff --git a/src/useUpdate.ts b/src/useUpdate.ts
new file mode 100644
index 00000000..cb1948cb
--- /dev/null
+++ b/src/useUpdate.ts
@@ -0,0 +1,5 @@
+import {useState} from './react';
+
+const useUpdate = () => useState(0)[1] as (() => void);
+
+export default useUpdate;