Search code examples
reactjsstateuse-effectuse-state

useState updates state on second click


I've seen that this issue has been asked many times before but none of the answers make sense to me. After trying about every solution i could find, decided i'd just ask it myself.

Below is my code, the main issue being that in the DOM filteredData only changes on second click. Note, projs is the prop containing data fetched that gets filtered out and displayed

  const langs = ["react", "next js", "material-ui", "tailwind css", "firebase"];

  const [projectData, setProjectData] = useState([]);
  const [filteredData, setFilteredData] = useState([]);
  const [category, setCategory] = useState("");

  useEffect(() => {
    setProjectData(projs);
  }, []);

  const handleCategory = (res) => {
    setCategory(res.data);
    const filter = projectData.filter((data) => data.category === category);
    setFilteredData(filter);
  };

button:

          {langs.map((data, index) => (
            <button
              key={index}
              class="px-2 sm:px-6 py-2 ring-2 font-semibold ring-portfBtnLight rounded-md transform hover:scale-110 transition duration-500"
              onClick={() => handleCategory({ data })}
            >
              {data}
            </button>
          ))}

I'm pretty lost at the moment, any help would be greatly appreciated


Solution

  • You can't use the newly set value of a state in the same render cycle. To continue calling both setState calls in the same handler you need to use the category from res.data to generate the new filtered array and then set both.

    const handleCategory = (res) => {
      const filter = projectData.filter((data) => data.category === res.data);
      setCategory(res.data);
      setFilteredData(filter);
    };
    

    const { useState, useEffect } = React;
    
    function App({ projs }) {
      const langs = ["react", "next js", "material-ui", "tailwind css", "firebase"];
    
      const [projectData, setProjectData] = useState([]);
      const [filteredData, setFilteredData] = useState([]);
      const [category, setCategory] = useState("");
    
      useEffect(() => {
        setProjectData(projs);
      }, [projs]); // need to watch for change as props won't update this automatically. Setting state from props is somewhat of an anti-pattern
    
      const handleCategory = (res) => {
        const filter = projectData.filter((data) => data.category === res.data);
        setCategory(res.data);
        setFilteredData(filter);
      };
    
      return (
        <div className="App">
          {langs.map((data) => (
            <button
              key={data} // avoid using index as key as this will lead to erros in updating.
              class={(data === category ? 'selected ' : '') + "px-2 sm:px-6 py-2 ring-2 font-semibold ring-portfBtnLight rounded-md transform hover:scale-110 transition duration-500"}
              onClick={() => handleCategory({ data })}
            >
              {data}
            </button>
          ))}
          <div>
            <h3>{category}</h3>
            {filteredData.length
              ? filteredData.map(p => (
                <div>
                  <p>{p.value} – {p.category}</p>
                </div>
              ))
              : <p>No projects match</p>}
          </div>
        </div>
      );
    }
    
    const projectData = [{ id: 1, value: 'Project 1', category: 'react' }, { id: 2, value: 'Project 2', category: 'next js' }, { id: 3, value: 'Project 3', category: 'react' }, { id: 4, value: 'Project 4', category: 'material-ui' }]
    
    ReactDOM.render(
      <App projs={projectData} />,
      document.getElementById("root")
    );
    .selected {
      background-color: tomato;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>


    Alternatively, update filteredData in a useEffect that watches for changes to either category or projectData

    useEffect(() => {
      setFilteredData(projectData.filter((data) => data.category === category));
    }, [category, projectData]);
    
    const handleCategory = (res) => {
      setCategory(res.data);
    };
    

    const { useState, useEffect } = React;
    
    function App({ projs }) {
      const langs = ["react", "next js", "material-ui", "tailwind css", "firebase"];
    
      const [projectData, setProjectData] = useState([]);
      const [filteredData, setFilteredData] = useState([]);
      const [category, setCategory] = useState("");
    
      useEffect(() => {
        setProjectData(projs);
      }, [projs]); // need to watch for change as props won't update this automatically. Setting state from props is somewhat of an anti-pattern
    
      useEffect(() => {
        setFilteredData(projectData.filter((data) => data.category === category));
      }, [category, projectData]);
    
      const handleCategory = (res) => {
        setCategory(res.data);
      };
    
      return (
        <div className="App">
          {langs.map((data) => (
            <button
              key={data}
              class={(data === category ? 'selected ' : '') + "px-2 sm:px-6 py-2 ring-2 font-semibold ring-portfBtnLight rounded-md transform hover:scale-110 transition duration-500"}
              onClick={() => handleCategory({ data })}
            >
              {data}
            </button>
          ))}
          <div>
            <h3>{category}</h3>
            {filteredData.length
              ? filteredData.map(p => (
                <div>
                  <p>{p.value} – {p.category}</p>
                </div>
              ))
              : <p>No projects match</p>}
          </div>
        </div>
      );
    }
    
    const projectData = [{ id: 1, value: 'Project 1', category: 'react' }, { id: 2, value: 'Project 2', category: 'next js' }, { id: 3, value: 'Project 3', category: 'react' }, { id: 4, value: 'Project 4', category: 'material-ui' }]
    
    ReactDOM.render(
      <App projs={projectData} />,
      document.getElementById("root")
    );
    .selected {
      background-color: tomato;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>


    As noted in the snippets –

    Setting state from props is a bit of anti-pattern, but if you must you should add the prop to the dependency array so that state will be updated should the prop change. (prop changes don't force remounting).

    Also, avoid using index as key, especially when mapping arrays whose order/membership will change as React will not update as expected.