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!
Top comments (12)
Thank you for sharing your knowledge. I learn React and I'm happy to read a practical example, at last. But I have an error when I try to run the code:
'React Hook "React.useState" is called in function "datasource" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks'
datasource is my useApi.
Could you help me, please?
Hi @shatvani
Where do you call useApi? Inside a Functional Component?
Hi,
Yes, from another functional component.
But now I have started a course in Udemy, because it is a hard stuff just to jim in it.
Thank you.
Yes, that makes sense. I guess React does not see "datasource" as a component and therefore you can't use a hook inside of it. But if you lear it bottom up with udemy thats definately a great idea!
Have fun!
Great post!
I'd change just one thing: the
data
object holds an array also nameddata
.Yes, this is one of the "only two hard things in Computer Science"...
I think I would actually call that object
state
(and rename the currentstate
tostatus
orapiState
)Hello @nicomartin , thanks a lot for sharing this, I loved this approach.
However I noticed that implementing this will give a linter rule error for react-hooks/exhaustive-deps because the UseEffect will depend on external data: url and SetPartData.
So I moved the SetPartData inside the UseEffect hook, put url in the dependency array (It will not change so I don't see a problem here) and made a functional update for the setData()
Please let me know what you think about this (I also made some renaming having in count the advice @targumon shared):
github.com/r-vasquez/React_mock_re...
Hi @nicomartin ,
Thanks for your great post. I would like to know, with the same sample code, in addition to the error message, how would you display a "Retry" button to fire a new API call?
Hi @mykel ,
Excellent question! In that case I think I would just move the loading logic into a new function that I can then return from the hook as well:
gist.github.com/nico-martin/24de58...
Thanks for the answer.
Hi, I am new to react. It is really not obvious to me what is the benefit of this.
It just looks a bit harder to read.
Other than not being clear about the component current state what else is the issue? could you point other articles that tackle the same problem? I could not really find anything on why having multiple states is bad or has any drawbacks.
Also how is this different from adding a isLoading and isError prop to the stats state for example?
Thank you.
The benefint is that you decouple logic (fetch) from render view.
I mean it's perfectly fine to call an api right inside your component and write the state-logic there as well. But decoupling makes it cleaner in my oppinion.
Especially if you then need to write unit tests you will benefit from this flexibility.
Has any drawbacks writing like:
āconst setPartData = (partialData) => setData(prevState => ({ ... prevState, ...partialData }));ā
It can prevent weird behaviors in more complex environments.