This article goes over how to to add Redux to a React TypeScript app.
Video
Watch YouTube video:
Demo
Prerequisites
Given an app bootstrapped by Create React App:
npx create-react-app my-app --template typescript && cd my-app
Install
Install the dependencies:
With npm:
npm install @reduxjs/toolkit @types/react-redux react-redux
Or with Yarn:
yarn add @reduxjs/toolkit @types/react-redux react-redux
Slice
Create the slice:
// src/slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
const initialState = {
value: 0,
};
export const name = 'counter';
const slice = createSlice({
name,
initialState,
// The `reducers` field allows us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers since
// 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.value++;
},
decrement: (state) => {
state.value--;
},
// The `PayloadAction` type allows us to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { actions, reducer } = slice;
Store
Create the store:
// src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { name, reducer } from './slice';
const store = configureStore({
reducer: {
[name]: reducer,
},
});
export default store;
Provider
Make the store available to all nested components with <Provider>
:
// src/index.tsx
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import Counter from './Counter';
import store from './store';
render(
<Provider store={store}>
<Counter />
</Provider>,
document.getElementById('root')
);
Component
Create the component:
// src/Counter.tsx
import { useDispatch, useSelector } from 'react-redux';
import { actions } from './slice';
export default function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<>
<p>{count}</p>
<button onClick={() => dispatch(actions.increment())}>+</button>
<button onClick={() => dispatch(actions.decrement())}>-</button>
</>
);
}
The UI is integrated with Redux with hooks.
Not typing the callback function in useSelector
will throw the TypeScript error:
Property 'counter' does not exist on type 'DefaultRootState'.
This can be fixed by typing state
:
// src/Counter.tsx
// ...
import store from './store';
type RootState = ReturnType<typeof store.getState>;
export default function Counter() {
const count = useSelector((state: RootState) => state.counter.value);
// ...
}
Hooks
Alternatively, type the hooks:
// src/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import store from './store';
type AppDispatch = typeof store.dispatch;
type RootState = ReturnType<typeof store.getState>;
// Use throughout your app instead of `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
For easier use in components:
// src/Counter.tsx
import { useDispatch, useSelector } from 'react-redux';
import { actions } from './slice';
-import store from './store';
-type RootState = ReturnType<typeof store.getState>;
+import { useAppDispatch, useAppSelector } from './hooks';
export default function Counter() {
- const count = useSelector((state: RootState) => state.counter.value);
- const dispatch = useDispatch();
+ const count = useAppSelector((state) => state.counter.value);
+ const dispatch = useAppDispatch();
return (
<>
<p>{count}</p>
<button onClick={() => dispatch(actions.increment())}>+</button>
<button onClick={() => dispatch(actions.decrement())}>-</button>
</>
);
}