Why there is no dependency array for useState(), something like:
const [state, setState] = useState<T>(initialState, [initialState]);
In React I often end up in this situation
export function EditComponent<T>(props: {
initialState: T,
onSave: (value: T) => void,
}) {
const { initialState, onSave } = props;
const [state, setState] = useState<T>(initialState);
useEffect(() => setState(initialState), [initialState]);
function revert() {
setState(initialState);
}
function save() {
onSave(state);
}
return (
...
)
}
Where I have an outer component that provides some data and an inner component that lets the user edit it (by modifying a copy of the data) to then eventually save or revert. Of course, I need the inner component to be reactive to any outer change (maybe new data has been fetched from the network or whatever).
What I dislike is doing this:
const [state, setState] = useState<T>(initialState);
useEffect(() => setState(initialState), [initialState]);
Cause from my understanding of React this is rendering the component twice when initialState changes:
initialState change). In this cycle, the useEffect update is added to the queue to be performed after rendering.useEffect update has been performed.What I need is a dependency array on useState, to do:
const [state, setState] = useState<T>(initialState, [initialState]);
It seems something straightforward, the same as initialState is synchronously consumed, and made available, during the first rendering cycle, when the dependency list changes this operation shall be performed again.
I attempted to implement it myself, but what I came up with seems more like a hack:
import { DependencyList, Dispatch, SetStateAction, useRef, useState } from 'react';
interface MemoContext<S> {
deps: DependencyList | undefined;
state?: S
}
// Is dependency list equal (L327 areHookInputsEqual)
function areHookInputsEqual(a: DependencyList | undefined, b: DependencyList | undefined): boolean {
if (!a) {
console.error('Prev deps should not be null')
return false;
} else if (!b) {
return false;
}
for (let i = 0; i < a.length && i < b.length; i++) {
if (!Object.is(a[i], b[i])) {
return false;
}
}
return true;
}
export function useMemoState<S>(
initialState: S | (() => S),
deps?: DependencyList,
): [S, Dispatch<SetStateAction<S>>] {
function resetInitialState() {
const s: S = typeof initialState === 'function' ? (initialState as any)() : initialState;
ctx.state = s;
ctx.deps = deps;
return s;
}
const ctx = useRef<MemoContext<S>>({ deps: undefined, state: undefined }).current;
// this is actually used just to preserve the rendering behaviour
const [state, setState] = useState<S>(resetInitialState);
if (!areHookInputsEqual(ctx.deps, deps)) {
// They are different, perform the update
resetInitialState()
}
function dispatch(action: SetStateAction<S>) {
setState(prevState => {
const s: S = typeof action === 'function' ? (action as any)(prevState) : action;
ctx.state = s;
ctx.deps = deps;
return s;
})
}
return [ctx.state!, dispatch];
}
To be honest React Core seems like something that should not be touched. So I'm wondering if I'm missing something and if there is a clear reason why such a feature does not exist. Or maybe there is a better solution to this?
A short answer - we do not need it=)
This is your refactored code snippet:
export function EditComponent<T>(props: {
initialState: T,
onSave: (value: T) => void,
}) {
const { initialState, onSave } = props;
const [state, setState] = useState<T>(initialState);
if (initialState !== state){
setState(initialState);
}
function revert() {
setState(initialState);
}
function save() {
onSave(state);
}
return (
...
)
}
Just conditionally update state during rendering.
There are 2 links which you may find helpful:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With