Separation of concerns with React hooks
If you've been working with React for a while, you probably came across container and presentational components , or smart and dumb components. These terms describe a pattern that divides the UI layer of React components from the logic.
Separating the UI from the business logic is nothing unique to React: separation of concerns is a design principle that has already been around in the 70s. For example, it's common practice to separate the code that accesses the database from the business logic on the backend.
So in React, we solved this issue by creating container components that contain all the logic, which would then pass down the data via props to the presentational component.
With the introduction of React hooks, there's a new approach to this: using custom hooks.
Why should we separate the logic from the component?
Before we start decoupling the logic from our React components, we should know why.
Organizing our code in a way where every function or component is responsible for only one thing has the advantage that it's much easier to change and maintain (Dave and Andrew call this "orthogonality" in their book The Pragmatic Programmer).
Applying this to React means that our component will look cleaner and more organized. We won't need to scroll past the wall of logic before editing the UI, for instance.
Organizing your code like this doesn't only make it look better and easier to navigate, it also makes it easier to change since changing the hook doesn't affect the UI and vice-versa.
Testing is more accessible as well: we can test the logic separately from the UI if we want to. The most significant advantage to me, however, is how this approach organizes my code.
How to decouple logic with React hooks
To decouple the logic from our component, we will first create a custom hook.
Let's take this component as an example. It calculates the exponential value of the base number and the exponent:
You can find the complete source code here.
The code looks like the following:
export const ExponentCalculator = () => {
const [base, setBase] = useState(4);
const [exponent, setExponent] = useState(4);
const result = (base ** exponent).toFixed(2);
const handleBaseChange = (e) => {
e.preventDefault();
setBase(e.target.value);
};
const handleExponentChange = (e) => {
e.preventDefault();
setExponent(e.target.value);
};
return (
<div className="blue-wrapper">
<input
type="number"
className="base"
onChange={handleBaseChange}
placeholder="Base"
value={base}
/>
<input
type="number"
className="exponent"
onChange={handleExponentChange}
placeholder="Exp."
value={exponent}
/>
<h1 className="result">{result}</h1>
</div>
);
};
This may look fine already, but for the sake of this tutorial, just imagine that there's more logic in here.
As a first step, we will move the logic to a custom hook and call it inside our component.
const useExponentCalculator = () => {
const [base, setBase] = useState(4);
const [exponent, setExponent] = useState(4);
const result = (base ** exponent).toFixed(2);
const handleBaseChange = (e) => {
e.preventDefault();
setBase(e.target.value);
};
const handleExponentChange = (e) => {
e.preventDefault();
setExponent(e.target.value);
};
return {
base,
exponent,
result,
handleBaseChange,
handleExponentChange,
};
};
export const ExponentCalculator = () => {
const {
base,
exponent,
result,
handleExponentChange,
handleBaseChange,
} = useExponentCalculator();
// ...
};
We could move this hook to a separate file for a more prominent separation of concerns.
Additionally, we can further separate our hook into smaller, reusable functions. In this case, we can only extract calculateExponent
.
useExponentCalculator.js
const calculateExponent = (base, exponent) => base ** exponent;
const useExponentCalculator = () => {
const [base, setBase] = useState(4);
const [exponent, setExponent] = useState(4);
const result = calculateExponent(base, exponent).toFixed(2);
// ...
};
Testing these functions is much easier than testing the entire component's code from the first example. We could test them with any Node.js testing library, which doesn't even need to support React components.
We now have our framework-specific code (React) in the code for the component and the hook, while our business logic lives in the different functions we defined later (which are framework-agnostic).
Best practices
Naming
I like to name my custom hooks after the component as a concatenation of use
and the component's name (e.g. useExponentCalculator
). I then call the file the same as the hook.
You may want to follow a different naming convention, but I recommend staying consistent in your project.
If I can reuse parts of a custom hook, I usually move it to another file under src/hooks
.
Don't overdo it
Try to be pragmatic. If a component has only a few lines of JS, it's not necessary to separate the logic.
CSS-in-JS
If you're using a CSS-in-JS library (useStyles
), you may want to move this code to another file as well.
You could move it into the same file as the hook. However, I prefer to either keep it above the component in the same file or move it into its own file if it grows too big.
Conclusion
Whether you think that using custom hooks improves your code or not, in the end, it comes down to personal preference. If your codebase doesn't include a lot of logic, the advantages of this pattern won't be too relevant for you.
Custom hooks are only one way to increase modularity; I would also highly recommend splitting components and functions into smaller, reusable chunks when possible.
The topic is also discussed on a more general level in The Pragmatic Programmer. I wrote an article covering my favorite topics of the book, so if that interests you, make sure to check that out.