How to Use Redux Toolkit with TypeScript
Responses (0)
The Redux Toolkit package is intended to be the standard way to write Redux logic. It simplifies the store creation and decreases the amount of boilerplate code.
Let's rewrite the application we wrote in one of the earlier posts from scratch to see what will change.
Installation#
First of all, we need to create a new app. We can use Create React App with TypeScript template and then add Redux Toolkit or we can use a template that already includes everything we need.
npx create-react-app redux-toolkit-app --template redux-typescript
# or
yarn create react-app redux-toolkit-app --template redux-typescript
Now, if you look in the redux-toolkit-app directory you will see not only App.tsx but also 2 new directories:
app
, it contains the store configuration;features
, it contains all the store parts.
For now, let's clear the features
directory, we won't need anything inside of it.
New Concept#
Redux Toolkit introduces a new concept called a slice
. It is an object that contains a reducer function as a field named reducer
, and action creators inside an object called actions
.
Basically, a slice is a store part responsible for a single feature. We may think of it as a set of actions, a reducer, and an initial state.
Slices allow us to write less code and keep feature-related code closer thus increasing cohesion.
Todos Slice#
For our first slice, we're going to use a createSlice
function. Let's start with re-creating typings for our store:
// features/todos/todosSlice.ts
// We can safely reuse
// types created earlier:
type TodoId = string;
type Todo = {
id: TodoId;
title: string;
completed: boolean;
};
type TodosState = {
list: Todo[];
};
Then, we create an initial state:
// features/todos/todosSlice.ts
const initialState: TodosState = {
list: [],
};
And now, we can start creating a slice:
// features/todos/todosSlice.ts
export const todosSlice = createSlice({
// A name, used in action types:
name: "todos",
// The initial state:
initialState,
// An object of "case reducers".
// Key names will be used to generate actions:
reducers: {
addTodo(
// Arguments of actions are basically the same.
// The first one is the state,
// the second one is an action.
state: TodosState,
// `PayloadAction` is a generic-type
// that allows you to specify an action
// with a typped payload.
// In our case, this payload is of `Todo` type:
action: PayloadAction<Todo>
) {
// RTK allows us to write
// “mutating” logic in reducers.
// It doesn't actually mutate the state
// because it uses the Immer library,
// which detects changes to a "draft state"
// and produces a brand new
// immutable state based off those changes:
state.list.push(action.payload);
},
toggleTodo(
// You can skip typing the state,
// it will be inferred from the `initialState`.
// I prefer to explicitly type everything I can
// but this is not obligatory.
// For example, this will work as well:
state,
action: PayloadAction<TodoId>
) {
const index = state.list.findIndex(
({ id }) => id === action.payload);
if (index) {
state.list[index].completed = !state.list[index].completed;
}
},
},
});
The todosSlice
object now contains actions
and reducer
fields. Let's export everything they have from the module:
// features/todos/todosSlice.ts
// Export all of the actions:
export const { addTodo, toggleTodo } = todosSlice.actions;
// It is a convention to export reducer as a default export:
export default todosSlice.reducer;
Todos Selector#
To use the state in our components, we need to create a state selector. For that, we need to define a RootState
type. Let's change the app/store.ts
a bit:
// app/store.ts
import { configureStore } from "@reduxjs/toolkit";
// Import our reducer from the sluce:
import todosReducer from "../features/todos/todosSlice";
// Use `configureStore` function to create the store:
export const store = configureStore({
reducer: {
// Specify our reducer in the reducers object:
todos: todosReducer,
},
});
// Define the `RootState` as the return type:
export type RootState = ReturnType<typeof store.getState>;
Now return to todosSlice.ts
and create a selector:
// features/todos/todosSlice.ts
// ...
// Import the `RootState` type:
import { RootState } from "../../app/store";
// ...
// Create and export the selector:
export const selectTodos = (state: RootState) => state.todos.list;
Using in Components#
The code of our component will be almost the same, except for imports and selector usage. Let's review it:
// features/todos/AddTodo.tsx
import React, { useState } from "react";
import { useDispatch } from "react-redux";
// Import the action from slice:
import { addTodo } from "./todosSlice";
// The rest of the code stays the same:
export const AddTodo = () => {
const [title, setTitle] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
setTitle("");
dispatch(
addTodo({
id: Date.now().toString(),
completed: false,
title,
})
);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="todoName"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button>Add Todo</button>
</form>
);
};
And the list:
// features/todos/TodoList.tsx
import React from "react";
import {
useSelector,
useDispatch,
TypedUseSelectorHook
} from 'react-redux'
// Import selector and action from slice:
import { selectTodos, toggleTodo } from "./todosSlice";
// We use RootState we defined earlier
// to make `useSelector` understand
// the store structure via type assertion
// with the `TypedUseSelectorHook` generic:
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
export const TodoList = () => {
const dispatch = useDispatch();
// Now, use the selector inside right away,
// no need to destructure the result:
const todos = useTypedSelector(selectTodos);
// The rest of the code stays the same:
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
{todo.title}
</li>
))}
</ul>
);
};
Comparison with “Vanilla Redux”#
We gain some advantages using RTK, such as:
There is much less code than before. We have only 1 file per store part now instead of a bunch of files.
We don't need to “patch” the useSelector hook, it works out of the box.
RTK increases cohesion by making us place the related code closer.
We don't need to type every single action by hand anymore.
There are however disadvantages as well:
RTK is another dependency that will take resources to maintain.