Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can React useEffect watch and update state? [duplicate]

Example code

export function useShape (){
  const [state, setState] = useState({
    shape: 'square',
    size: {
      width: 100,
      height: 100
    }
  })

  // Change shape name while size update
  useEffect(()=>{
    const {size: {width, height}} = state

    setState({
      ...state,
      shape: width===height ? 'square' : 'rect'
    })
  }, [state, state.size])
}

Expected

When size updated, the side effect will change the size name based on width,height.

The goal is to make state consistent, so I will always get the correct shape no matter how size changes.

But got problem

The useEffect function got into a loop, if I remove the 'state' dependence it will be good, but the intelliSense requested 'state' dependence, so what's the solution?

like image 459
Yokiijay Avatar asked Jul 19 '20 10:07

Yokiijay


People also ask

Does useEffect update state?

Passing no 2nd argument causes the useEffect to run every render. Then, when it runs, it fetches the data and updates the state. Then, once the state is updated, the component re-renders, which triggers the useEffect again.

Can I use useEffect twice in React?

Yes, you read that right, it prints Call! twice. Specifically, this component is mounted, then unmounted, and then remounted. This also means that when you fetch data in useEffect, it will be sent twice!

Can I use useEffect twice in same component?

If you have just made a new project using Create React App or updated to React version 18, you will notice that the useEffect hook is called twice in development mode. This is the case whether you used Create React App or upgraded to React version 18.

How do you call only useEffect on update?

Use the Cleanup function of the useEffect without using an empty array as a second parameter: useEffect(() => { return () => { // your code to be run on update only. } }); You can use another useEffect (with an empty array as a second parameter) for initial mount, where you place your code in its main function.


2 Answers

The useEffect function got into a loop...

That's because you create a new object for state every time, and state is listed as a dependency.

When using hooks, instead of building multi-part state objects, you're usually better off using smaller pieces. In this case, I'd use three:

const [shape, setShape] = useState("square");
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);

useEffect(() => {
    setShape(width === height ? "square" : "rect");
}, [width, height]);

Now, what you're setting (shape) isn't a dependency of the effect hook, so it won't fire endlessly.

const {useState, useEffect} = React;

function Example() {
    const [shape, setShape] = useState("square");
    const [width, setWidth] = useState(100);
    const [height, setHeight] = useState(100);
    
    useEffect(() => {
        setShape(width === height ? "square" : "rect");
    }, [width, height]);

    function onWidthInput({target: {value}}) {
        setWidth(+value);
    }

    function onHeightInput({target: {value}}) {
        setHeight(+value);
    }

    return <div>
        <div>
            Width: <input type="number" value={width} onInput={onWidthInput} />
        </div>
        <div>
            Height: <input type="number" value={height} onInput={onHeightInput} />
        </div>
        <div>
            Shape: {shape}
        </div>
    </div>;
}

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

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

You can probably do that with your existing state object if you want, though:

useEffect(() => {
    setState(current => {
        const {size: {width, height}} = current;
        return {
            ...current,
            shape: width === height ? "square" : "rect"
        };
    });
}, [state.size, state.size.width, state.size.height]);

const {useState, useEffect} = React;

function Example() {
    const [state, setState] = useState({
        shape: 'square',
        size: {
            width: 100,
            height: 100
        }
    });

    useEffect(() => {
        setState(current => {
            const {size: {width, height}} = current;
            return {
                ...current,
                shape: width === height ? "square" : "rect"
            };
        });
    }, [state.size, state.size.width, state.size.height]);

    function onSizePropInput({target: {name, value}}) {
        setState(current => {
            return {
                ...current,
                size: {
                    ...current.size,
                    [name]: +value
                }
            };
        });
    }

    const {shape, size: {width, height}} = state;
    return <div>
        <div>
            Width: <input type="number" name="width" value={width} onInput={onSizePropInput} />
        </div>
        <div>
            Height: <input type="number" name="height" value={height} onInput={onSizePropInput} />
        </div>
        <div>
            Shape: {shape}
        </div>
    </div>;
}

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

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

Note the use of the callback form of setState in that. You want the then-current version of the state, not the state as it was when the effect callback was first created, since part of what you use for the update isn't a dependency (and so may be stale).

like image 148
T.J. Crowder Avatar answered Sep 28 '22 04:09

T.J. Crowder


You can check out this sandbox link for the solution

The infinite loop is due to placing "state" as a dependency while modifying the state itself in useEffect.

The solution is to decouple your state, by keeping your view variable separate from your controlled variables.

this is how you can define your useShape hook.

function useShape() {
  const [shape, setShape] = useState("square");
  const [dimension, setDimension] = useState({
    width: 100,
    height: 100
  });

  // Change shape name while size update
  useEffect(() => {
    const { height, width } = dimension;

    setShape(height === width ? "square" : "react");
  }, [dimension, dimension.height, dimension.width]);

  return [shape, setDimension];
}

this way you can expose your Dimension setter, and you view variable as independent Pieces.

like image 26
Monika Aggarwal Avatar answered Sep 28 '22 05:09

Monika Aggarwal