skip to Main Content

So, I am wondering if this makes sense at all. I am trying to cache the data/users when the data is fetched with Axios, but make this useEffect run only when cachedUsers.length has no length. Does this have any propose or is there a better way to do this?

import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";

const GetAllUsers = () => {
  const [fsUsers, setFsUsers] = useState([]);
  const [notFoundErr, setNotFoundErr] = useState("");
  const [loading, toggleLoading] = useState(true);
  const navigate = useNavigate();

  const cachedUsers = useMemo(() => fsUsers, [fsUsers]);

  // Gets all users in Firestore DB.
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        toggleLoading(true);
        const res = await axios({
          method: "GET",
          url: "http://localhost:4000/auth/api/firebase/users",
          validateStatus: (status) => {
            return status === 200 || status === 404;
          },
        });
        console.log(res.data);
        if (res && res.status === 200) {
          setFsUsers(res.data);
        } else if (res && res.status === 404) {
          setNotFoundErr("No users found.");
        }
      } catch (error) {
        console.error(error);
        navigate("/error500");
      } finally {
        toggleLoading(false);
      }
    };
    fetchUsers();
  }, [!cachedUsers.length]);

  return [cachedUsers, notFoundErr, loading];
};

export default GetAllUsers;

3

Answers


  1. Chosen as BEST ANSWER

    This is what I was trying to aim for. Cache the user (for profile page) when the user visits that page for the first time. Then when the user navigates to that page again, it doesn't have to send that request to the server and not waste time and resources.

    // CacheContext.js
    import { createContext, useState, useEffect } from "react";
    
    export const CacheContext = createContext();
    
    export const CacheProvider = ({ children }) => {
      const [cache, setCache] = useState({ userProfile: null });
    
      return (
        <CacheContext.Provider value={{ cache, setCache }}>
          {children}
        </CacheContext.Provider>
      );
    };
    
    // useCache.js
    import { useContext } from "react";
    import { CacheContext } from "../contexts/cache";
    
    const useCache = () => {
      return useContext(CacheContext);
    };
    
    export default useCache;
    
    // GetUser.js
    import { useState, useEffect } from "react";
    import { useNavigate } from "react-router-dom";
    import axios from "axios";
    
    import useCache from "../../../hooks/useCache";
    
    const GetUser = (id) => {
      const [notFoundErr, setNotFoundErr] = useState("");
      const { cache, setCache } = useCache();
      const navigate = useNavigate();
    
      // Gets stored user.
      useEffect(() => {
        let abortController;
    
        const fetchDetails = async () => {
          try {
            if (cache.userProfile === null) {
              abortController = new AbortController();
              setNotFoundErr("");
              const res = await axios({
                method: "GET",
                url: `http://localhost:4000/auth/api/firebase/users/${id}`,
                signal: abortController.signal,
                validateStatus: (status) => {
                  return status === 200 || status === 404;
                },
              });
              // console.log(res.data);
    
              if (res && res.status === 200) {
                setCache((prev) => ({ ...prev, userProfile: res.data }));
              } else if (res && res.status === 404) {
                setNotFoundErr(res.data.user);
              }
            }
          } catch (error) {
            if (error.code === "ECONNABORTED" || error.message === "canceled") {
              console.warn("Request was aborted.");
            } else {
              console.error(error);
              navigate("/error500");
            }
          }
        };
        fetchDetails();
    
        return () => {
          if (abortController) abortController.abort();
        };
      }, []);
    
      return [cache.userProfile, notFoundErr];
    };
    
    export default GetUser;
    
    

  2. The useMemo in your code has no effect since you’re just returning the value in state (you can use that directly). useMemo will re-evaluate every time fsUsers updates so unless you were transforming/normalizing that data there’s no point to memoize.

    In terms of the useEffect if I understand correctly, you want to make this request once, so you can just use an empty dependency array to make the api call whenever the hook mounts:

    import { useState, useEffect, useMemo } from "react";
    import { useNavigate } from "react-router-dom";
    import axios from "axios";
    
    const GetAllUsers = () => {
      const [fsUsers, setFsUsers] = useState([]);
      const [notFoundErr, setNotFoundErr] = useState("");
      const [loading, toggleLoading] = useState(true);
      const navigate = useNavigate();
    
      // Gets all users in Firestore DB.
      useEffect(() => {
        const fetchUsers = async () => {
          try {
            toggleLoading(true);
            const res = await axios({
              method: "GET",
              url: "http://localhost:4000/auth/api/firebase/users",
              validateStatus: (status) => {
                return status === 200 || status === 404;
              },
            });
            console.log(res.data);
            if (res && res.status === 200) {
              setFsUsers(res.data);
            } else if (res && res.status === 404) {
              setNotFoundErr("No users found.");
            }
          } catch (error) {
            console.error(error);
            navigate("/error500");
          } finally {
            toggleLoading(false);
          }
        };
        fetchUsers();
      }, []);
    
      return [cachedUsers: fsUsers, notFoundErr, loading];
    };
    
    export default GetAllUsers;
    
    Login or Signup to reply.
  3. !cachedUsers.length is true if and only if cachedUsers.length == 0.

    Including !cachedUsers.length in the useEffect dependency array will trigger the hook to run whenever !cachedUsers.length changes,
    this only happens when !cachedUsers.length goes from true to false or the other way around.
    In other words when cachedUsers.length goes from 0 to n or the other way around..

    So this useEffect runs :

    1. on mount (this will happen anyway) so if that’s enough and you want your useEffect to fire only once, after the component is mounted, then give it an empty dependency array [] as second parameter.

    2. when cachedUsers become an empty array (but not if it was already an empty array).

    3. also when the cachedUsers is no longer an empty array: for example after the first mount, useEffect runs and updates fsUsers which will trigger useMemo in the next render and this latter will trigger useEffect because cachedUsers is no longer empty array).

      to avoid this, you can add a condition inside the useEffect callback function:

      if (cachedUsers.length === 0) {
      //...
      }
      

      this won’t prevent useEffect from running but at least the code inside it won’t be executed if cachedUsers is not empty, in other words when cachedUsers.length goes from 0 to n since you don’t care about this case

    Here is a codesandbox without strict mode, have a look, and try comment/uncomment the commented code

    keep in mind that:

    setFsUsers(res.data); // res.data may be an empty array
    

    As it is mentioned in @DannyMoshe answer the useMemo hook in your example is useless, its value changes each time fsUsers is updated so it is nothing but a copy of it.
    get rid of it and use fsUsers instead

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search