skip to Main Content

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?

2

Answers


  1. 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>
    Login or Signup to reply.
  2. The issue here is that you only update the value of cards (using setCards) one time—right after the component mounts. This is because you only call generateCards within a useEffect hook with an empty dependency array so it only runs the first time the component renders. Since the value of cards is the one from the first render, the addTag function given to each card is the initial value of addTag, which references the initial value of tagList (an empty array).

    While you can fix this by adding tagList to the dependency array of the useEffect hook, a more straightforward way to handle this would be to put the conditional logic of addTag within the callback passed to setTagList, like this:

    function addTag(newTag) {
      setTagList(oldTagList => !oldTagList.includes(newTag) ? [...oldTagList, newTag] : oldTagList);
    }
    

    Additionally, there is no need to store cards in state, and doing so can introduce problems such as the one you have encountered here. If you need to optimize performance, you should use useMemo, but I would start out just rendering the cards directly in the return statement, like this:

    export default function App() { 
      const [tagList, setTagList] = useState([]);
    
      function addTag(newTag) {
        setTagList(oldTagList => !oldTagList.includes(newTag) ? [...oldTagList, newTag] : oldTagList)
      }  
    
      function deleteTag(tag) {
        setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
      }
    
      function clearTags() {
        setTagList([]);
      }
    
      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">
            {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}
              />
            ))}
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search