-
-
Notifications
You must be signed in to change notification settings - Fork 461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure that state changes are applied immediately on mount #256
Changes from all commits
cb11f70
ba21f9a
16a1269
cc25a0e
dcf92ff
5d64e30
17feeca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import React from 'react'; | ||
import { renderHook } from 'react-hooks-testing-library'; | ||
import { useImmediateState } from './useImmediateState'; | ||
|
||
it('updates state immediately during mount', () => { | ||
let initialState; | ||
let update = 0; | ||
|
||
const setState = jest.fn(); | ||
|
||
const spy = jest.spyOn(React, 'useState').mockImplementation(state => { | ||
expect(state).toEqual({ x: 'x', test: false }); | ||
initialState = state; | ||
return [state, setState]; | ||
}); | ||
|
||
const { result } = renderHook(() => { | ||
const [state, setState] = useImmediateState({ x: 'x', test: false }); | ||
if (update === 0) setState(s => ({ ...s, test: true })); | ||
update++; | ||
return state; | ||
}); | ||
|
||
expect(setState).not.toHaveBeenCalled(); | ||
expect(result.current).toEqual({ x: 'x', test: true }); | ||
expect(result.current).toBe(initialState); | ||
expect(update).toBe(1); | ||
|
||
spy.mockRestore(); | ||
}); | ||
|
||
it('behaves like useState otherwise', () => { | ||
const setState = jest.fn(); | ||
const spy = jest | ||
.spyOn(React, 'useState') | ||
.mockImplementation(state => [state, setState]); | ||
|
||
renderHook(() => { | ||
const [state, setState] = useImmediateState({ x: 'x' }); | ||
React.useEffect(() => setState({ x: 'y' }), [setState]); | ||
return state; | ||
}); | ||
|
||
expect(setState).toHaveBeenCalledTimes(1); | ||
spy.mockRestore(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { useRef, useEffect, useState, useCallback } from 'react'; | ||
|
||
type SetStateAction<S> = S | ((prevState: S) => S); | ||
type SetState<S> = (action: SetStateAction<S>) => void; | ||
|
||
/** This is a drop-in replacement for useState, limited to object-based state. During initial mount it will mutably update the state, instead of scheduling a React update using setState */ | ||
export const useImmediateState = <S extends {}>(init: S): [S, SetState<S>] => { | ||
const isMounted = useRef(false); | ||
const initialState = useRef<S>({ ...init }); | ||
kitten marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const [state, setState] = useState<S>(initialState.current); | ||
|
||
// This wraps setState and updates the state mutably on initial mount | ||
// It also prevents setting the state when the component is unmounted | ||
const updateState: SetState<S> = useCallback((action: SetStateAction<S>) => { | ||
kitten marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (isMounted.current) { | ||
setState(action); | ||
kitten marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if (typeof action === 'function') { | ||
kitten marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const update = (action as any)(initialState.current); | ||
Object.assign(initialState.current, update); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, totally could be missing something, but don't we need to do the assignment to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, the first argument is being assigned to mutably 😀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right, I forgot that |
||
} else { | ||
Object.assign(initialState.current, action as any); | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
isMounted.current = true; | ||
return () => { | ||
isMounted.current = false; | ||
}; | ||
}, []); | ||
|
||
return [state, updateState]; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️this really is such a nice surprise!