2

Consider the following React component:

interface Props {
    cpi: number;
    onCpiChange: (value?: number) => void;
}

const Assumption: FunctionComponent<Props> = (props: Props) => {
    const [cpiValue, setCpiValue] = useState<number>();

    useEffect(() => {
        if (props.cpi != cpiValue) {
            setCpiValue(props.cpi);
        }
    }, [props.cpi]);

    return (
        <FormattedNumberInput
            value={cpiValue}
            onBlur={() => props.onCpiChange(cpiValue)}
            onValueChange={setCpiValue}
        />
    );
};

export default Assumption;

The useEffect is complaining that it's missing the cpiValue - and as it's not in the dependency array it doesn't update when comparing using props.cpi != cpiValue. I don't want this useEffect to trigger every time the cpiValue changes. How would I have this useEffect respond only to when props.cpi changes yet still have access to the other variables required such as cpiValue with which I'm comparing against?

Additionally when initially learning React Hooks I learned that there are ways of controlling when a useEffect will trigger as follows:

  1. If you don't include a dependency array it will update on every render.
  2. If you provide an empty array it should update only on the first time the component loads
  3. If you provide any members inside the array the useEffect will trigger every time that one of these variables were to update.

The only one of these that makes sense is the third option. Options one and two have no scope to variables outside of the useEffect if you don't include them in the dependency array, but then adding the required variables to the dependency array changes the definition of the useEffect from 1 or 2 to 3. Am I missing something here?

2 Answers 2

3

You can move the conditional test into the state updater and this should remove it as a dependency. Using a functional state update you can use the cpiValue of the previous state and use a ternary to either return new cpi props value or the previous state cpiValue value.

useEffect(() => {
  setCpiValue(cpiValue => props.cpi !== cpiValue ? props.cpi : cpiValue);
}, [props.cpi]);

In my opinion I don't think the conditional test is necessary as it seems any time props.cpi updates and triggers the useEffect callback that you currently only enqueue an update if it isn't already equal to the current state. Why not just always update the local state cached version of props.cpi when it updates?

useEffect(() => {
  setCpiValue(props.cpi);
}, [props.cpi]);

If they are actually already equal then they will still be equal after. React can bail out of state updates if the value is the same.

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

Sign up to request clarification or add additional context in comments.

2 Comments

That's very helpful, thanks, and has definitely helped me out of this bind. I do question how you'd solve it for if it were another non-related value say that wasn't going to be accessed via a previous state? With respect to state updates, I wasn't aware of React bailing out of state updates if the value is the same. I've always ended up in infinite loops, but I'll give it another look. Thanks again.
@Misanthropist I think the typical cause of render looping is unconditionally updating a value (like state) in an useEffect callback that is part of the effect's dependency array. In the answer there is no cycle between updating local state and dependency array.
0

The useState will trig rerender every time when it called. And the props changed will also trig the component rerender. May be you can use the useState(initialize).

interface Props {
    cpi: number;
    onCpiChange: (value?: number) => void;
}

const Assumption: FunctionComponent<Props> = (props: Props) => {
    const [cpiValue, setCpiValue] = useState<number>(props.cpi);

    // useEffect(() => {
    //     if (props.cpi != cpiValue) {
    //         setCpiValue(props.cpi);
    //     }
    // }, [props.cpi]);

    return (
        <FormattedNumberInput
            value={cpiValue}
            onBlur={() => props.onCpiChange(cpiValue)}
            onValueChange={setCpiValue}
        />
    );
};

export default Assumption;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.