Let's start diving into Toolkit by looking at slices from the main point. Whatever we do inside slices, they generate the usual reducers and actions, which we pass to Redux.
In other words, slices do not add any new features to Redux. They automate routines, reduce the amount of code, and provide more convenient handles for controlling actions and states.
Let us create a slice. We need at least three components — a name, an initial state, and a set of reducers:
import { createSlice } from '@reduxjs/toolkit';
// Initial value
const initialState = {
value: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
// Reducers in slices mutate the state and return nothing to the outside
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1
},
// An example with some data
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
});
Name
We use the name as a prefix when naming the action. In the picture to the left, you see the Navigation. It helps to debug by showing us where the action came from:
Initial state
The initial state refers to the underlying data structure and some initial data, if any. It can be the value 0 for a counter, for example. Note that the initial state does not include data you pump out via the API. We fill them in later through actions.
Reducers
Reducers in Toolkit are similar to those in Redux but with some differences. Each reducer corresponds to a specific action, so there's no switch
construction inside, and the reducers themselves are very small. And there is a direct change of state within the Reducers. How is that possible?
Despite the conceptual beauty and purity of Redux, it becomes inconvenient to work with when a state is deeply nested. The prohibition of direct changes generates complex constructions. We have to write them to update deeply hidden data:
{
...state,
firstLevel: {
...state.firstLevel,
secondLevel: {
...state.firstLevel.secondLevel,
thirdLevel: {
...state.firstLevel.secondLevel.thirdLevel,
property1: action.data
},
},
},
}
There are many libraries in JavaScript to solve this problem, but they all require learning another tool. This tool reduces the amount of code but introduces another level of abstraction with all its challenges and complexities of use. It went on until the Immer appeared. This library allows you to trace direct changes within an object to update the original without mutations. In other words, we create a Redux-style copy:
import produce from 'immer';
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
},
];
// The draft contains the same data as `baseState`, but it is wrapped in `Proxy`
// So we can track changes and use the info to update `baseState`
const nextState = produce(baseState, (draft) => {
draft[1].done = true;
draft.push({title: 'Hexlet teach me'});
});
// They are different objects
nextState !== baseState;
Immer handles it like the reducers in Redux in an unchangeable style instead of directly changing the baseState
.
Every reducer in Toolkit works like a callback from Immer, into which we pass the draft. Now we can mutate the state, but internally, everything works as if we didn't. This approach preserves all features of Redux, including its DevTool, the utility for analyzing what happens in the browser. And this is the best part. We get the perks of both worlds, keeping the entire Redux ecosystem intact.
And finally, exports. The createSlice()
function generates a re-renderer and its actions. The official documentation recommends exporting reducers by default and actions by names:
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
Don't forget to add each new directory to the store:
export default configureStore({
reducer: {
counter: counterReducer,
lessons: lessonsReducer,
// And all other reducers
},
});
Batch
In cases with several slices, you should update the state with several simultaneous actions. If you do it one by one, a component will be redrawn:
// file: components/App.jsx
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { addUsers } from '../slices/usersSlice.js';
import { addPosts } from '../slices/postsSlice.js';
import Posts from './Posts.jsx';
export default () => {
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
// Get data from users and posts
const { data: posts } = await axios.get('/posts');
const { data: users } = await axios.get('/users');
dispatch(addPosts(posts));
dispatch(addUsers(users));
});
fetchData();
});
return (<Posts />)
};
// file: components/Posts.jsx
export default () => {
// Pulling data from the store. state means the whole state
const users = useSelector((state) => state.usersSlice.users);
const posts = useSelector((state) => state.postsSlice.posts);
const renderPost = (post) => {
const author = users.find((user) => user.id === post.authorId);
// Error! Users not yet added to the store
const body = `Author: ${author.name}. Text: ${post.body}.`;
return <div>{body}</div>;
};
return (
{posts.map(renderPost)}
);
};
The code renders the Posts
component whenever the state changes. It happens twice: when we add posts dispatch(addPosts(posts))
and when we add users dispatch(addUsers(users))
.
In the first case, we have a problem. We cannot find the author since we have not added the users yet. To avoid this, we have a batch()
function, which allows you to combine several state handlers:
// file: components/App.jsx
import React, { useEffect } from 'react';
// importing `batch`
import { batch } from 'redux';
import { useDispatch } from 'react-redux';
import { addUsers } from '../slices/usersSlice.js';
import { addPosts } from '../slices/postsSlice.js';
import Posts from './Posts.jsx';
export default () => {
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
// getting the data of users and posts
const { data: posts } = await axios.get('/posts');
const { data: users } = await axios.get('/users');
// `batch` takes a function that we can use to dispatch actions
batch(() => {
dispatch(addPosts(posts));
dispatch(addUsers(users));
});
};
fetchData();
}, []);
return (<Posts />)
};
In this case, when uploading posts and users and adding them to the table, the Posts
component is rendered once — after we have added all the data. Note the internal function fetchData()
. According to the documentation, we shouldn't declare the function passed to useEffect()
as asynchronous, so we created an internal asynchronous function.
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.