-
Notifications
You must be signed in to change notification settings - Fork 577
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
simplify syncing props to state in ThemeProvider
#4855
Changes from all commits
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,5 @@ | ||
--- | ||
'@primer/react': patch | ||
--- | ||
|
||
avoid useeffect when syncing theme config |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import {act, renderHook} from '@testing-library/react' | ||
import {useSyncedState} from '../useSyncedState' | ||
|
||
const renderUseSyncedState = ( | ||
initialValue: string | (() => string), | ||
{isPropUpdateDisabled}: {isPropUpdateDisabled: boolean} = {isPropUpdateDisabled: false}, | ||
) => { | ||
return renderHook(props => useSyncedState(props.initialValue, {isPropUpdateDisabled: props.isPropUpdateDisabled}), { | ||
initialProps: { | ||
initialValue, | ||
isPropUpdateDisabled, | ||
}, | ||
}) | ||
} | ||
test('it renders a default', () => { | ||
const {result} = renderUseSyncedState('default') | ||
expect(result.current[0]).toEqual('default') | ||
}) | ||
|
||
test('it updates state from the internal state setter', () => { | ||
const {result} = renderUseSyncedState('default') | ||
expect(result.current[0]).toEqual('default') | ||
act(() => { | ||
result.current[1]('new value') | ||
}) | ||
expect(result.current[0]).toEqual('new value') | ||
}) | ||
|
||
test('it updates state from the internal state setter with an updater fn', () => { | ||
const {result} = renderUseSyncedState('default') | ||
expect(result.current[0]).toEqual('default') | ||
act(() => { | ||
result.current[1](prev => `${prev} new value`) | ||
}) | ||
expect(result.current[0]).toEqual('default new value') | ||
}) | ||
|
||
test('it updates state from the external state setter', () => { | ||
const {result, rerender} = renderUseSyncedState('default') | ||
expect(result.current[0]).toEqual('default') | ||
|
||
rerender({initialValue: 'new value', isPropUpdateDisabled: false}) | ||
|
||
expect(result.current[0]).toEqual('new value') | ||
}) | ||
|
||
test('it properly handles init functions', () => { | ||
const {result, rerender} = renderUseSyncedState(() => 'default') | ||
expect(result.current[0]).toEqual('default') | ||
|
||
rerender({initialValue: () => 'new value', isPropUpdateDisabled: false}) | ||
|
||
expect(result.current[0]).toEqual('new value') | ||
}) | ||
|
||
test('it does not update from props if disabled', () => { | ||
const {result, rerender} = renderUseSyncedState('default', {isPropUpdateDisabled: true}) | ||
expect(result.current[0]).toEqual('default') | ||
|
||
rerender({initialValue: 'new value', isPropUpdateDisabled: true}) | ||
|
||
expect(result.current[0]).toEqual('default') | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import {useState} from 'react' | ||
|
||
/** | ||
* When the value that initialized the state changes | ||
* this hook will update the state to the new value, immediately. | ||
* | ||
* This uses an Object.is comparison to determine if the value has changed by default | ||
* | ||
* If you use a non-primitive value as the initial value, you should provide a custom isEqual function | ||
* | ||
* This is adapted almost directly from https://beta.reactjs.org/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes | ||
*/ | ||
|
||
export const useSyncedState = <T,>( | ||
initialValue: T | (() => T), | ||
{isPropUpdateDisabled = false, isEqual = Object.is} = {}, | ||
) => { | ||
const [state, setState] = useState(initialValue) | ||
const [previous, setPrevious] = useState(initialValue) | ||
|
||
const nextInitialValue = initialValue instanceof Function ? initialValue() : initialValue | ||
if (!isPropUpdateDisabled && !isEqual(previous, nextInitialValue)) { | ||
setPrevious(nextInitialValue) | ||
setState(nextInitialValue) | ||
} | ||
Comment on lines
+21
to
+25
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. these the main value is we save a full render from react on any change - a render we know will immediately be discarded |
||
|
||
return [state, setState] as const | ||
} |
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.
these are all synced automatically in the
useSyncedState
calls and avoiduseEffect
so we get to skip a render pass every time one changesThere 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.
also, I doubt these get hit often, but I saw this while looking at something related and just figured why not