simbathesailor.DEV

Useful Patterns with React hooks

patterns
react-hook
usecallback
useref

January 04, 2020

Photo by Sebastian Bednarek on Unsplash

Well, here I am explaining the pattern in React hooks. Who am I ? No body !! Don’t be mad because this is opinionated. Things which worked for me and the people around me.
That’s it.

First let’s take a step back, and understand what changed with react hooks. Well, A lot has changed but we will talk about those things which are going to be relevant for the the discussion ahead.

From now on I will be calling React hooks as just hooks. I am tired of adding React in front of every hook word :).

So the hooks come with a beautiful way of sharing stateful logic between components and also make the reusability of logic much easier. The hooks allows to segregate the logical concerns properly. Remember how we were adding a functionality spread across various lifecycle methods with classes approach.

componentDidMount() {
// add event listener
// subscribe for event listener
}
componentWillUnMount(){
// remove some event listener from window
// or unsubscribe something on unmount
}
componentDidUpdate(prevProps, prevState) {
// do some comparison and then
// resubscriibe and unsubscribe
}

It is all good, but problem starts when we think how do we share the logic to other components. we then use to take on HOC patterns and render props pattern, which are very good patterns for sharing the logic. But with all goodness, they also bring lot more not needed verbosity and false hierarchy of our components. It also bring in friction to extract reusable logics.

We will not discuss more on class based components. Because I think you are here to see the patterns with hooks and some food for thought.

Hooks are very good, but there are certain thing which can make situations difficult for even a seasoned developer. One of the problem which I used to encounter is with the custom hooks. A simple custom hooks is quite straight forward to work on. But its start’s getting confusing when it takes multiple arguments.

Even multiple arguments are good, but when the argument are objects, array or functions , It becomes very difficult to wrap your head around the hook.
Yes, React has provided certain hooks which allows me to mix and match them and solve the issue. I felt myself putting in a lot of focus and head while writing a custom hook which now has started accepting functions. Damn why I have to be so cautious. Why I can’t just pass functions as make it work. Why the infinite loops are getting triggered, why I have to every time think , Should I use useRef or useCallback ? At the time , I would be like

Image from giphy

Yes, but some would say ohh, so you are experienced and you are not able to figure out closures and references. Oh yes my friends, I find myself good with those concepts , but does not like the fact of thinking it every time ,when i am passing references to the hooks. For me custom hooks are just a utility that does processing and return back something which I use in my visual components.

So some might say, I have not felt that way. I am totally ok with how to write hooks. I would say cool, you have got it. May god teach all of us that finesse.

But before we see some code, let’s understand why objects, arrays and especially objects are little complicated with hooks.

My opinion is different for (object, arrays) and different for functions. I would say nothing has changed for objects and arrays as such. If we pass a new reference of arrays or object in class based component, the componentDidUpdate will get triggered. With hooks similarly, a hook with dependencies as arrays or object will rerun and do it’s thing.

But when it comes to functions we are so used to write them as component instance, that function does not change very often with class based component

class App extends React.Component() {
callback = () => {
// some logic
};
callback2 = () => {
// some logic
};
render() {
return (
<div>
<SomeChildComponent callback={callback} callback2={callback2} />
</div>
);
}
}

Notice above how the Child component (SomeChildComponent) is taking two props:

callback : available on instance, does not change on re-render
callback2: same as above

But with hooks, there are no lifecycles now, mental model has changed to an extent.we still write the functions we need to pass on to other hooks or component inside the function. I think that comes natural. It means every time the component re-render the new reference is being passed to the respective hook or components.Btw It has benefits as we are having access to value outside due to closure. Could have been a dragged effort to pull the functions out of the components every time.

function App() {
// Notice the callback passed to component will be a new reference
// on every rerender for both the callbacks
function callback() {
// some logic
}
function callback2() {
// some logic
}
return <SomeChildComponent callback={callback} callback2={callback2} />;
}

