Home > Software design >  React function takes two button clicks to run
React function takes two button clicks to run

Time:11-01

I have an array of Notes that I get from my database, the notes objects each have a category assigned to it. There are also buttons that allow the user to filter the notes by category and only render the ones with the corresponding one.

Now, it's all working pretty well but there's one annoying thing that I can't get rid of: whenever I click on any of the buttons: <button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>, the filterNotes() function is only called on the second click. I suspect it has to do something with me calling setState() twice, or maybe with the boolean that I set in the functions, but I tried various combinations to call the function on the first click, but to no avail so far.

Here's my MainArea code:

import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import Note from "./Note";
import axios from "axios"

function CreateArea(props) {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [filterOn, setFilter] = useState(false)

const [note, setNote] = useState({
  title: "",
  content: "",
  category: ''
    });

useEffect(() => {
  fetch('http://localhost:5000/categories')
  .then(res => res.json())
  .then(json => setCategories(json))
}, [])


useEffect(() => {
  if(fetchB) {
    fetch('http://localhost:5000/notes')
    .then(res => res.json())
    .then(json => {
      console.log(json)
      setNotes(json)
      setFetch(false)
    })
  }
}, [fetchB])

function handleChange(event) {
const { name, value } = event.target;
console.log("handleChange called")
setNote(prevNote => {
return {
        ...prevNote,
        [name]: value
      };
    });
  }

function submitNote(e){
      e.preventDefault();
      axios.post("http://localhost:5000/notes/add-note", note)
           .then((res) => {
      setNote({
      category: '',
      title: "",
      content: ""
              })
      setFetch(true)
      console.log("Note added successfully");
      console.log(note)
            })
            .catch((err) => {
      console.log("Error couldn't create Note");
      console.log(err.message);
            });

        }


    function expand() {
    setExpanded(true);
      }

    function filterNotes(category){
        if(filterOn){
          fetch('http://localhost:5000/notes')
          .then(res => res.json())
          .then(json => {
            console.log("filter notes")
            setNotes(json)
            setNotes(prevNotes => {
              console.log("setNotes called with category "   category)
              return prevNotes.filter((noteItem) => {
                return noteItem.category === category;
              });
            });
            setFilter(false)
          })
        }   
    }

    

      return (
      <div>
          <Header/>
          <ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFilter={setFilter} filterOn={filterOn} setFetch={setFetch}/>
            <form className="create-note">
      {isExpanded && (
                <input
      name="title"
      onChange={handleChange}
      value={note.title}
      placeholder="Title"
      />
              )}
              <textarea
      name="content"
      onClick={expand}
      onChange={handleChange}
      value={note.content}
      placeholder="Take a note..."
      rows={isExpanded ? 3 : 1}
      />
                <select
                name="category"
      onChange={handleChange}
      value={note.category}>
      {
                      categories.map(function(cat) {
      return <option
      key={cat.category} value={cat.value} > {cat.category} </option>;
                      })
      }
      </select>

              <button onClick={submitNote}>Add</button>
      </form>

      <Note notes={notes} setFetch={setFetch}/>
            <Footer/>
            <button onClick={()=>{setFetch(true)}}>All</button>
      </div>
        );
      }
export default CreateArea;

And ListCategories where I get call the function and get the chosen category from the buttons:

import React, { useState } from "react";
import CreateCategory from "./CreateCategory";

export default function ListCategories(props) {

    function handleClick(category){
        props.setFilter(true)
        props.filterNotes(category)
    }
 
    return (
        <div className="category-group">
        <CreateCategory/>
        <div className="btn-group">
        {props.categories.map((categoryItem, index) =>{
             return(
                <button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>
        )
    })}
        </div>
        </div>
    )
    }

I'm not sure what the best practice is with such behaviour - do I get the notes from the database each time as I'm doing now or should I do something completely different to avoid the double-click function call?

Thanks in advance for any suggestions!

CodePudding user response:

Your issue is this function:

function handleClick(category){
     props.setFilter(true)
     props.filterNotes(category)
 }

Understand that in React, state is only updated after the current execution context is finished. So in handleClick() when you call setFiler(), that linked filterOn state is only updated when the rest of the function body finishes.

so when your filterNotes() function is called, when it evaluates filterOn, it is still false, as it was initially set. After this function has executed, the handleClick() function has also finished, and after this, the filterOn state now equals true

This is why on the second click, the desired rendering effect occurs.


There are multiple ways to get around this, but I normally use 'render/don't-render' state by including it as an embedded expression in the JSX:

<main>
  {state && <Component />}
</main>

I hope this helps.

CodePudding user response:

You diagnosed the problem correctly. You shouldn't be using state like you would a variable. State is set asynchronously. So, if you need to fetch some data and filter it, do that and THEN add the data to state.

function filterNotes(category){
    fetch('http://localhost:5000/notes')
        .then(res => res.json())
        .then(json => {
            const filtered = json.filter((noteItem) => (noteItem.category === category));
            setNotes(filtered);
        })    
    }   
}

It's not clear to me why you would need the filterOn state at all.

CodePudding user response:

Depending on how your frequently your data is updated and if you plan on sharing data across users, the answer to this question will vary.

If these notes are specific to the user then you should pull the notes on load and then store them in a local state or store. Write actions that can update the state or store so that this isn't coupled with your react UI rendering. Example: https://redux.js.org/ or https://mobx.js.org/README.html.

Then update that store and your remote database accordingly through dispatching actions. This avoids lots of calls to the database and you can perform your filtering client-side as well. You can then also store data locally for offline use through this method so if it's for a mobile app and they lose internet connection, it'll still render. Access the store's state and update your UI based on that. Specifically the notes and categories.

If you have multiple users accessing the data then you'll need to look at using websockets to send that data across clients in addition to the database. You can add listeners that look for this data and update that store or state that you will have created previously.

There are many approaches to this, this is just an approach I would take. You could also create a context and provider that maintains your state on the first load and persists after that. Then you can avoid passing down state handlers through props

  • Related