In this final part of the two-part in-depth series on React Hooks, we continue to discuss a few more hooks and custom hooks.
In the first article in the series, which was carried in the October 2022 issue of OSFY, we learnt what hooks and custom hooks are, and discussed a few hooks briefly. We will look at a few more hooks and custom hooks in this second and final article on React Hooks.
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps]); function Button (props, ref) { const buttonRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { console.log (“Inside focus”); buttonRef.current.focus(); } })); return ( <button ref={buttonRef} {...props}> Button </button> ); } export default forwardRef (Button);
Usually, when you use useRef, you are given the instance value of the component the ref is attached to. This allows you to interact with the DOM element directly.
useImperativeHandle is very similar but it lets you do two things — it gives you control over the value that is returned; so instead of returning the instance element, you explicitly state what the return value will be. Also, it allows you to replace native functions such as blur and focus with functions of your own, thus allowing side effects to the normal behaviour or different behaviour altogether. And the best point is you can call the function whatever you like. So, basically, useImperativeHandle customises the instance value that is exposed to parent components when using ref.
In the example above, the value we get from ref will only contain the function focus, which we declared in useImperativeHandle. Also, you can customise the function to behave in a different way than you would normally expect. So, for example, in the focus function, we can update the document title or log as seen in the code but notice that we have used forwardRef. In React, forwardRef is a method that allows parent components to pass down ref to their children, and it gives a child component a reference to a DOM element, which is created by its parent component.
So, basically, useImperativeHandle is used when you might not want to expose native properties to the parent or (as in our case) want to change the behaviour of a native function. As in the case of additional hooks, you’ll rarely use useImperativeHandle in your code base, but it’s good to know how to use it.
useLayoutEffect const ref = React.useRef() React.useEffect (() => { ref.current = ‘Lorem ipsum’ }) React.useLayoutEffect (() => { console.log(ref.current) // This runs first, will log old value })
As we saw with the useEffect, the code inside of useLayoutEffect is fired after the layout is rendered. Although this makes it suitable for, let’s say, setting up subscriptions and event handlers, not all effects can be deferred. So, basically, we should understand the difference between passive and active event listeners. Let’s say we want to make changes to the DOM synchronously before we render the additional components. If we use useEffect, this can lead to inconsistencies in the UI. As a result, for these types of events, React provides us with useLayoutEffect. It has the same signature as useEffect but the only difference is when it is fired. Your code runs immediately after the DOM has been updated but before the browser has had a chance to paint those changes.
For example, if you want to make DOM measurements like getting the scroll position or other styles of an element, and then make mutations or trigger synchronous re-render, useLayoutEffect is our guy. If you don’t need to do all this, you should use useEffect. I’ll go as far as to say that 99 per cent of the time, you would be using useEffect.
Custom hooks
You may have heard a lot about custom hooks. Let us try and understand what they are exactly. Custom hooks are normal JavaScript functions that can use other hooks inside them, and contain a common stateful logic that can be reused within multiple components. These functions are prefixed with the word ‘use’. You can name them anything as long as they have the ‘use’ prefix. Let’s say you have two functions or components which implement some common logic. You can create a third function with this common logic, and implement it in the other two functions. After all, hooks are just functions. They give us the advantage of fewer keystrokes and less repetitive code. I will show how this happens and we’ll create our very own hook now.
Let’s say we have a count state variable and we want to store it in the local storage whenever the value updates. useEffect is the perfect candidate here.
function App() { const [count, setCount] = useState(() => { return JSON.parse( window.localStorage.getItem(‘counter-value’) || ‘0’ ); }); useEffect (() => { window.localStorage.setItem(‘counter-value’, count); }, [count]); return ( <div> <button onClick={() => { setCount(count + 1) }}>{count}</button> </div> ); }
But what if we want to update this value in ten other components? The highlighted code will be repeated in all these 10 components. To avoid this repetition, we will use a custom hook.
function useLocalStorage(key, default) { const [state, setState] = useState(() => { return JSON.parse( window.localStorage.getItem(key) || String(default) ); }); useEffect (() => { window.localStorage.setItem(key, state); }, [state]); } function App() { const [count, setCount] = useLocalStorage(‘counter-value’, 0); return ( <div> <button onClick={() => { setCount(count + 1) }}>{count}</button> </div> ); }
As you see in the above code, we have created our very own custom hook called useLocalStorage and added the repeated code to this hook. Now, in all our 10 components we can simply call the useLocalStorage hook, and pass in the key and the default value. It’s as simple as that and will work in the exact same way as before.
With the introduction of custom hooks, people started creating hooks for the most common use cases. A simple search for a local storage hook in npm gave 60 already existing packages for various use cases. So whenever you find the use case for a custom hook in your React application, I recommend going through this list; maybe someone has already created a package that you can reuse and save development time.
This is one of the strengths of having such a vast and active community and React has that — you can surely use it to your advantage.
useDebugValue function useLocalStorage (key, default) { const [count, setCount] = useState(0); // ... // Show a label in DevTools next to this Hook // e.g. “LocalStorage: Empty” useDebugValue (count == 0 ? ‘Empty’: ‘Not Empty’); useDebugValue(count == 0 ? ‘Empty’ : ‘Not Empty’); return count; }
As you may know, React dev tools are an amazing way to debug apps. React shows a component tree with props values while running the app. useDebugValue is used to display labels for custom hooks in the React dev tools. For example, if you look at a custom hook in the code above, we can display the state value of our custom book by using useDebugValue, which will show us the label with the key as the hook name (it will just remove the ‘use’ word; so it says local storage here instead of ‘useLocalStorage’) and the value will be dependent on the state value.
We can pass an optional function to this hook solely for formatting purposes, i.e., for data to look good when we inspect the components. Keep in mind that this is only for debugging and it won’t be seen on the UI. This is just made to make the lives of developers easy when they inspect elements. useDebugValue takes in the debug value and returns the formatted display just for the developers to see the data. So the data looks pretty and is easier to understand.
Some rules
Now that you know all about hooks, you should know two additional rules that are imposed by them. First, we have to call hooks only at the top level. That is, don’t call hooks inside loops, conditions, or nested functions. Second, call hooks only from React function components, and not from regular JavaScript functions. There is just one other valid place to call hooks, and that is your own custom hook as we saw in the custom hook section. Basically, follow these two rules and everything will work as expected for you.