skip to Main Content

I have two components in my react (ts) app.

App.tsx:

import { useState } from 'react'
import Number from './Number'
import './App.css'

function App() {
    const [list, setList] = useState(["1", "2", "3", "4"]);
    const [x, setX] = useState({});

    function ComponentList(){
        return(<>
            {list.map(i => <Number num={i} key={i}/>) }
        </>)
    }

    return (
        <div className="App">
            <ComponentList/>
            <div onClick={()=>{
                setX({});
            }}>Update</div>
        </div>
    )
}

export default App

Number.tsx:

import React, {useState} from 'react'

type props = {
    num: string
}

function Number({num}:props) {
    const [color, setColor] = useState("rgb(0, 0, 0)");

    return (
        <div style={{color: color}} onClick={()=>{
            setColor("rgb(255, 255, 255)");
        }}>
            {num}
        </div>
    )
}

export default Number

Here’s the problem:
When any state on the parent component (App.tsx) changes, the Number components in the list re-mount, resetting all their states to initial value. How can I get rid of this problem while keeping the list component?

The issue is very simple to re-create. When you use these two components in a react app and click the numbers to change their color, the color is reset upon clicking the "update" button, which seems illogical to me since the "setX" call doesn’t change anything about the list.

2

Answers


  1. function App() {
        // ...
    
        function ComponentList(){
            // ...
        }
    }
    

    You’ve defined ComponentList inside of App. This isn’t supported. Every time App rerenders, you create a brand new definition for ComponentList. Its code may have the same text as the previous one, but it’s a different function, so a different component type as far as react is concerned. So react has to unmount the old ones and mount the new ones.

    The fix is to move the component out. You’ll need to change list to be a prop instead of a closure variable

    function App() {
      const [list, setList] = useState(["1", "2", "3", "4"]);
      // ...
      <ComponentList list={list}/>
      // ...
    }
    
    function ComponentList({ list }){
      return(<>
        {list.map(i => <Number num={i} key={i}/>) }
      </>)
    }
    
    Login or Signup to reply.
  2. You created your ComponentList inside the parent, and since you don’t use useCallback, it is recreated when the parent re-render.
    Here is how to implement both solutions, but the best one is to take your component outside of the parent :

    import React from 'react';
    import './style.css';
    
    // Solution 2
    function ComponentList({ list }) {
      return (
        <>
          {list.map((i) => (
            <Number num={i} key={i} />
          ))}
        </>
      );
    }
    
    export default function App() {
      const [list, setList] = React.useState(['1', '2', '3', '4']);
      const [x, setX] = React.useState({});
    
      // Solution 1
      // const ComponentList =  React.useCallback(() => {
      //     return(<>
      //         {list.map(i => <Number num={i} key={i}/>) }
      //     </>)
      // }, [])
    
      return (
        <div className="App">
          {/* <ComponentList /> solution 1 */}
          <ComponentList list={list} /> {/* Solution 2 */}
          <div
            onClick={() => {
              setX({});
            }}
          >
            Update
          </div>
        </div>
      );
    }
    
    function Number({ num }) {
      const [color, setColor] = React.useState('rgb(0, 0, 0)');
    
      return (
        <div
          style={{ color: color }}
          onClick={() => {
            setColor('rgb(255, 255, 255)');
          }}
        >
          {num}
        </div>
      );
    }
    
    

    Here is the repro on Stackblitz
    Also, here is a video of Jeff Herrington about this exact problem.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search