Now for simple cases , it’s all ok, but as the logic progresses inside component and the components around it, it demands a lot of focus.

To the readers who are still with me, let’s see some code and understand various ways of handlings references with hooks. We will go through various cases and by the end we will have correct mental model and patterns to work with hooks. Take all these cases below as some of the ways of working with hooks , not all.

We will be taking small examples, base code will not change much across cases except certain parts of them. Every example will have base problem and the ways to fix it.

CASE 1:

Input

function useFunctionHook(fn) {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    // some logic
    // if fn changes the hooks callback will run
  }, [count, setCount, fn]);
  return [setCount, fn];
}
function App() {
  const [countInParent, setCountInParent] = React.useState(0);
function fn() {
    // some logic
  }
  const [setCount] = useFunctionHook(fn);
return (
    <div>
      some child component inside.
    </div>
  );
}
view raw blog16.md hosted with ❤ by GitHub

Problem : In above code snippet , we are passing fn function as the argument to useFunctionHook. The fn reference changes on every render of component App. It can cause the effect to re-run again even when it is not desired. Let’s give our first try to fix this issue.

CASE 1 TRY 1:

function useFunctionHook(fn) {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    // some logic
    // if fn changes the hooks callback will run
  }, [count, setCount, fn]);
  return [setCount, fn];
}
// Moved the fn out , now fn will have the same reference across re-render
function fn() {
    // some logic
}

function App() {
  const [countInParent, setCountInParent] = React.useState(0);

  const [setCount] = useFunctionHook(fn);
return (
    <div>
      some child component inside.
    </div>
  );
}
view raw blog1.md hosted with ❤ by GitHub

Above we moved out the fn definition and now, it will have same reference across re-render. But it’s not always possible to do. We most of the times need access to the local variables available in Components and custom hooks. Following the approach above , will make it difficult to do so.

CASE 1 TRY 2:

React also gives us a special hook named as useCallback, which allows us to keep the reference intact based on dependency list passed as the second argument. Here is the API for useCallback

const fn = useCallback(function A() {}, [a, b, c])

The first argument function is synchronised with the elements in dependency lists. The fn will only point to a new reference when a, b or c changes in this example. Let’s make use of it to solve our problem.

function useFunctionHook(fn) {
  // now the fnCallback will only not change across rerender
  const fnCallback = React.useCallback(fn, [])
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    // some logic
    // if fnCallback changes the hooks callback will run
  }, [count, setCount, fnCallback]);
  return [setCount, fnCallback];
}

function App() {
  const [countInParent, setCountInParent] = React.useState(0);

function fn() {
    // some logic
  }
  const [setCount] = useFunctionHook(fn);
return (
    <div>
      some child component inside.
    </div>
  );
}

In the example above, now the fn is again part of the component, but now fn has the access to all the variables inside component due to closures. And also the useFunctionHook makes use of useCallback to persist the reference of fn. In the example the reference of fn will not change across re-render.

Now lets say the fn access some value from the component something like this:

const [count, setCount] = React.useState(0);

function fn() {
  //  access count which is available in fn scope
  setCount(count + 1)
}
view raw blog3.md hosted with ❤ by GitHub

But the the fn reference will not change across re-render as the dependency is blank array in our case.

const fnCallback = React.useCallback(fn, [])

So in our case , The count referenced by function fn will refer to same initial value across re-render.

The closures created once will persist across re-renders unless the
callback runs again.

Fix is easy, now add count as the dependency for useCallback. For that we must pass countInparent also now to useFunctionHook.

function useFunctionHook(fn, countInParent) {
  // now the fnCallback will only not change across rerender
  const fnCallback = React.useCallback(fn, [countInParent])
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    // some logic
    // if fnCallback changes the hooks callback will run
  }, [count, setCount, fnCallback]);
  return [setCount, fnCallback];
}

