Search code examples
javascriptreactjsreact-router-domuse-effect

reactjs, no infinite loop, how come?


I don't get it, I thought with below code, when I visit Fruit.js page/component('/fruits/:slug' route), it should cause an infinite loop... but it didn't, why? (I am glad it didn't, but just curious why?)

App.js

const App = () => {
    const [dummyData, setDummyData] = useState([
        { slug: 'apple', name: 'Apple', color: 'Red' },
        { slug: 'watermelon', name: 'Watermelon', color: 'Green' },
        { slug: 'peach', name: 'Peach', color: 'Pink' },
        { slug: 'banana', name: 'Banana', color: 'Yellow' },
    ]);

    const [fruitSlug, setFruitSlug] = useState('');

    const getSlug = (slug) => {
        setFruitSlug(slug);
        console.log(slug);
    };

    return (
        <div className='App'>
            {dummyData.map((fruit) => (
                <span className='fruit-link' key={fruit.slug}>
                    <Link to={`/fruits/${fruit.slug}`}>{fruit.name}</Link>{' '}
                </span>
            ))}

            <Switch>
                <Route path='/' exact component={Home} />
                <Route
                    path='/fruits/:slug'
                    render={(props) => <Fruit {...props} getSlug={getSlug} />}
                />
            </Switch>
        </div>
    );
};

export default App;

Fruit.js

const Fruit = ({ getSlug }) => {
    const { slug } = useParams();

    useEffect(() => {
        getSlug(slug);
    }, [getSlug, slug]);

    return (
        <div className='fruit'>
            <h1>Fruit page</h1>
        </div>
    );
};

export default Fruit;

Solution

  • Let me start of by giving two similar examples, removing some of the problem overhead present in your current question.

    You are wondering why the useEffect callback in the snippet below does not trigger infinite re-renders.

    const { useState, useEffect } = React;
    
    function Example() {
      const [a, setA] = React.useState("");
      const [b, setB] = React.useState(a);
      
      useEffect(() => {
        setB(a);
      }); // <- without dependencies triggers every render
        
      return (
        <div>
          <input value={a} onChange={e => setA(e.target.value)} />
          <p>{b}</p>
        </div>
      );
    }
    
    ReactDOM.render(<Example />, document.querySelector("#root"));
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <div id="root"></div>

    While it does seem to do so when you pass a more complex object from for example an API.

    const { useState, useEffect } = React;
    
    function Example() {
      const [a, setA] = React.useState("");
      const [b, setB] = React.useState({ value: a });
      
      useEffect(() => {
        setB({ value: a });
      }); // <- without dependencies triggers every render
        
      return (
        <div>
          <input value={a} onChange={e => setA(e.target.value)} />
          <p>{b.value}</p>
        </div>
      );
    }
    
    ReactDOM.render(<Example />, document.querySelector("#root"));
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <div id="root"></div>

    This is due to the simple fact that useState has a "bail out" mechanism:

    Bailing out of a state update

    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.

    The first example would infinity trigger a re-render if it where not for this bail out mechanism. When React tries to set a state that is the same as the current state, it stops.

    From APIs we often receive JSON, that gives us an complex object when parsed. Two objects are never equal to each other, even if they contain the same contents.

    const a = { name: "John Doe" };
    const b = { name: "John Doe" };
    const c = a;
    
    Object.is(a, b) //=> false
    Object.is(a, c) //=> true
    

    In the above example, although a and b contain the same contents, they are not the same object and are thus not equal to each other. Whereas a and c are both the same object, they are just two different labels referring to the same object. If you where to mutate a, c would also change. Therefore they are considered equal.

    Parsing JSON always builds new objects and the result is therefore never equal (unless the JSON describes a primitive data type).

    const json = '{ "name": "John Doe" }';
    const a = JSON.parse(json);
    const b = JSON.parse(json);
    
    Object.is(a, b) //=> false