Home > OS >  React Hooks - useDidUpdateEffect with deep compare
React Hooks - useDidUpdateEffect with deep compare

Time:09-17

I have the following useDidUpdateEffect hook

import { useRef, useEffect } from "react";

export default function useDidUpdateEffect(effect, deps) {
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      effect();
    } else {
      didMount.current = true;
    }
  }, deps);
}

And I am currently using it as follows:

useDidUpdateEffect(() => { ... }, [JSON.stringify(complexNestedObject)]);

The problem is that, as my dep is complex and deep, sometimes the callback is being executed without being necessary.

I have one solution for useEffect, which is useDeepEffect:

import { useRef, useEffect } from "react";
import { isEqual } from "lodash";

export default function useDeepEffect(effect, deps = undefined) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    const isSame = prevDeps.current.every((obj, index) =>
      isEqual(obj, deps[index])
    );

    if (isFirst.current || !isSame) {
      effect();
    }

    isFirst.current = false;
    prevDeps.current = deps;
  }, deps);
}

But I can't think of anything to do a deep compare with useDidUpdateEffect, without having to create another hook.

Any ideas?

====================

UPDATE:

I am in this current situation because of this scenario. Maybe this could be done simpler, but to be honest, I tried my best (I am not super professional)

  1. I have a folder "api" with different modules which contains different functions to connect to my server (I am not using hooks for this part)
   services/
       firebase/
           api/
              users/
                 helpers/
                 cache/
                    usersCache.js
                 index.js
                 ...

As you can see, I have a module "usersCache.js" (memoization pattern) which will avoid some RTTs and reduce costs. To avoid problems with RAM, I have implemented there a caching algorithm "LRU".

  1. In my app I am not using REDUX (maybe the worst idea I have taken, but too late to adapt it, 90% of the work is done. Will try to learn this tech and adopt it in production, with a long refactoring).

To manage complex states, I am using React Context useReducer, something that somehow simplifies my life, as it is a little similar to Redux.

In my case, for users I have 2 contexts:

  1. CurrentUserContext
  2. OtherUsersContext

Both of them contains sensitive and non-sensitive data of the fetched users. Here you may think: why to have the usersCache? If both contexts can be used as an in-memory cache?

The answer (at least what I think) is:

  1. There is no way to consume contexts outside React components or hooks. Its not possible to do it inside my api modules in order to return cached data and avoid making server requests.

  2. I am saving sensitive data in the contexts (isFollowed, for example) which depend on the current user session. So, when the user logs out, the contexts are unmounted (protected routes). In the other hand, my usersCache module is still there, with no sensitive data.

Here is an example of my CurrentUserContext (I am not using a reducer here because of simplicity, but in my OtherUsersContext, as state management is complex, I do):

import React, { createContext } from "react";

import useDidUpdateEffect from "../../hooks/useDidUpdateEffect";
import useStateWithCallback from "../../hooks/useStateWithCallback";
import { usersCache } from "../../services/firebase/api/users";

const CurrentUserContext = createContext(null);

export default CurrentUserContext;

export function CurrentUserProvider({ children }) {
  const [data, setData] = useStateWithCallback(undefined);

  const updateData = (data, merge = true, callback = undefined) => {
    if (merge) {
      setData((prevData) => ({ ...prevData, ...data }), callback);
    } else {
      setData(data, callback);
    }
  };

  useDidUpdateEffect(() => {
    console.log(JSON.stringify(data, null, 2));
    /*
      As the current user data is not sensitive, we can
      synchronize the users cache here.
    */
    usersCache.updateCachedUser(data.id, data);
  }, [JSON.stringify(data)]); // TODO - Avoid unnecessary executions -> deepCompare ?

  return (
    <CurrentUserContext.Provider
      value={{
        data,
        setData,
        updateData,
      }}
    >
      {children}
    </CurrentUserContext.Provider>
  );
}

To avoid running multiple useDidUpdateEffects, I am stringifying the user data. But, as it is complex and nested:

 userData = {
     avatar: {
         uri,
         thumbnailUri, 
     },
     ...
 }

the effect is executed when the data hasn't change, because of receiving the same data disordered:

userData = {
     avatar: {
         thumbnailUri,
         uri, 
     },
     ...
 }

The top level fields are nor disordered.

CodePudding user response:

I think (and I could be wrong) this whole problem goes away if you use this effect:

useEffect(() => {
  if(!data) return;

  // do some stuff 
},[data]);

CodePudding user response:

The problem I was having was that my server was retuning JSON data with disordered fields... So, you can try both of the current solutions.

@Adam's answer is great, and avoid playing with custom hooks. In its solution, he just generate a hash with the JSON object, and compare with the previous one. If equal hashes, then do not update state, avoiding running the useEffect.

But, this might make you write some conditions and generate hashes in different parts of your code... So, if you don't care about deep comparing your object, you can try this hook:

import { useRef, useEffect } from "react";
import { isEqual } from "lodash";

export default function useDeepEffect(
  effect,
  deps = undefined,
  options = { ignoreFirstExecution: false }
) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    if (!isFirst.current || !options.ignoreFirstExecution) {
      const isSame = prevDeps.current.every((obj, index) =>
        isEqual(obj, deps[index])
      );

      if (isFirst.current || !isSame) {
        effect();
      }
    }

    isFirst.current = false;
    prevDeps.current = deps;
  }, deps);
}

with the option ignoreFirstExecution set to true, you get the same behavior as using the typical useDidUpdateEffect.

  useDeepEffect(
    () => {
      usersCache.updateCachedUser(data.id, data);
    },
    [data],
    { ignoreFirstExecution: true }   
  );
  • Related