React Hooks were first introduced in React 16.8 at React Conf 2018. Since then, they have become very popular for several reasons. Firstly, with Hooks, you can extract stateful logic from a component so it can be independently tested and reused. Hooks allow you to reuse stateful logic without changing your component hierarchy, making it easy to share. Secondly, you can use Hooks to split components into smaller functions (e.g. setting up a subscription, fetching data, etc.). Lastly, Hooks let you use more of React’s features without classes.
In this article, we’ll cover the advantages of using Hooks in your React project. We’ll take a deeper dive into the composition of Hooks. Yes! You can create your own custom Hooks to implement logic where you compose hooks together. Let’s take a look at how we can do this.
Note: We won’t be covering the basics in this article. If you’re not familiar with React Hooks, take a look at the official docs here.
Dispatch Redux actions
As an example, we’ll look at an application using Redux. But the core idea can be applied to any library exposing Hooks. You can compose your own custom Hooks as well.
In our project, Redux is used as global storage. The React-Redux library exposes Hooks to us that we can use to manage our store. One of them is a useDispatch Hook that helps us dispatch actions.
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}
As the official React-Redux docs suggest: “ When passing a callback using dispatch to a child component, you may sometimes want to memoize it with useCallback” to avoid unnecessary rendering of the component where the function is called. The dispatch function reference will be stable as long as the same store instance is passed to the <Provider>. There is no guarantee that the store will be stable, so we need to add it as a dependency for useCallback.
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch]
)
return (
<div>
<span>{value}</span>
<button onClick={incrementCounter}>
Increment counter
</button>
</div>
)
}
Create a custom Hook
Cool! Now let’s think about our application. It’ll probably have many components where we would like to retrieve data stored in the Redux global store. What we can do to simplify this is to create a custom Hook and move logic there. We can then reuse the custom Hook within our application.
// use-action.js
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
export const useAction = (action) => {
const dispatch = useDispatch();
return useCallback(() => dispatch(action), [dispatch, action]);
};
Please note that we added an action argument to our array of dependencies because we need to be sure that the reference we are getting from the useAction Hook is updated only if the action param changes.
From React docs:
"Unlike a React component, a custom Hook doesn’t need a specific signature. We can decide what it takes as arguments and what, if anything, it should return. In other words, it’s just like a normal function. Its name should always start with ‘use’ so that you can tell at a glance that the rules of Hooks apply to it."
Now our component will look like this:
const incrementAction = { type: 'increment-counter' };
export const CounterComponent = ({ value }) => {
const incrementCounter = useAction(incrementAction)
return (
<div>
<span>{value}</span>
<button onClick={incrementCounter}>
Increment counter
</button>
</div>
)
}
Enhance implementation
We extracted incrementAction outside the component because otherwise, on each re-render, we would pass a new object as an argument and therefore create a new function.
That already looks better! But let’s enhance our useAction Hook even more. Oftentimes, you need to pass some payload with your action. To achieve that, we need to pass an additional parameter to the useAction Hook.
Note that we are using useMemo instead of useCallback here because we can memoize/remember the function's return value. That way, we can avoid re-running potentially heavy calculations when the dependencies stay the same between renders.
// use-action.js
import { useMemo } from 'react';
import { useDispatch } from 'react-redux';
export const useAction = (action, value) => {
const dispatch = useDispatch();
return useMemo(() => dispatch(action(value)), [dispatch, action, value]);
};
Memoize the result
And our realistic structure will be:
// actions.js
export const incrementAction = ( data ) => ({
type: 'increment-counter',
payload: data
})
// counter-component.js
import { incrementAction } from 'actions'
import { useAction } from 'use-action'
export const CounterComponent = React.memo(({ value, incrementor }) => {
const incrementCounter = useAction(incrementAction, incrementor)
return (
<div>
<span>{value}</span>
<button onClick={incrementCounter}>
Increment counter
</button>
</div>
))
We used memo to memoize our CounterComponent so it will only rerender if our properties change. Also, we add a useMemo Hook to memoize the result of incrementCounter.
Our custom Hook is flexible and can be used within different components across our application. That is how easily we can achieve a decent level of reusability of our dispatcher using the React Hooks API.
Read also: Simplifying React Forms with Hooks