Search code examples
reactjsreact-routerfetch-apireact-custom-hooks

navigate function does not rerender the component


first of all my project link on github: https://github.com/ARN1380/BookingHotel

I'm using react-router dom v6, I have a BookmarkList in my project and I need to add or delete bookmarks from it, the problem is when I add a bookmark from another component I need to navigate to the BookmarkList component. This navigation was supposed to rerender the BookmarkList but it does not so my custom Hook doesn't run and new list doesn't get fetch from server. (if i refresh the page it does fetch and everything is ok) edit: it does rerender here is my BookmarkList component:

import ReactCountryFlag from "react-country-flag";
import { useBookmarks } from "../context/BookmarksProvider";
import { Link } from "react-router-dom";
import { closePopup } from "../map/Map";
import { HiTrash } from "react-icons/hi";

export default function BookmarkList() {
  const {bookmarks, deleteBookmark} = useBookmarks();

  function handleBookmarkDelete(e, id) {
    e.preventDefault();
    deleteBookmark(id);    
  }
  
  return (
    <div>
      <h2>Bookmark List</h2>
      <div className="bookmarkList">
        {bookmarks.map((bookmark) => {
          return (
            <Link
              key={bookmark.id}
              to={`${bookmark.id}?lat=${bookmark.latitude}&lng=${bookmark.longitude}`}
              onClick={closePopup}
            >
              <div className="bookmarkItem">
                <div>
                  <ReactCountryFlag svg countryCode={bookmark.countryCode} />
                  &nbsp; <strong>{bookmark.cityName}</strong> &nbsp;{" "}
                  <span>{bookmark.country}</span>
                </div>
                <button
                  type="button"
                  onClick={(e) => {
                    handleBookmarkDelete(e, bookmark.id);
                  }}
                >
                  <HiTrash className="trash" />
                </button>
              </div>
            </Link>
          );
        })}
      </div>
    </div>
  );
}

and this is AddBookmark component:

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAnglesLeft } from "@fortawesome/free-solid-svg-icons";
import { useNavigate, useSearchParams } from "react-router-dom";
import useFetch from "../../hooks/useFetch";
import { useBookmarks } from "../context/BookmarksProvider";
import { useEffect, useId, useState } from "react";
import {v4 as uuidv4} from 'uuid';


export default function AddBookmarks() {
  const navigate = useNavigate();
  const [searchParams, setSearchParams] = useSearchParams();
  const [cityNameState, setCityNameState] = useState("");
  const [countryNameState, setCountryNameState] = useState("");
  const {addBookmark} = useBookmarks();
  const lat = searchParams.get("lat");
  const lng = searchParams.get("lng");

  const {isLoading, data} = useFetch(
    "https://api.bigdatacloud.net/data/reverse-geocode-client",
    `latitude=${lat}&longitude=${lng}`
  );

  useEffect(() => {
    if (data) {
      setCityNameState(locationData.city);
      setCountryNameState(locationData.countryName);
    }
  }, [data]);

  if (isLoading) return <div>Loading ...</div>;

  const locationData = data;

  function handleSubmit(e) {
    e.preventDefault();
    
    const newBookmark = {
      cityName: cityNameState || locationData.locality,
      countryName: countryNameState,
      countryCode: locationData.countryCode,
      latitude: `${locationData.latitude}`,
      longitude: `${locationData.longitude}`,
      host_location: locationData.city + " " + locationData.countryName,
      id: uuidv4(),
    };

    addBookmark(newBookmark);
    navigate("/bookmark");
  }

  return (
    <div className="capitalize">
      <h2 className="font-bold">add bookmark</h2>
      <form className="mt-2 [&>div>label]:text-sm" onSubmit={handleSubmit}>
        <div className="flex flex-col">
          <label className="">city:</label>
          <input
            className="border-b border-0 border-solid mt-2"
            type="text"
            required
            value={cityNameState}
            onChange={(e) => setCityName(e.target.value)}
          />
        </div>
        <div className="flex flex-col mt-4">
          <label>country:</label>
          <input
            className="border-b border-0 border-solid mt-2"
            type="text"
            required
            value={countryNameState}
            onChange={(e) => setCountryName(e.target.value)}
          />
        </div>
        <div className="mt-5 flex justify-between">
          <button
            onClick={() => {
              navigate(-1);
            }}
            type="button"
            className="border   border-slate-400 border-solid rounded-md flex space-x-1 items-center p-0.5 px-1"
          >
            <FontAwesomeIcon icon={faAnglesLeft} size="xs" />
            <p>Back</p>
          </button>
          <button className="bg-purple-600 text-white px-3 py-0.5 rounded-md">
            save
          </button>
        </div>
      </form>
    </div>
  );
}

and this is my useFetch custom hook in case:

import axios, { AxiosError } from "axios";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";

export default function useFetch(url, query = "") {
  const [data, setData] = useState();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
    axios
      .get(`${url}?${query}`)
      .then((response) => {
        setData(response.data);
      })
      .catch((err) => {
        toast.error(err.message);
      })
      .finally(() => setIsLoading(false));
  }, [url, query]);

  return {isLoading, data};
}

here is my routing in App.jsx:

        <Routes>
          <Route path="/" element={<HotelsList />} />
          <Route path="/search" element={<SearchLayout />}>
            <Route index element={<Hotels />} />
            <Route path="Hotels/:id" element={<SingleHotel />} />
          </Route>
          <Route path="/bookmark" element={<BookmarkLayout />}>
            <Route index element={<BookmarkList />} />
            <Route path=":id" element={<SingleBookmark />} />
            <Route path="add" element={<AddBookmarks /> } />
          </Route>
          <Route path="/404" element={<ErrorPage />} />
          <Route path="*" element={<Navigate to="/404" />} />
        </Routes>

what is my problem? i can't see why BookmarkList doesn't rerender when navigate happens ...

i tried using an useEffect in BookmarkList component but it didn't work

adding path="/bookmark/" to

<Route path="/bookmark/" index element={} />


Solution

  • In your bookmarksProvider you have this code:

    let [isLoading, data] = useFetch(BOOKMARK_URL);
    
    if (isLoading) return <div > Loading... < /div>;
    
    function addBookmark(newBookmark) {
      axios
        .post(BOOKMARK_URL, newBookmark)
        .then((response) => {})
        .catch((error) => {
          toast.error("couldn't save your bookmark!");
          console.log(error.response.data);
        });
    }

    In your addBookmark function, you only POST to the url. I'm assuming you then want useFetch to automatically refetch bookmarks. That won't happen because of the useEffect you use in useFetch. The useFetch will only rerun when url or query changes. Since those are hardcoded, useEffect will be run once and never again (unless you refresh your window of course).

    You will need to create a way to either add the new bookmark to your data array, or add a new parameter to useFetch, that you can update with a new unique value so that useEffect within useFetch will run again.

    Another option is to add a function to your context (getBookmarks for instance) that you can call with useEffect inside of your list page so that everytime you visit the list page, the bookmarks are fetched.