Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React useState hook with dependency

TL;DR

Why there is no dependency array for useState(), something like:

const [state, setState] = useState<T>(initialState, [initialState]);

Question

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:

  1. First to render its parent's children (due to initialState change). In this cycle, the useEffect update is added to the queue to be performed after rendering.
  2. and then a second type after the 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?

like image 595
Newbie Avatar asked Jul 02 '26 15:07

Newbie


1 Answers

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

like image 179
Andrey Smolko Avatar answered Jul 05 '26 04:07

Andrey Smolko