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.

Learn TypeScript

NEW!🎉LIMITED LAUNCH DISCOUNT

An interactive course for JavaScript developers who want to learn modern TypeScript

  • Learn to use TypeScript's amazing type system with your existing JavaScript skills to boost your productivity
  • Over 70 interactive tutorial style lessons
  • Quizzes in each chapter to reinforce knowledge
  • Covers beginner topics through to advanced
Learn TypeScript
Find out more