Search code examples
javascriptreactjsreact-hooksuse-effect

How can I display changes in component instantly in React?


I'm building this website with MERN stack and having this rendering bug:

Component won't render out changes instantly on the searched table

  • On start, I have a foodList table rendering out all of the food in the database.

  • I already have a useEffect() with the foodList inside the dependency array - so anytime the users make changes to the foodList table (Add/Edit/Delete), it will instantly render out that added dish without refreshing the page.

  • When users search for something in this Search & Filter bar, it will hide the foodList table and return a table of searchedFood that is filtered from the foodList array.

  • But when the users use this Search & Filter functionality and then try to Edit/Delete from that searchedFood table. It won't render the changes instantly - they have to refresh the page to see the changes they made.

This might relate to the useEffect() but I don't know how to apply it for the searchedFood table without disrupting the foodList table.


App.js

export default function App() {
  const [foodName, setFoodName] = useState('')
  const [isVegetarian, setIsVegetarian] = useState('no')
  const [priceRange, setPriceRange] = useState('$')
  const [foodUrl, setFoodUrl] = useState('')

  const [foodList, setFoodList] = useState([])

  const [searchedFood, setSearchedFood] = useState([])

  const [noResult, setNoResult] = useState(false)

  
  
  // Display food list:
  useEffect(() => {
    let unmounted = false
    Axios.get("https://project.herokuapp.com/read")
    .then((response) => {
      if (!unmounted) {
        setFoodList(response.data)
      }
    })
    .catch(error => {
      console.log(`The error is: ${error}`)
      return
    })
    return () => {
      unmounted = true
    }
  }, [foodList])
  
  
  // Add Food to list:
  const addToList = async (event) => {//Axios.post logic in here}


    // Paginate states:
    const [currentPage, setCurrentPage] = useState(1)
    const [foodPerPage] = useState(5)
    
    // Get current food:
    const indexOfLastFood = currentPage * foodPerPage
    const indexOfFirstFood = indexOfLastFood - foodPerPage
    const currentFood = foodList.slice(indexOfFirstFood, indexOfLastFood)
    const currentSearchedFood = searchedFood.slice(indexOfFirstFood, indexOfLastFood)
    
    const paginate = (pageNumber) => {
      setCurrentPage(pageNumber)
    }
  
  return (
    <section>
      <FilterSearch
        foodList={foodList}
        searchedFood={searchedFood}
        setSearchedFood={setSearchedFood}
        noResult={noResult}
        setNoResult={setNoResult}
        paginate={paginate}
      />
      {noResult ? <ResultNotFound/>
        :
          <FoodListTable
            foodName={foodName}
            priceRange={priceRange}
            isVegetarian={isVegetarian}
            foodUrl={foodUrl}
            foodList={foodList}
            currentFood={currentFood}
            searchedFood={searchedFood}
            currentSearchedFood={currentSearchedFood}
            totalFood={foodList.length}
            totalSearchedFood={searchedFood.length}
            currentPage={currentPage}
            paginate={paginate}
            noResult={noResult}
            foodPerPage={foodPerPage}
          />
      }
    </section>
  )
}

FoodListTable.js

export default function FoodListTable(props) {
    return (
        <div>
            <table>
                <thead>
                    <tr>
                        <th>
                            Food name
                        </th>
                        <th>Price</th>
                        <th>
                            Action
                        </th>
                    </tr>
                </thead>
                <body>
             // Return a table with data from searchFood on search: 
                    {props.searchedFood.length > 0 ? props.currentSearchedFood.map((val) => {
                        return (
                            <FoodListRow
                                val={val}
                                key={val._id} 
                                foodName={val.foodName}
                                isVegetarian={val.isVegetarian}
                                priceRange={val.priceRange}
                                foodUrl={val.foodUrl}
                            />    
                        )
                    }) : props.currentFood.map((val) => { // If not on search, return a table with data from foodList:
                        return (
                            <FoodListRow
                                val={val}
                                key={val._id}
                                foodName={val.foodName}
                                isVegetarian={val.isVegetarian}
                                priceRange={val.priceRange}
                                foodUrl={val.foodUrl}
                            />
                        )
                        })
                    }
                </tbody>
            </table>
            // Display different Pagination on searched table and food list table:
            {props.searchedFood.length > 0 ? 
                <Pagination foodPerPage={props.foodPerPage} totalFood={props.totalSearchedFood} paginate={props.paginate} currentPage={props.currentPage} />
                :<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalFood} paginate={props.paginate} currentPage={props.currentPage} />
            }
        </div>
    )
}

