Impossible States in React – Predictable State Management with useReducer

Using multiple useState hooks to manage related states like data, loading, and error can lead to impossible states — combinations that should never occur in a well-designed UI. This article shows how to consolidate related states into a single useReducer hook, enabling predictable transitions and eliminating bugs like simultaneous loading and error indicators. By structuring state as a complex object and dispatching clearly defined actions, we gain control and clarity in asynchronous data flows.

impossible stateuseReducerstate managementcomplex reducer

~3 min read • Updated Oct 22, 2025

Introduction


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.


Combining States with useReducer


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 }
);

Dispatching Actions for Transitions


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' })
  );

Reducer Function with Complex State


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();
  }
};

Rendering Based on Unified State


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} />
)}

Conclusion


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.


Written & researched by Dr. Shahin Siami