Last active
August 2, 2023 13:35
-
-
Save ypresto/4d78f7d9d30a46c2d44937a79ee84cef to your computer and use it in GitHub Desktop.
Hooks to mitigate calling setState from useEffect/useLayoutEffect. Zero-rerender on hook value change. See useOverrideValue.ts first. https://zenn.dev/ypresto/articles/02f7adcb7c57b4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useRef } from 'react' | |
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' | |
/** | |
* Hook that its return value increments when the flag is changed to true. | |
* You can use this with useOverrideValue() / useEffect() to dispatch only when specific condition is met, | |
* each time or at first (=== 1). | |
*/ | |
export function useFlipCount(flag: boolean) { | |
const ref = useRef({ flag, count: 0 }) | |
useIsomorphicLayoutEffect(() => { | |
ref.current.flag = flag | |
if (flag) { | |
ref.current.count++ | |
} | |
}, [flag]) | |
return flag && !ref.current.flag ? ref.current.count + 1 : ref.current.count | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useLayoutEffect } from 'react' | |
// Helper for next.js | |
// https://github.com/mui-org/material-ui/issues/15798#issuecomment-495078241 | |
const canUseDOM = | |
typeof window !== 'undefined' && | |
typeof window.document !== 'undefined' && | |
typeof window.document.createElement !== 'undefined' | |
export const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { renderHook, act } from '@testing-library/react-hooks' | |
import { useOverrideValue } from './useOverrideValue' | |
describe('useOverrideValue', () => { | |
test('returns default value when setValue() is not called', () => { | |
let defaultValue = 0 | |
const { result, rerender } = renderHook(() => useOverrideValue(defaultValue, [0])) | |
expect(result.current[0]).toBe(0) | |
defaultValue = 1 | |
rerender() | |
expect(result.current[0]).toBe(1) | |
}) | |
test('returns overridden value until deps is changed', () => { | |
let defaultValue = 0 | |
let dep = false | |
const { result, rerender } = renderHook(() => useOverrideValue(defaultValue, [dep])) | |
act(() => { | |
const [, setState] = result.current | |
setState(1) | |
}) | |
expect(result.current[0]).toBe(1) | |
defaultValue = 2 | |
rerender() | |
expect(result.current[0]).toBe(1) | |
dep = true | |
rerender() | |
expect(result.current[0]).toBe(2) | |
}) | |
test('supports multiple deps', () => { | |
let dep1 = false | |
let dep2 = '' | |
const { result, rerender } = renderHook(() => useOverrideValue(0, [dep1, dep2])) | |
act(() => { | |
const [, setState] = result.current | |
setState(1) | |
}) | |
dep1 = true | |
rerender() | |
expect(result.current[0]).toBe(0) | |
act(() => { | |
const [, setState] = result.current | |
setState(1) | |
}) | |
dep2 = 'foo' | |
rerender() | |
expect(result.current[0]).toBe(0) | |
}) | |
test('restoring deps to previous value does not return overridden value', () => { | |
let dep = false | |
const { result, rerender } = renderHook(() => useOverrideValue(0, [dep])) | |
act(() => { | |
const [, setState] = result.current | |
setState(1) | |
}) | |
dep = true | |
rerender() | |
expect(result.current[0]).toBe(0) | |
dep = false | |
rerender() | |
expect(result.current[0]).toBe(0) | |
}) | |
test('setValue with callback receives current value', () => { | |
let defaultValue = 0 | |
let dep = false | |
const { result, rerender } = renderHook(() => useOverrideValue(defaultValue, [dep])) | |
defaultValue = 1 | |
rerender() | |
act(() => { | |
const [, setState] = result.current | |
setState(value => { | |
expect(value).toBe(1) | |
return 2 | |
}) | |
}) | |
act(() => { | |
const [, setState] = result.current | |
setState(value => { | |
expect(value).toBe(2) | |
return 3 | |
}) | |
// next render uses new default value | |
defaultValue = 4 | |
dep = true | |
}) | |
act(() => { | |
const [, setState] = result.current | |
setState(value => { | |
expect(value).toBe(4) | |
return 0 | |
}) | |
}) | |
}) | |
test('ref of setValue does not change', () => { | |
let defaultValue = 0 | |
let dep = false | |
const { result, rerender } = renderHook(() => useOverrideValue(defaultValue, [dep])) | |
const [, setValue] = result.current | |
defaultValue = 1 | |
rerender() | |
expect(result.current[1]).toBe(setValue) | |
dep = true | |
rerender() | |
expect(result.current[1]).toBe(setValue) | |
act(() => { | |
const [, setState] = result.current | |
setState(2) | |
}) | |
expect(result.current[1]).toBe(setValue) | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useReducer, useRef } from 'react' | |
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' | |
/** | |
* useState() which resets to defaultValue when one of deps is changed. | |
*/ | |
export function useOverrideValue<T>(defaultValue: T, deps: unknown[] = [defaultValue]) { | |
type UpdateFunc = (prevState: T) => T | |
const [, forceUpdate] = useReducer(x => x + 1, 0) | |
const stateRef = useRef<{ defaultValue: T; deps: unknown[]; override?: { value: T; snapshot: unknown[] } }>({ | |
defaultValue, | |
deps, | |
}) | |
if (stateRef.current.override && deps.length !== stateRef.current.override.snapshot.length) { | |
throw new Error('useOverrideValue: deps array length should not be changed.') | |
} | |
const setValue = useRef((value: T | UpdateFunc) => { | |
const state = stateRef.current | |
const currentValue = state.override?.snapshot.every((v, i) => state.deps[i] === v) | |
? state.override.value | |
: state.defaultValue | |
const newValue = typeof value === 'function' ? (value as UpdateFunc)(currentValue) : value | |
stateRef.current.override = { value: newValue, snapshot: stateRef.current.deps } | |
if (newValue !== currentValue) { | |
forceUpdate() | |
} | |
}).current | |
useIsomorphicLayoutEffect(() => { | |
// These can be used by setOverride() call, but below code must be called earlier than useLayoutEffect() in children. | |
stateRef.current.override = undefined | |
stateRef.current.deps = deps | |
}, deps) | |
useIsomorphicLayoutEffect(() => { | |
stateRef.current.defaultValue = defaultValue | |
}, [defaultValue]) | |
const value = stateRef.current.override?.snapshot.every((v, i) => deps[i] === v) | |
? stateRef.current.override.value | |
: defaultValue | |
return [value, setValue] as const | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment