Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Managing app state with Redux and TypeScript - Part 2

June 23, 2020
reacttypescript

In the last post, we implemented a Redux store where actions were synchronous. In this post, we are going to start from the same store and make the actions asynchronous.

React, Redux and TypeScript

State

As a reminder, here’s our store state:

type Person = {
  id: number;
  name: string;
};
type AppState = {
  people: Person[];
};

So, our app state contains an array of people.

This app is going to be deliberately simple so that we can focus on the Redux Store and how a React component interacts with it in a strongly-typed manner.

Allowing the store to work with asynchronous actions

We are going to use Redux Thunk to allow Redux to work with asynchronous actions. This is middleware that we define when the store is created:

function configureStore(): Store<AppState> {
  const store = createStore(
    rootReducer,
    undefined,
    applyMiddleware(thunk)  );
  return store;
}

applyMiddleware is a function from Redux that allows us to add middleware to the store, and thunk is Redux Thunk’s middleware.

Actions and action creators

Our action creators are now going to be asynchronous. Here is the action creator for adding a person:

const addPerson = (personName: string) => async (
  dispatch: Dispatch<AddPersonAction>
) => {
  await wait(200);
  dispatch({
    type: "AddPerson",
    payload: personName,
  } as const);
};

We are faking an asynchronous function with a wait function in our example.

The addPerson function returns a function rather than the action object as it did when the function was synchronous. The inner function takes in a parameter, dispatch, which is another function that is used to dispatch the action. That’s a lot of functions!

Notice that we have explicitly typed the dispatch parameter with Dispatch<AddPersonAction>. Dispatch is a standard type from Redux that we’ve passed the type for our action, AddPersonAction, into. Here’s the definition for AddPersonAction:

type AddPersonAction = {
  readonly type: "AddPerson";
  readonly payload: string;
};

Here’s the action creator for removing a person:

type RemovePersonAction = {
  readonly type: "RemovePerson";
  readonly payload: number;
};
const removePerson = (id: number) => async (
  dispatch: Dispatch<RemovePersonAction>
) => {
  await wait(200);
  dispatch({
    type: "RemovePerson",
    payload: id,
  } as const);
};

Reducer

The reducer action parameter type is a created differently to the synchronous version:

type Actions =
  | AddPersonAction
  | RemovePersonAction;

We inferred the action types in the synchronous version, but here we have explicitly created the types.

The reducer function is exactly the same as the synchronous version:

function peopleReducer(
  state: Person[] = [],
  action: Actions
) {
  switch (action.type) {
    case "AddPerson":
      return state.concat({
        id: state.length + 1,
        name: action.payload,
      });
    case "RemovePerson":
      return state.filter(
        (person) => person.id !== action.payload
      );
    default:
      neverReached(action);
  }
  return state;
}

function neverReached(never: never) {}

Cool!

Connecting components

Connecting React components to the store is exactly the same as synchronous version. useSelector is used to get data from the store:

const people: Person[] = useSelector(
  (state: AppState) => state.people
);

useDispatch is used to invoke store actions:

const dispatch = useDispatch();
...
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  dispatch(addPerson(newPerson));
  ...
};
...
const dispatchNewPerson = (id: number) => () => {
  dispatch(removePerson(id));
};
...
<button onClick={dispatchNewPerson(person.id)}>Remove</button>

Nice!

This example can be found in CodeSandbox at https://codesandbox.io/s/react-redux-async-e0zcw?file=/src/index.tsx

Wrap up

Redux needs a library like Redux Thunk to handle asynchronous actions. React components interact with synchronous and asynchronous actions in the same way. The main difference is in the action creators. Asynchronous action creators return a function that eventually dispatches an action object, whereas synchronous action creators return the action object immediately.

Discuss this post on twitter

Using TypeScript with React

For React developers eager to learn TypeScript

Learn how to utilize TypeScript’s sophisticated type system to make React development faster and your code more readable
Using TypeScript with React
Find out more