Nico Martin
Posted on April 12, 2020
One thing first. I really like the flexibility of React. Going through the official React documentation I don't find a lot of must-use patterns or anti-patterns. The goal is clear: React is the framework, use it however you want. And in my opinion that's also one of the main advantages over more "opinionated" frameworks like VueJS or Angular.
The only problem is that this makes it quite easy to write messy code without even noticing. Let's take a very basic example. Let's assume you need to fetch some data:
// ./PostList.jsx
import React from 'react';
const PostList = () => {
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState('');
const [data, setData] = React.useState([]);
React.useEffect(() => {
setLoading(true);
fetch('https://api.mysite.com')
.then((response) => response.json())
.then((data) => {
setLoading(false);
setData(data);
})
.catch((e) => {
setLoading(false);
setError('fetch failed');
});
}, []);
if (loading) {
return <p>loading..</p>;
}
if (error !== '') {
return <p>ERROR: {error}</p>;
}
return (
<React.Fragment>
<p>Data:</p>
<ul>
{data.map((element) => (
<li>{element.title}</li>
))}
</ul>
</React.Fragment>
);
};
At first sight this look ok. And to be honest that's pretty much how I made my api calls ever since I started with hooks.
The problem
But then there was this Tweet by Aleksej Dix, that made me thinking:
please ignore my stupid reply. I completely missunderstood his point at this time 🤦♂️
The problem seems to be pretty clear. There is no clear definition of what status the component has at any given time. The component status always depends on a combination of different "React-states". Maybe in this very simple example it's not too hard to "guess" the component states and handle them appropriately. But if you think about more complex examples in the wild you will quickly get into some troubles.
The second thing that bothered me was that the logic and the presentation are all mixed up in one component. It's not too bad but I just like to have a clear separation of those tasks. Also this makes it nearly umpossible to write meaningful unit tests.
The solution: custom hooks
After some discussions with friends and collegues I really wanted the try this one aproach: To create a custom hook that handles the fetch and the data so the actual component only needs to display the outcome. And here's my solution.
// ./useApi.jsx
import React from 'react';
export const apiStates = {
LOADING: 'LOADING',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR',
};
export const useApi = url => {
const [data, setData] = React.useState({
state: apiStates.LOADING,
error: '',
data: [],
});
const setPartData = (partialData) => setData({ ...data, ...partialData });
React.useEffect(() => {
setPartData({
state: apiStates.LOADING,
});
fetch(url)
.then((response) => response.json())
.then((data) => {
setPartData({
state: apiStates.SUCCESS,
data
});
})
.catch(() => {
setPartData({
state: apiStates.ERROR,
error: 'fetch failed'
});
});
}, []);
return data;
};
// ./PostList.jsx
import React from 'react';
import {apiStates, useApi} from './useApi.jsx'
const PostList = () => {
const { state, error, data } = useApi('https://api.mysite.com');
switch (state) {
case apiStates.ERROR:
return <p>ERROR: {error || 'General error'}</p>;
case apiStates.SUCCESS:
return (
<React.Fragment>
<p>Data:</p>
<ul>
{data.map((element) => (
<li>{element.title}</li>
))}
</ul>
</React.Fragment>
);
default:
return <p>loading..</p>;
}
};
Yes, you could argue that the code is bigger now. But in the end we now have two completely separate functions, where each one has their single job. A hook that fetches the content and a component that displays the data. BTW, the hook could very well be used as some kind of high-order-hook that handles all API-requests of your application.
But more than this we can be sure that our hook will always return this one standardized object. A state (which has to be one of the defined apiStates
), an error and a data-Array.
Even if we forget to reset the error after a second try it should not matter. If error
is not empty we still know that the fetch was successfull because of the state
we got from the hook.
My return object is of course a very simplified example. If you have more complex data it might makes sense to adjust those properties an make them more flexible (for example state
and "generic" context
). But I think it's enough to get the idea.
In my opinion this is so much more stable than the previous aproach. And last but not least it makes it easier to test both functions with unit tests.
Of course this is only one possible aproach to have propper state handling and separation of logic and view. So I'd really like to get your feedback in the comments!
Posted on April 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.