Pricing Login
Pricing
Support
Demo
Interactive demos

Click through interactive platform demos now.

Live demo, real expert

Schedule a platform demo with a Sumo Logic expert.

Start free trial
Back to blog results

May 24, 2019 By Milosz Piechocki

Typing a useReducer React hook in TypeScript

Hooks (such as a useReducer hook) are a recent addition to React that enable more of your components to be written as functions by providing less complex alternatives to class features. One significant advantage is that typing function components in TypeScript is simpler and more direct.

In this article we will implement a common data fetching scenario with the useReducer hook. We will see how to take advantage of TypeScript’s discriminated unions to correctly type reducer’s actions. Finally, we will introduce a useful pattern for representing the state of data fetching operations. 

Do we need React useReducer Typescript?

We will base our code on an example from the official React documentation. The demo linked from this article is a simple implementation of a very common pattern - fetching a list of items from some backend service. In this case, we’re fetching a list of Hacker News article headers.

What functionality is missing in this little demo? When fetching data from backend it’s useful to indicate to the user that an operation is in progress and if the operation fails, to show the error to the user. Neither is included in the demo as coded.

The attached code uses useState hook to store the list of items after it is retrieved. We will need two additional pieces of state to implement our enhancements - a boolean indicating whether an action is in progress and an optional string containing the error message.

We could use more useState hooks to store this information. There’s a better way to do this, though. Notice that we’re modifying multiple pieces of state at the same time as a result of certain actions. For example, when data is retrieved from the backend, we update both the data piece and the loading indicator piece. What’s more, we’re modifying the state in multiple places. Wouldn’t it be cleaner and easier to follow if we there was only a single place in the component where the state is updated?

We can achieve it by using the React useReducer hook. It will allow us to centralize all state modification, making them easier to track and reason about.

The type of useReducer

Let’s take a look at React useReducer’s type signature (I simplified the code slightly by taking initializer out of the picture).

useReducer TypeScript Example

