In React, using multiple useState hooks to manage related values like data, loading, and error can lead to impossible states. For example, the UI might show both a loading spinner and an error message at the same time. To avoid such inconsistencies, we can consolidate related states into a single useReducer hook.
Instead of managing each state separately, we define a unified reducer with a complex state object:
const [stories, dispatchStories] = React.useReducer(
storiesReducer,
{ data: [], isLoading: false, isError: false }
);We use dispatchStories to trigger state transitions during data fetching:
dispatchStories({ type: 'STORIES_FETCH_INIT' });
fetch(`${API_ENDPOINT}react`)
.then((response) => response.json())
.then((result) => {
dispatchStories({
type: 'STORIES_FETCH_SUCCESS',
payload: result.hits,
});
})
.catch(() =>
dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
);The reducer handles all transitions and ensures consistency:
const storiesReducer = (state, action) => {
switch (action.type) {
case 'STORIES_FETCH_INIT':
return { ...state, isLoading: true, isError: false };
case 'STORIES_FETCH_SUCCESS':
return { ...state, isLoading: false, isError: false, data: action.payload };
case 'STORIES_FETCH_FAILURE':
return { ...state, isLoading: false, isError: true };
case 'REMOVE_STORY':
return {
...state,
data: state.data.filter(
(story) => action.payload.objectID !== story.objectID
),
};
default:
throw new Error();
}
};We now access loading, error, and data from a single state object:
{stories.isError && <p>Something went wrong ...</p>}
{stories.isLoading ? (
<p>Loading ...</p>
) : (
<List list={searchedStories} onRemoveItem={handleRemoveStory} />
)}By consolidating related states into a single useReducer hook, we avoid impossible states and gain more predictable, maintainable control over our UI. This approach is especially powerful for managing asynchronous data and complex state transitions in React applications.