Search code examples
reactjsonclickgatsbysetstate

React hooks with Gatsby. onClick changes setState on second Click only


im making a personal site of sorts, where I have a bunch of stylized cards rendering through a map of an array created with GraphQL query.

Those Cards have a tag, and my objective is that when you click said tag, it will filter the original array leaving only the items which include the tag.

Goes Something like this


const Tutoriales = ()=> {




    const blog = useStaticQuery(graphql`
    query {
        allMarkdownRemark (
          sort: {fields: frontmatter___date, order: DESC}

         )


        {
            edges {
                node { 
                    frontmatter {
                        title,
                        date,
                        type,
                        abs,
                        tag,
                        featuredImage {
                            relativePath,
                            absolutePath,
                            childImageSharp{
                                fixed(width: 300) {
                                    ...GatsbyImageSharpFixed
                                }
                            }
                        }

                    }
                    html,
                    excerpt,
                    fields {
                        slug
                    }
                }
            }
        }
    }
    `
    );


    let pijon = blog.allMarkdownRemark.edges;
    const [cards, setCards] = useState(pijon);
    const [filter, setFilter] = useState("");

 return (
<div>

{
            cards.map((edge)=> {

 <Card className={classes.card}>
      <CardHeader
                      avatar={
                        <Avatar aria-label="Recipe" className={classes.avatar}>
                          {edge.node.frontmatter.type}
                        </Avatar>
                      }

                      title={<Link className={center.nodecor} to={url}>{edge.node.frontmatter.title}</Link>}
                      subheader={<Link className={center.nodecor} to={url}>{edge.node.frontmatter.date} </Link>}

                    />

 // This is my tag DIV, Im trying to make it work so that when you click
 // on the div, it will use the Filter Hook, to filter the card array


 <div onClick={ () => {
            setFilter(edge.node.frontmatter.tag);


            console.log(filter)
            let filteredCards = cards.filter((card) => {
              return card.node.frontmatter.tag === filter
            }

            )
          setCards(filteredCards);

        }

        }
           className={center.right}>
              {edge.node.frontmatter.tag}  </div>
          </div>


 </Card>

    )
            })
            }

</div>

    )
}

export default Tutoriales



The expected result would be that the cards hook would be now filtered therefor making all of the cards that do not include the tag selected to dissappear.

But when doing this all I get is a blank array. I know it has something to do with the setState hook being asynchronous, and Ive been reading about the UseEffect, but I cant seem to make it work.

Using console.log I was able to realize, that only the the second time I click the tag (with the onClick value) it works.

Any comment is appreciated.


Solution

  • You are right, this happens beacuse setState calls are batched. So if you call setFilter and start using filter in the same block, you are actually using the previous value of it. That's the reason it works the second time.

    setFilter(edge.node.frontmatter.tag);
    console.log(filter);    <- filter has not actually changed yet
    

    The easiest fix would be to remove const [filter, setFilter] = useState(""); and just use the filter you selected directly:

    <div 
      onClick={() => {
        const filter = edge.node.frontmatter.tag;
        let filteredCards = cards.filter((card) => {
          return card.node.frontmatter.tag === filter
        });
        setCards(filteredCards);
      }}
      className={center.right}
    >
        {edge.node.frontmatter.tag}
    </div>
    

    Another solution would be to use the useEffect hook, and update the cards after the filter changed:

    const [cards, setCards] = useState(pijon);
    const [filter, setFilter] = useState("");
    useEffect(() => {
      if (filter) {  // don't do anyhiting when filter is empty on initial render
        const filteredCards = cards.filter((card) => {
          return card.node.frontmatter.tag === filter
        }
        setCards(filteredCards);
      }
    }, [filter, setCards]);
    
    // ...
    
    <div 
      onClick={() => {setFilter(edge.node.frontmatter.tag);}}
      className={center.right}
    >
        {edge.node.frontmatter.tag}
    </div>