Search code examples
javascriptreactjsreact-hooksreact-state

ReactJS: State is not working inside nested function


So I tried to modify the tagList using setTagList(). It changed the content of the page (it adds tags in the <Filter /> component) but I cannot implement that each tag in <Filter /> is unique.

import Filter from "./components/Filter";
import Card from "./components/Card";
import data from "./assets/data.json";
import { useState, useEffect } from "react"


export default function App() { 
  const [cards, setCards] = useState([]);
  const [tagList, setTagList] = useState([]);
  

  useEffect(() => {
    generateCards();
  }, []);

  function addTag(newTag) {
    // console.log(tagList) always outputs []

    if (!tagList.includes(newTag)) {
      setTagList(oldTagList => [...oldTagList, newTag])
      // console.log(tagList) here always outputs []
      // But if I change "const [tagList, setTagList] = useState([]);" to "let [tagList, setTagList] = useState([]);" and add "tagList = [...tagList, newTag]" here, it works
    }  
  }  

  function deleteTag(tag) {
    setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
  }

  function clearTags() {
    setTagList([]);
  }

  function generateCards() {
    setCards(
      data.map((cardData) => (
        <Card
          key={cardData.id}
          logo={cardData.logo}
          company={cardData.company}
          isNew={cardData.new}
          isFeatured={cardData.featured}
          position={cardData.position}
          postedAt={cardData.postedAt}
          contract={cardData.contract}
          location={cardData.location}
          tags={[
            cardData.role,
            cardData.level,
            ...cardData.languages,
            ...cardData.tools,
          ]}
          addTag={addTag}
          filter={filter}
        />
      ))
    );
  }

  // console.log(tagList) works normally

  return (
    <div className="bg-bg-color h-screen">
      <div className="relative bg-wave-pattern w-full h-28 text-bg-color bg-primary">
        {tagList.length !== 0 && (
          <Filter tagList={tagList} deleteTag={deleteTag} clearTags={clearTags} />
        )}
      </div>
      <div className="2xl:px-96 xl:px-64 lg:px-32 md:px-16 sm:px-4 py-16 space-y-8">{cards}</div>
    </div>
  );
}

I tried to console.log(tagList) in the addTag() function but it always returns the default value. But when console.log(tagList) outside the function, it works like normal. But when I changed the const keyword to let when setting the state and add "tagList = [...tagList, newTag]" right after setTagList(...), it works. Is React broken?


Solution

  • The generateCards function is only called from the useEffect above, which has no dependencies, meaning it's only called when the component is mounted.

    This means that the addTag callback prop from the Card component always references the same addTag function that was created on component mount, so its closure only sees the initial tagList value ([]).

    One quick fix might be to add tagList as dependency to the useEffect that calls generateCards, so that all cards are re-rendered every time tagList changes, making the newly rendered cards reference the most recent addTag function that also has the most recent tagList value in its context:

    const data = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4']
    
    function App() { 
      const [cards, setCards] = React.useState([]);
      const [tagList, setTagList] = React.useState([]);
    
      React.useEffect(() => {
        generateCards();
      }, [tagList]); // <= ONLY CHANGED THIS LINE
    
      function addTag(e) {
        const newTag = e.currentTarget.textContent;
    
        if (!tagList.includes(newTag)) {
          setTagList(oldTagList => [...oldTagList, newTag])
        }  
      }  
    
      function deleteTag(e) {
        const tag = e.currentTarget.textContent;
        
        setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
      }
    
      function clearTags() {
        setTagList([]);
      }
    
      function generateCards() {    
        setCards(
          data.map((tag) => {
            const isSelected = tagList.includes(tag);
            
            return (
              <button
                key={tag}
                onClick={ isSelected ? deleteTag : addTag }
                className={ `card${ isSelected ? ' selected' : '' }` }>
                { tag }
              </button>
            )
          })
        );
      }
    
      return (
        <div>
          {cards}
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.querySelector('#app'));
    body,
    button {
      font-family: monospace;
    }
    
    #app {
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
    }
    
    .card {
      display: block;
      background: white;
      padding: 8px 16px;
      border: 2px solid black;
      border-radius: 3px;
    }
    
    .card + .card {
      margin-top: 8px;
    }
    
    .card.selected {
      border-color: blue;
    }
    <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
    <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
    
    <div id="app"></div>

    However, I would advise you to use useMemo rather than storing JSX in the state, as well as updating your addTag function to check if a tag is included in the currently selected ones that come from the previous state, rather than using the tagList from the function's context:

    const data = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4']
    
    function App() { 
      const [tagList, setTagList] = React.useState([]);
    
      function addTag(e) {
        const tag = e.currentTarget.textContent;
    
        // CHANGED THIS:
        setTagList((oldTags) => oldTags.includes(tag) ? oldTags : [...oldTags, tag])
      }  
    
      function deleteTag(e) {
        const tag = e.currentTarget.textContent;
        
        setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
      }
    
      function clearTags() {
        setTagList([]);
      }
        
      // CHANGED THIS:
      const memoizedCards = React.useMemo(() => {
        return data.map((tag) => {
          const isSelected = tagList.includes(tag);
    
          return (
            <button
              key={tag}
              onClick={ isSelected ? deleteTag : addTag }
              className={ `card${ isSelected ? ' selected' : '' }` }>
              { tag }
            </button>
          )
        })
      }, [tagList])
    
      return (
        <div>
          {memoizedCards}
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.querySelector('#app'));
    body,
    button {
      font-family: monospace;
    }
    
    #app {
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
    }
    
    .card {
      display: block;
      background: white;
      padding: 8px 16px;
      border: 2px solid black;
      border-radius: 3px;
    }
    
    .card + .card {
      margin-top: 8px;
    }
    
    .card.selected {
      border-color: blue;
    }
    <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
    <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
    
    <div id="app"></div>