function App() {
  const [countInParent, setCountInParent] = React.useState(0);

function fn() {
    // some logic
  }
  const [setCount] = useFunctionHook(fn, countInParent);
return (
    <div>
      some child component inside.
    </div>
  );
}
view raw blog4.md hosted with ❤ by GitHub

One more alternate adjustment we could have done here is instead of having memoized the callback in useFunctionHook, we could have done it in App component.

But I think that’s not a good way to go ahead. It’s better if consumer of a custom hook has to do less things to make it work. In our case , we pulled the burden for maintaining the reference to function fn from App component. I think that’s good developer experience.

On the same lines, some may say useRef is also a viable option here. I would say the typical answer which is it depends. So let’s try the solutions with useRef.

CASE 1 TRY 3 :

// Solution 1
function useFunctionHook(fn, countInParent) {
  const [count, setCount] = React.useState(0);
  const fnRef = React.useRef(null)
  React.useEffect(() => {
    fnRef.current = fn
  })
  React.useEffect(
    () => {
     
      fnRef.current();
    },
    [count, setCount, fnRef, countInParent] 
 // here we have made fnRef kind of silent. 
 // It will never change after first run of the hook. as we are not changing
 // the cover
       
  );
  return [setCount, fnRef.current];
}

function App() {
  const [countInParent, setCountInParent] = React.useState(0);
  function fn() {
    console.log("Hi bro");
  }
  

  const [setCount] = useFunctionHook(fn, countInParent);

  return (
    <div>
      <button
        style={{ marginTop: "50px" }}
        onClick={() => setCountInParent(countInParent + 1)}
      >
        <h1>{countInParent}</h1>
        Change Count
      </button>
    </div>
  );
}
view raw blog5.md hosted with ❤ by GitHub

This approach also work for various scenarios. But there are few things to keep in mind before choosing useRef.

  1. Like useCallback there is no separate dependencies like array, so what could have been achieved with single line may require multiple lines with useRef. we can also write a custom hook like this, which removes some of verbosity from the above code.

    function useRefValues(value) {
      const ref = React.useRef(value);
      React.useEffect(() => {
        ref.current = value;
      });
      return [ref];
    }
    view raw bog6.md hosted with ❤ by GitHub

Now the above example can be written as :

// Solution 1
function useFunctionHook(fn, countInParent) {
  const [count, setCount] = React.useState(0);
  const [fnRef] = useRefValues(fn)
  React.useEffect(
    () => {
     
      fnRef.current();
    },
    [count, setCount, fnRef, countInParent] 
 // here we have made fnRef kind of silent. 
 // It will never change after first run of the hook. as we are not changing
 // the cover
       
  );
  return [setCount, fnRef.current];
}

function App() {
  const [countInParent, setCountInParent] = React.useState(0);
  function fn() {
    console.log("Hi bro");
  }
  

  const [setCount] = useFunctionHook(fn, countInParent);

  return (
    <div>
      <button
        style={{ marginTop: "50px" }}
        onClick={() => setCountInParent(countInParent + 1)}
      >
        <h1>{countInParent}</h1>
        Change Count
      </button>
    </div>
  );
}
view raw blog7.md hosted with ❤ by GitHub

The ref approach becomes silent dependencies. What do I mean by silent dependencies ? . Let’s see that

useEffect(() => {
  // some logic
}, [refDependency, nonRefDependency1, nonRefDependency2])

Here above , refDependency will never change after first run. But the values refDependency might be carrying will be updated on every run .

const { current: updatedValueAlways } = refDependency
//The value updates always but not the ref(refDependency) reference

But nonRefDependency1 and nonRefDependency2 are non ref dependency which will change when their respective value changes on re-render.
At times, it becomes impossible to trigger rerun of the effect callback on value change.
So if only refDependency.current changes , it will not trigger the effect callback to run because refDependency itself has not changed.

