Carl Rippon

Building SPAs

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

React Redux Hooks and TypeScript - Part 1

June 16, 2020
reacttypescript

Redux is a popular library used to manage state in React apps. In this post, we’ll use Redux to manage some state in an app in a strongly-typed fashion with TypeScript. We’ll use the hooks approach to interact with the Redux store from a React component.

React, Redux and TypeScript

State

Let’s start with the stores state object:

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.

Actions and action creators

A change to state is initiated by an action, which is an object containing everything we need to make the change. We have two actions in our example:

  • AddPerson. This is triggered when a person is added to state. The action object will contain the person to be added.
  • RemovePerson. This is triggered when a person is removed from state. The action object will contain the id of the person to be removed.

Actions creators are functions that create and return action objects. Here are our action creators for our two actions:

function addPerson(personName: string) {
  return {
    type: "AddPerson",
    payload: personName,
  } as const;
}

function removePerson(id: number) {
  return {
    type: "RemovePerson",
    payload: id,
  } as const;
}

Notice that we use const assertions on the return object so that the properties in the actions are readonly.

Notice also that we haven’t explicitly created types for our actions because we are going to infer these from the action creator functions.

Reducer

The reducer is a function that will update the state. First, we will create a type for the action parameter in preparation for this function implementation.

type Actions =
  | ReturnType<typeof addPerson>
  | ReturnType<typeof removePerson>;

This is a union type of all the actions. We have used the typeof keyword to get the type for the action creators and then the ReturnType utility type to get the return type of those functions. Using this approach, we don’t need to create types for the action objects explicitly.

The reducer function is as follows:

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

We explicitly type the function parameters and allow the return type to be inferred.

Notice that the switch statement on the action type property is strongly-typed, so, if we mistype a value, an error will be raised.

The action parameter within the branches of the switch statement has its type narrowed to the specific action that is relevant to the branch. If we hover over the payload property in the branches we’ll see that it has been narrowed to the correct type:

payload type

Notice that we use the never type in the default switch branch to signal to the TypeScript compiler that it shouldn’t be possible to reach this branch. This is useful as our app grows, and we need to implement new actions.

Store

We create a function to create the store which uses the createStore function from Redux:

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

Typing the store is straightforward. We use the generic Store type from the core Redux library passing in the type of our app state which is AppState in our example:

The combineReducers function from Redux is used to create the rootReducer:

const rootReducer = combineReducers<AppState>({
  people: peopleReducer,
});

combineReducers has a generic type parameter for the store’s state type, which we pass in.

Connecting components

Moving on to connecting components now.

First we need to wrap the Provider component from React Redux around the topmost component that needs access to the store. We need to pass our store into the Provider component:

const store = configureStore();
const App = () => (
  <Provider store={store}>
    <Page />
  </Provider>
);

Inside the component we can use the useSelector hook from React Redux to get data from the store:

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

We pass a function into useSelector that takes in the state from the store and returns the relevant piece of data. We explicitly type the state parameter with our AppState type.

We can use the useDispatch hook to invoke store actions:

const dispatch = useDispatch();

useDispatch returns a function that we name dispatch. We then invoke actions using dispatch by passing our action creators into it:

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>

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

Wrap up

When actions are synchronous, we can implement our strongly-typed Redux code in a reasonably straightforward manner making heavy use of inference. The way that TypeScript narrows the action type in reducers is really smart, and the use of never is a nice touch.

How do we implement asynchronous strongly-typed actions? We’ll find out in the next post.

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Using TypeScript with React
Find out more

Want more content like this?

Subscribe to receive notifications on new blog posts and courses

Required
© Carl Rippon
Privacy Policy