function useReducer<R extends Reducer<any, any>>(
 reducer: R,
 initialState: ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

Our React hook is a function (yes, React hooks are just functions). It accepts two parameters: reducer and initialState.

The first parameter’s type must extend Reducer<any, any>. Reducer is just an alias for a function type that takes a state object and an action and returns an updated version of the state object. In other words, reducer describes how to update the state based on some action.

type Reducer<S, A> = (prevState: S, action: A) => S;

The second parameter allows us to provide the initial version of the state. ReducerState is a conditional type that extracts the type of the state from Reducer type.

Finally, useReducer returns a tuple. The first element in the tuple is the recent version of the state object. We will render our component based on values contained in this object. The second item is a dispatch function. 

What is react dispatch function?

It is a function that will let us dispatch actions that will trigger state changes. Similarly to ReducerState, ReducerAction extracts action type from Reducer.

Behold the power of static typing - reading a well typed function’s signature is often enough to understand its purpose.

Typing state for useReact Typescript

Now is the time to fill in the gaps and define types representing state and actions.

Typing state example for useReact 

It was already mentioned that apart from the data received from the server, we’re also going to store a flag indicating whether we’re loading that data and an optional error message.

Therefore, the shape of state can be described with the following types:

type State = {
 data?: HNResponse;
 isLoading: boolean;
 error?: string;
}

type HNResponse = {
 hits: {
 title: string;
 objectID: string;
 url: string;
 }[]
};

HNResponse interface is based on the response received from https://hn.algolia.com/api/v1/search endpoint which we’re going to use in this example. It is a free service that returns headers of Hacker News articles.

Typing actions with TypeScript discriminated unions

Action is an object that represents some event in our application and result in modification of the state. What kind of actions are there in our app?

The first action describes the fact that the user typed some text into the search text field. This action will initiate a backend request.

The second action will be triggered when the data from the backend is fetched. Action object should contain this data.

The third action will represent an error that occurred during data fetching. It should encompass the error message.

TypeScript discriminated union example

How to represent the type of all these actions in TypeScript? We can take advantage of a very useful concept called discriminated union type.

type Action =
 | { type: 'request' }
 | { type: 'success', results: HNResponse }
 | { type: 'failure', error: string };

Action is a union of three object types. What makes it special is the fact that all of those types have a common property called type. The type of this property in each interface is a different literal type. This lets us distinguish between those types.

Why is this useful? TypeScript creates automatic type guards for discriminated unions. This means that if we write an if statement in which we compare the type property of given Action object with a specific type (e.g. ‘success’), the type of the object inside the statement’s body will be narrowed to the matching component of the union type.

For example, in the following code, accessing action.results will not cause a compile error because the type of action inside the body of the if statement will be appropriately narrowed!

function display(action: Action) {
 if (action.type === 'success') {
 console.log(action.results);
 }
}

Implementing React useReducer

We’re all good to implement the reducer. As already mentioned, it takes a state and an action and returns an updated state.

For request action, we’re going to set isLoading flag to true.

The success action will disable isLoading flag and also set data to the results received from the server.

Finally, failure action will also disable isLoading and set the error message.

React useReducer example

function reducer(state: State, action: Action): State {
 switch (action.type) {
 case 'request':
 return { isLoading: true };
 case 'success':
 return { isLoading: false, data: action.results };
 case 'failure':
 return { isLoading: false, error: action.error };
 }
}

Thanks to discriminated unions, we can access action’s properties inside case blocks in a type-safe way.

Using the useReducer hook

All that is left is to pass our reducerto useReducer hook.

useReducer hook example

const [{
 data,
 isLoading,
 error
 }, dispatch] = useReducer(reducer, { isLoading: false });

I’m passing the reducer function to the hook along with initial state which has isLoading set to false and the remaining properties undefined. The result is a pair with the current state object as first element (which I’m instantly destructuring) and dispatch function as the second element.

Next, I need to update the usage of useEffect react state hook so that it dispatches relevant actions.

useEffect hook example

 useEffect(() => {
 let ignore = false;

 dispatch({ type: 'request' });
 axios(`https://hn.algolia.com/api/v1/search?query=${query}`).then(
 (results) => { if (!ignore) dispatch({ type: 'success', results: results.data }); },
 (error) => dispatch({ type: 'failure', error }),
 )

 return () => { ignore = true; }
 }, [query]);

Finally, we should update the JSX to take the new pieces of state into account and show the loading indicator and the error message when available.

 return (
 <div>
 <input value="{query}" onchange="{e" ==""> setQuery(e.target.value)} />
 {isLoading && <span>Loading...</span>}
 {error && <span>Error: {error}</span>}
 <ul>
 {data && data.hits && data.hits.map(item => (
 <li key="{item.objectID}">
 <a href="{item.url}">{item.title}</a>
 </li>
 ))}
 </ul>
 </div>

The whole component implementation can be found here. Note that we’re still using useState hook to store the query. This information is completely unrelated to the rest of the state, therefore there would be no advantage in including it the state managed by useReducer.

useReducer React hook: State representation

If we take a look at the interface representing state of this component, we will notice that some combinations of properties are not valid.

For example, it is not possible that isLoading === true while data is not empty.

Similarly, error and data cannot be both defined at the same time.

How can we improve this? Let’s use discriminated unions again!

type State =
 | { status: 'empty' }
 | { status: 'loading' }
 | { status: 'error', error: string }
 | { status: 'success', data: HNResponse 

Why is this approach better? Because it makes illegal states unrepresentable. The previous interface definition allowed certain combinations of property values even though we were sure that they would never occur in reality.

In a more complex component this could force us to add some type casts or handle impossible situations. Thanks to discriminated unions we can eliminate this issue.

It is generally a good idea to make your types match reality as closely as possible.

Please find the updated implementation here.

Summary: useReducer React hook in TypeScript

In this article we have looked at typing the useReducer React hook in a real-world scenario. What’s more, we’ve learned some patterns of typing state and actions using discriminated unions. Finally, we’ve seen how advanced TypeScript features can help make our types more precise.

Complete visibility for DevSecOps

Reduce downtime and move from reactive to proactive monitoring.

Sumo Logic cloud-native SaaS analytics

Build, run, and secure modern applications and cloud infrastructures.

Start free trial
Milosz Piechocki

Milosz Piechocki

Milosz Piechocki is a senior frontend engineer at Sumo Logic focused on developing the user interface for the company’s metrics solution. He’s passionate about modern web development and functional programming, and in his spare time, runs a personal blog, codewithstyle.info, speak at meetups and tech conferences, and loves to hike in the mountains.

More posts by Milosz Piechocki.

People who read this also enjoyed