Before, we move ahead let’s see one of the pattern that can used for avoiding the more work from consumers of custom hooks.

CASE 2

Let’s call it ref callback pattern for now(So this name is pretty common, that I have heard recently, but sounds legit). Now let’s observe the code below:

function useFunctionHook(fn, ref) {
  
  React.useEffect(
    () => {
            fn();
    },
    [fn]
  );
  React.useEffect(() => {
    // some trivial update happening to 
    if(ref.current) {
        ref.current.style.backgroundColor = "hotpink"
    }
    
  }, [ref])
}


function fnCallback() {
    console.log("Hi bro");
}

function App() {
  const ref = React.useRef(null)
  
  const [countInParent, setCountInParent] = React.useState(0);
  
  const fn = React.useCallback(fnCallback, [])

  useFunctionHook(fn, ref);

  return (
    <div ref={ref}>
      {countInParent}
      <button onClick={() => setCountInParent(c => c + 1)}>
        Increment Parent's count
      </button>
    </div>
  );
}
view raw blog9.md hosted with ❤ by GitHub

PROBLEM: In the code above , the App holds the responsibility of passing ref to useFunctionHook. But again as discussed above, we should avoid the effort from the consumer of the hook as much as possible. I think same concept applies for almost every thing in programming. So let’s try to do so.

CASE 2 TRY 1 :

// Benefits: Takes away the burden of defining the ref at the consumer end. 
function useFunctionHook(fn) {
  const [domElem, setDomElem] = React.useState(null)
  
  function setElem(elem) {
    if(elem) {
        setDomElem(elem)
        setDomElem.current = elem // This is important , because might be some other component which will be needing
        // access to the elem
       }
  }
  React.useEffect(
    () => {
      // if (!(count % 2 === 0)) {
        // console.log(`I will call function }`);
        // debugger
            fn();
        
      // }
    },
    [fn]
  );
  React.useEffect(() => {
      // we can so imperative logics with dom element
    if(domElem) {
        domElem.style.backgroundColor = "hotpink"
    }
    
  }, [domElem])
  return [setElem];
}


function fnCallback() {
    console.log("Hi bro");
}

function App() {
  // const [ref, setRef] = React.useRef(null) remove this
  
  const [countInParent, setCountInParent] = React.useState(0);
  
  const fn = React.useCallback(fnCallback, [])

  const [setElem] = useFunctionHook(fn);

  return (
    <div ref={setElem}>
      {countInParent}
      <button onClick={() => setCountInParent(c => c + 1)}>
        Increment Parent's count
      </button>
    </div>
  );
}
view raw blog10.md hosted with ❤ by GitHub

Notice above , how we are using the our callback way of setting up ref. We have also pulled out the responsibility of creating ref from the App component.
Now consumers do not need to worry about creating a ref every time to consume the hook. It would be good if we can extract the functionality of creating callback refs in a common hook. Let’s do that.

function useCallbackRef() {
  const [domElem, setDomElem] = React.useState(null);
  function setElem(elem) {
    if (elem) {
      setDomElem(elem);
      setDomElem.current = elem; // This is important , because might be some other component which will be needing
      // access to the elem
    }
  }
  return [domElem, setElem];
}
view raw blog11.md hosted with ❤ by GitHub

Now the code changes to this:

function useFunctionHook(fn) {
  const [domElem, setElem] = useCallbackRef();

  React.useEffect(() => {
    // if (!(count % 2 === 0)) {
    // console.log(`I will call function }`);
    // debugger
    fn();

    // }
  }, [fn]);
  React.useEffect(() => {
    // we can so imperative logics with dom element
    if (domElem) {
      domElem.style.backgroundColor = "hotpink";
    }
  }, [domElem]);
  return [setElem];
}

function fnCallback() {
  console.log("Hi bro");
}

