Fundamental Concepts
- Redux Store - Where all the global states are stored. This is immutable.
- Actions - Objects that describe the state change. It can include a payload that could be used for updating the state.
- Reducers - Pure functions (does not cause mutations) that execute state changes based on the action.
Redux and Redux Toolkit
Redux Toolkit simplifies working with Redux. It is developed because pure Redux on its own requires a lot of boilerplate code that is prone to errors/bugs, like accidentally causing mutations.
To create a redux store, use the configureStore function from RTK:
// src/app/store.ts
export const store = configureStore({
reducer: {} // Empty for now
});
Make sure to also export the RootState and AppDispatch types that will be used in the selector and dispatch hooks (more on that later):
// src/app/store.ts
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
To apply Redux in the entire app, wrap the root component in the Provider from react-redux:
// src/main.ts
import { store } from "./app/store";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
);
The Provider component requires the store prop. Just pass the store created using the configureStore to make it globally available.
This is basically the initial setup. To use Redux, the store needs to have reducers with their states and actions. This can be created using slices that contains the initial state, the shape of the state (using TypeScript), the actions, and reducers.
RTK provides a convenient way to do it.
RTK Slice
In Redux Toolkit, you can create a slice, which is basically a slice of the global state with its reducers. It is defined using the createSlice function from RTK that accepts an object with three required parameters:
-
name: The name of the slice that will be used to refer to this state and prepended to its action types. -
initialState: The initial value used for the state. -
reducers: An object that contains the reducer functions for the slice’s state. Each reducer function can optionally accept two parameters:-
state: The slice’s state that can be used to modify or calculate the next state. -
action: The action that’s used by the reducer function to modify the state. This action parameter has two properties:-
type: This is derived from the slice’snameand the reducer function’s name (example:counter/increment,counter/decrement, etc.) -
payload: An optional property that could be used to modify the state.
-
-
This is an example slice for a Counter with its name, initial state, and reducers:
type CounterState = {
count: number;
};
const initialState: CounterState = {
count: 0 // Set 0 as the default counter state
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state, action: PayloadAction<number>) => {
// Note that reducers must be immutable, but with RTK,
// this action doesn't necessarily cause mutation because
// RTK uses immer under the hood to create a duplicate state
state.count += action.payload;
},
decrement: (state, action: PayloadAction<number>) => {
state.count -= action.payload;
}
}
});
After defining the slice, export the reducer and actions from it:
-
Export the reducer as default and import in the store to register it:
// src/features/counter-slice.ts export default counterSlice.reducer; // src/app/store.ts import counterReducer from "../features/counter-slice"; export const store = configureStore({ reducer: { counter: counterReducer } }); -
Then, export the actions as well, which will be used in the dispatch function calls. Dispatching these actions will run their corresponding reducers to cause state changes.
// src/features/counter-slice.ts export const { increment, decrement } = counterSlice.reducer; // src/components/counter.component.ts export default function Counter() { const dispatch = useDispatch<AppDispatch>(); }
Async Reducers
Async reducers can be created using the createAsyncThunk function from RTK. It accepts 2 required parameters and one optional:
-
typePrefix(string, required): This is the action’s type (or name). Note that on regular reducers, the actiontypeis automatically inferred by RTK by combining the slice name and reducer function’s name, but for async thunk reducers, the type needs to be explicitly defined here. -
payloadCreator(function, required): The async function that can receive a payload where async tasks or operations are performed. Its return value can be accessed in the payloadactionof the async reducer’sfulfilledreducer in theextraReducers(refer to the example below). This is useful for modifying the state (i.e. showing results of the async operation or its status). -
options(AsyncThunkOptions, optional): Thunk options
export const incrementAsync = createAsyncThunk(
"counter/incrementAsync",
async (amount: number) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return amount;
},
);
Next step is to add it in the slice.
There is an optional property in the createSlice's parameter called extraReducers that can be used to define async reducers. It accepts a function with a builder (type ActionReducerMapBuilder) parameter used to build extra reducers by chaining them using the addCase method. Each addCase can accept the type of an async reducer’s type and the function where state can be modified, like updating the status of the async process.
Use this to define the reducers in the slice.
const counterSlice = createSlice({
...
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, () => {
console.log("incrementAsync.pending");
})
.addCase(incrementAsync.rejected, () => {
console.error("incrementAsync.rejected");
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.count += action.payload;
});
}
});
Using the State in Redux Store and Dispatching Actions
The react-redux library provides convenient hooks to easily obtain slices of the state and dispatch actions using hooks:
-
useSelector- This hook can retrieve the entire global state from the redux store, or slice(s) of the state that can be used in the component. -
useDispatch- returns a dispatch function that can be used to dispatch actions that reducers will use to change the state.
Pro Tip: Define custom hooks that wrap the
useSelectoranduseDispatchhooks with the TypeScript typesRootStateandAppDispatchso that you don’t need to re-declare the types every time you use them:
// src/app/hooks.ts
import type {
// These are the exported types derived from the store
AppDispatch,
RootState
} from "./store.ts";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Usage:
// src/components/counter.component.ts
export default function Counter() {
// This way, the hooks come pre-typed, no need to redeclare types
// every time they're used
const count = useAppDispatch(state => state.counter.count);
const dispatch = useAppDispatch();
}
This is an example counter component that uses the counter state from the Redux store and dispatching actions to it to modify the state:
export default function Counter() {
const [countInput, setCountInput] = useState("1");
const count = useAppSelector((state) => state.counter.count);
const dispatch = useAppDispatch();
return (
<div>
<h2>Counter</h2>
<p>Count: {count}</p>
<div>
<label htmlFor="count-input">Counter Step</label>
<input
type="number"
id="count-input"
value={countInput}
onInput={(e) => setCountInput(e.currentTarget.value)}
/>
<button
onClick={() => dispatch(incrementByAmount(+countInput))}
>
Increment
</button>
<button
onClick={() => dispatch(decrementByAmount(+countInput))}
>
Decrement
</button>
</div>
<h3>Async</h3>
<button
onClick={() => dispatch(incrementAsync(+countInput))}
>
Increment Async
</button>
</div>
);
}























