Skip to content

Commit 88c53cb

Browse files
authored
feat: add useSafeState (#13)
1 parent e024289 commit 88c53cb

File tree

3 files changed

+101
-1
lines changed

3 files changed

+101
-1
lines changed

src/useSafeState.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Dispatch, SetStateAction, useCallback } from 'react'
2+
import useMounted from './useMounted'
3+
import { AsyncSetState } from './useStateAsync'
4+
5+
type StateSetter<TState> = Dispatch<SetStateAction<TState>>
6+
7+
/**
8+
* `useSafeState` takes the return value of a `useState` hook and wraps the
9+
* setter to prevent updates onces the component has unmounted. Can used
10+
* with `useMergeState` and `useStateAsync` as well
11+
*
12+
* @param state The return value of a useStateHook
13+
*
14+
* ```ts
15+
* const [show, setShow] = useSafeState(useState(true));
16+
* ```
17+
*/
18+
function useSafeState<TState>(
19+
state: [TState, AsyncSetState<TState>],
20+
): [TState, (stateUpdate: React.SetStateAction<TState>) => Promise<void>]
21+
function useSafeState<TState>(
22+
state: [TState, StateSetter<TState>],
23+
): [TState, StateSetter<TState>]
24+
function useSafeState<TState>(
25+
state: [TState, StateSetter<TState> | AsyncSetState<TState>],
26+
): [TState, StateSetter<TState> | AsyncSetState<TState>] {
27+
const isMounted = useMounted()
28+
29+
return [
30+
state[0],
31+
useCallback(
32+
(nextState: SetStateAction<TState>) => {
33+
if (!isMounted()) return
34+
return state[1](nextState)
35+
},
36+
[isMounted, state[1]],
37+
),
38+
]
39+
}
40+
41+
export default useSafeState

src/useSet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class ObservableSet<V> extends Set<V> {
3333
/**
3434
* Create and return a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) that triggers rerenders when it's updated.
3535
*
36-
* ```tsx
36+
* ```ts
3737
* const ids = useSet<number>([1,2,3,4]);
3838
*
3939
* return (

test/useSafeState.test.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { mount } from 'enzyme'
2+
import React, { useEffect, useState } from 'react'
3+
import { act } from 'react-dom/test-utils'
4+
import useSafeState from '../src/useSafeState'
5+
import useStateAsync from '../src/useStateAsync'
6+
7+
describe('useSafeState', () => {
8+
it('should work transparently', () => {
9+
let state
10+
11+
function Wrapper() {
12+
state = useSafeState(useState(false))
13+
return null
14+
}
15+
16+
const wrapper = mount(<Wrapper />)
17+
18+
expect(state[0]).toEqual(false)
19+
20+
act(() => {
21+
state[1](true)
22+
})
23+
expect(state[0]).toEqual(true)
24+
25+
wrapper.unmount()
26+
27+
act(() => {
28+
state[1](false)
29+
})
30+
expect(state[0]).toEqual(true)
31+
})
32+
33+
it('should work with async setState', async () => {
34+
let state
35+
36+
function Wrapper() {
37+
state = useSafeState(useStateAsync(false))
38+
return null
39+
}
40+
41+
const wrapper = mount(<Wrapper />)
42+
43+
expect(state[0]).toEqual(false)
44+
45+
await act(async () => {
46+
await state[1](true)
47+
})
48+
49+
expect(state[0]).toEqual(true)
50+
51+
wrapper.unmount()
52+
53+
await act(async () => {
54+
await state[1](true)
55+
})
56+
57+
expect(state[0]).toEqual(true)
58+
})
59+
})

0 commit comments

Comments
 (0)