function App() {
  const [countInParent, setCountInParent] = React.useState(0);

  const fn = React.useCallback(fnCallback, []);

  const [setElem] = useFunctionHook(fn);

  return (
    <div ref={setElem}>
      {countInParent}
      <button onClick={() => setCountInParent(c => c + 1)}>
        Increment Parent's count
      </button>
    </div>
  );
}
view raw blog12.md hosted with ❤ by GitHub

To summarise the ways we can handle callbacks with react hooks are:

1. If possible move out the callback outside the component. This will not be possible almost every time. So mind it.

2. If the callback is not dependent on any of the values in component scope, then go for useCallback approach or useRef approach with blank dependencies.Remember they become silent in those cases. Can be tricky as the closure created once will not update itself unless it runs again.

3. If the callback need to change when certain values changes, go for useCallback approach, passing in desired dependencies.

4. Try to minimize the things that consumer of a hook or component have to do to get things working.

Now we talked about various ways of handling functions with custom hooks. Let’s understand how we are going to handle array and objects with custom hooks.

const option = { a: 1, b: 2, c: 3 };

// Type 1
function useAcceptOptions(option) {
  React.useEffect(() => {
    // also ok, but splices the ability to debug the things , can be anything as the option keys, lets say its a function
    // will screw everything, it was the case in earlier example also but atleast we knew what is coming in option and what to
    // consider for the effect
  }, [...option]);
}

// Type 2
function useAcceptOptions(option) {
  const { a, b, c } = option;
  React.useEffect(() => {
    // all good
  }, [a, b, c]);
}
view raw blog13.md hosted with ❤ by GitHub

Notice Type 1 and Type 2 versions of useAcceptOptions hook. Doing the Type 1 will make the the debugging difficult. But the Type 2 can be more deterministic and debugging can be easy. For arrays and objects, nothing has changed. Try to avoid putting dependency like this:

// Type 2
function useAcceptOptions(option) {
  
  React.useEffect(() => {
    // all good
  }, [option]);
}
view raw blog14.md hosted with ❤ by GitHub

It’s very common to pass the options inline , so writing like above will run the callback of effect hook to run every time. Try to keep the options as small set of values. Having big objects and array argument to custom hooks becomes difficult to reason later.
Let’s see one more example having functions, object and arrays in it.

function useRefValues(value) {
  const ref = React.useRef(value);
  React.useEffect(() => {
    ref.current = value;
  });
  return [ref];
}


function useAcceptOptions(option, arr) {
  const { a, b, c, ...otherValues } = option;

  const { callback1, array1 } = otherValues;

  // usecallback takes second argument which make it more usable in most of the cases. The cases where we want
  // callbacks to change, but not every time. There is no such thing with useRef. Ofcourse we can simulate
  // that with the the help of some other hooks. But too much work

  const finalCallback = React.useCallback(callback1, [a, b]);
  const [refArr] = useRefValues(arr);
  // Here refArr is silent dependency
  React.useEffect(() => {
    console.log("some logic based on final callback");
  }, [a, b, c, finalCallback, refArr]);
}

function App() {
  // const [ref, setRef] = React.useRef(null) remove this
  const obj = {
    a: 1,
    b: 2,
    c: 3,
    callback1: aCallback
  };
  function aCallback() {
    console.log("hello a callback", obj.a, obj.b);
  }
  const arr = ["x", "y", "z"];

  useAcceptOptions(obj, arr);

  return <div>Hello World</div>;
}
view raw blog15.md hosted with ❤ by GitHub

In the example we are just using the knowledge which we have discussed in this article till now. I am not going to explain the code above. I think you should go ahead and read it.

I think mix of useCallback, useRefValues and useCallbackRef solves almost all of the issues.


Feel free to point if any issues with the code snippets above. I have also written a handy library to debug hooks. Feel free to try it and let me know. A babel plugin is also available for it.

As told above, these are my experiences while writing hooks since last year. Please let me know if there are any other good patterns that you guys have been using.

Thanks

Join the discussion