FoodListRow.js

export default function FoodListRow(props) {
    // Edit food name:
    const [editBtn, setEditBtn] = useState(false)
    const handleEdit = () => {
        setEditBtn(!editBtn)
    }


    // Update Food Name:
    const [newFoodName, setNewFoodName] = useState('')
    const updateFoodName = (id) => {
        if (newFoodName) {
            Axios.put("https://project.herokuapp.com/update", {
                id: id,
                newFoodName: newFoodName,
            })
            .catch(error => console.log(`The error is: ${error}`))
        }
    }
    

    // Delete food:
    const deleteFood = (id) => {
        const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`); 
        if(confirm === true){ 
          Axios.delete(`https://project.herokuapp.com/delete/${id}`)
        }
    }

    return (
        <tr key={props.val._id}>
            <td>
                {props.val.foodName}
                {editBtn && 
                    <div>
                        <input
                            type="text"
                            name="edit"
                            placeholder="New food name.."
                            autoComplete="off"
                            onChange={(event) => {setNewFoodName(event.target.value)}}
                        />
                        <button
                        onClick={() => updateFoodName(props.val._id)}
                        >
                            ✓
                        </button> 
                    </div>
                }
            </td>
            <td>{props.val.priceRange}</td>
            <td>
                <a 
                    href={props.val.foodUrl} 
                    target="_blank"
                    rel="noopener noreferrer" 
                >
                    🔗
                </a>
                <button 
                    onClick={handleEdit}
                >
                    ✏️
                </button>
                <button 
                    onClick={() => deleteFood(props.val._id)}
                >
                    ❌
                </button>
            </td>
        </tr>
    );
}

Solution

  • As Mohd Yashim Wong mentioned, we need to re-render every time there's change to the backend.

    I ditched the foodList inside the useEffect()'s dependency array and try another method because this is not the correct way to re-render the axios calls. It just keeps sending read requests indefinitely if I use this way. That might be costly.

    This is what I have switched to:

    • I set the dependency array empty
    • Pull the data from the backend and return it to the frontend after the axios calls

    addToList function:

    const addToList = async (event) => {
        event.preventDefault()
        try {
          await Axios.post(
            "https://project.herokuapp.com/insert", 
            {
              foodName: foodName,
              isVegetarian: isVegetarian,
              priceRange: priceRange,
              foodUrl: foodUrl,
            }
          )
            .then((response) => {
             // Return the data to the UI:
              setFoodList([...foodList, { _id: response.data._id, foodName: foodName, isVegetarian: isVegetarian, priceRange: priceRange, foodUrl: foodUrl }])
              setFoodName('')
              setIsVegetarian('no')
              setPriceRange('$')
              setFoodUrl('')
            })
          } catch(err) {
            console.error(`There was an error while trying to insert - ${err}`)
          }
        }
    

    updateFoodName function:

    const updateFoodName = (id) => {
            if (newFoodName) {
                Axios.put("https://project.herokuapp.com/update", {
                    id: id,
                    newFoodName: newFoodName,
                })
                .then(() => {
                    // Update on searchedFood:
                    props.searchedFood.length > 0 ?
                    props.setSearchedFood(props.searchedFood.map((val) => {
                        return (
                            val._id === id ? 
                            {
                                _id: id,
                                foodName: newFoodName,
                                isVegetarian: props.isVegetarian, priceRange: props.priceRange, 
                                foodUrl: props.foodUrl,
                            } : val
                        )
                    })) //Update on foodList
    
    : props.setFoodList(props.foodList.map((val) => {
                        return (
                            val._id === id ? 
                            {
                                _id: id,
                                foodName: newFoodName,
                                isVegetarian: props.isVegetarian, priceRange: props.priceRange, 
                                foodUrl: props.foodUrl,
                            } : val
                        )
                    }))
                })
                .catch(error => console.log(`Update name failed: ${error}`))
            }
        }
    

    deleteFood function:

    const deleteFood = (id) => {
            const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`); 
            if(confirm === true){ 
              Axios.delete(`https://project.herokuapp.com/delete/${id}`)
              .then(() => {
                props.searchedFood.length > 0 
                ? props.setSearchedFood(props.searchedFood.filter((val) => {
                    return val._id !== id
                }))
                : props.setFoodList(props.foodList.filter((val) => {
                    return val._id !== id
                }))
              })
            }
        }