React Redux Hooks and TypeScript - Part 1
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.
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:
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: