cccn
cccn

Reputation: 949

Parent's methods in array dependencies in useCallback

Let say we have parent and children components like this:

const {useState, useCallback} = React;

const ComponentB = (props) => {
    const [text, setText] = useState('')
    const { onClick } = props

    const handleChange = useCallback((event) => {
        setText(event.target.value)
    }, [text])

    const handleClick = useCallback(() => {
        onClick(text)
    }, [onClick, text]) // Should I to take into account 'onClick' props?

    return (
        <div>
            <input type="text" onChange={ handleChange } />
            <button type="button" onClick={ handleClick }>Save</button>
        </div>
    )

}

const ComponentA = () => {
    const [stateA, setStateA] = useState('')

    const handleSetStateA = useCallback((state) => {
        setStateA(state)
    }, [stateA])

    return (
        <div>
            <ComponentB onClick={ handleSetStateA } />
            { stateA && `${ stateA } saved!` }
        </div>
    )
}

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <ComponentA />
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

React documentation says that:

every value referenced inside the callback should also appear in the dependencies array

And I'm wondering if I need to put onClick method to array dependencies in useCallback? And if so, why I should do that?

Upvotes: 2

Views: 374

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074248

And I'm wondering if I need to put onClick method to array dependencies in useCallback?

Yes.

And if so, why I should do that?

Because it's possible that your component will get re-rendered with a new and different function for the onClick prop that behaves differently from the old one. Without saying it's a dependency, you'll continue using the old value.

In fact, in your given code, it's not just possible but definite: you create a new handleSetStateA function every time stateA changes.

That said, in ComponentA:

  1. There's no reason to have stateA as a dependency in your useCallback creating handleSetStateA; handleSetStateA never uses stateA. (It uses the state setter function for it, but that's not the same thing.)

  2. There's not really any reason for handleSetStateA at all; just pass setStateA directly as onClick. But I'm assuming you do more than just setting the state in that function and just left things out for the purposes of the question.

(Similarly, in ComponentB there's no reason for text to be a dependency on the useCallback for handleChange; handleChange doesn't use text.)

But even if you change ComponentA to pass setStateA directly (or at least provide a stable function), ComponentB shouldn't rely on onClick being unchanging between renders, so you'd use onClick in your useCallback dependency list regardless.

Finally: There's not much point in using useCallback with functions you're passing to unmemoized components. For it to be useful, the component you're providing the callback function to should be memoized (for instance, via React.memo or, for a class component, via shouldComponentUpdate). See my answer here for details on that.

Here's an updated version of your snippet using React.memo and only the necessary dependencies; I've left handleSetStateA in (I added a console.log so it isn't just a wrapper):

const { useState, useCallback } = React;

const ComponentB = React.memo(({ onClick }) => {
    const [text, setText] = useState("");

    const handleChange = useCallback((event) => {
        setText(event.target.value);
    }, []);

    const handleClick = useCallback(() => {
        console.log(`Handling click when text = "${text}"`);
        onClick(text);
    }, [onClick, text]);

    return (
        <div>
            <input type="text" onChange={handleChange} />
            <button type="button" onClick={handleClick}>
                Save
            </button>
        </div>
    );
});

const ComponentA = () => {
    const [stateA, setStateA] = useState("");

    const handleSetStateA = useCallback((state) => {
        console.log(`Setting stateA to "${state}"`);
        setStateA(state);
    }, []);

    return (
        <div>
            <ComponentB onClick={handleSetStateA} />
            {stateA && `${stateA} saved!`}
        </div>
    );
};

ReactDOM.createRoot(document.getElementById("root")).render(<ComponentA />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

Upvotes: 2

Related Questions