Redux Toolkit popularity is growing every month. What exactly helps developers to write code faster, easier, more clearly? One of the helpers is createSlice
function. createSlice
takes an object of reducer functions, a slice name, and an initial state value and lets us auto-generate action types and action creators, based on the names of the reducer functions that we supply. It also helps you organize all of your Redux-related logic for a given slice into a single file.
What is createSlice?
It's a function that deals with everything you need for each slice, do you remember using createAction
and createReducer
manually? Now it's available in this specific slice function.
So what the returned object from createSlice
contains:
name
: a parameter that will be the prefix for all of your action typesinitialState
: the initial values for our reducerreducers
: it's an object where the keys will become action type strings, and the functions are reducers that will be run when that action type is dispatched.
The other benefit of using createSlice
is our files structure. We can put all of our Redux-related logic for the slice into a single file. You'll see how to do it in our tutorial.
Slice configuration
We have made basic Redux configuration with Redux Toolkit. But what is the most important benefit of using Toolkit? Definitely, it's createSlice
function that you will probably use for most application you're developing.
If you don't want to start from zero, you can use our basic Redux configuration with Redux Toolkit.
To our previous little app with fake authorization, we're gonna add a feature to show users data to logged user.
Firstly let's create file src/store/users.js
and create our slice:
import { createSlice } from '@reduxjs/toolkit'
// Slice
const slice = createSlice({
name: 'users',
initialState: {
users: []
},
reducers: {
getUsers: (state, action) => {
state.users = action.payload;
},
},
});
export default slice.reducer
That's basic slice configuration, it contains name
, initialState
and reducers
parameters.
Adding actions
Now let's add actions. To keep it clear and simply add it in our slice's file. We don't have to write them individually in separate file anymore. We can export all the actions. the reducer, the asynchronous thunk and selector
that gives us access to state from any component without using connect
.
import { createSlice } from '@reduxjs/toolkit'
+ import { api } from '../api/index'
// Slice
const slice = createSlice({
name: 'users',
initialState: {
users: [],
},
reducers: {
usersSuccess: (state, action) => {
state.users = action.payload;
state.isLoading = false;
},
},
});
export default slice.reducer
// Actions
+ const { usersSuccess } = slice.actions
+ export const fetchUsers = () => async dispatch => {
+ try {
+ await api.get('/users')
+ .then((response) => dispatch(usersSuccess(response.data)))
+ }
+ catch (e) {
+ return console.error(e.message);
+ }
+}
Connecting to store
We have to connect our reducer to the store. Let's import our users
reducer and combine them all into one root reducer.
import { configureStore } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'
import user from './user'
+ import users from './users'
const reducer = combineReducers({
user,
+ users,
})
const store = configureStore({
reducer,
})
export default store;
Connecting App to API
We prepared basic configuration of our slice but we want to get data to display. We're gonna use JSONPlaceholder - fake online REST API. Let's add Axios to our app.
npm install --save axios
And lets setup our API basic configuration. Create src/api/index.js
.
import axios from 'axios'
export const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
headers: {
'Content-Type': 'application/json'
},
})
Data displaying
To extract data from our store we're gonna use useSelector
hook. It's equivalent to mapStateToProps
argument in connect
complex. We can use useSelector()
multiple times within a single function component and each call will create individual subscription to the store.
Our first action lets us fetch users data. Let's display data to the user. Create file src/components/usersList.js
containing:
import React from "react";
import { useSelector } from "react-redux";
import User from "./user";
const UsersList = () => {
const { users, isLoading } = useSelector(state => state.users);
return (
<table>
<thead>
<tr>
<td>ID</td>
<td>Name</td>
<td>Phone</td>
</tr>
</thead>
{!isLoading ? (
<tbody>
{users.map(user => (
<User user={user} />
))}
</tbody>
) : (
<div>Loading...</div>
)}
</table>
);
};
export default UsersList;
We choose users slice from our state and gets users and isLoading data.
Then create src/components/user.js
with:
import React from "react";
const User = ({ user: { id, name, phone } }) => {
return (
<tr>
<td>{id}</td>
<td>{name}</td>
<td>{phone}</td>
</tr>
);
};
export default User;
And update our app.js:
...
function App() {
const dispatch = useDispatch();
if (user) {
return (
<div>
<div>
Hi, {user.username}!
<button onClick={() => dispatch(logout())}>Logout</button>
</div>
+ <UsersList />
</div>
);
}
The last step is to fetch the data. We are using useEffect
to trigger the action after the component has been mounted.
...
+ import { fetchUsers } from "../store/users";
const UsersList = () => {
+ const dispatch = useDispatch();
const { users, isLoading } = useSelector(state => state.users);
+ useEffect(() => {
+ dispatch(fetchUsers());
+ }, [dispatch]);
return (
...
Errors and loading handling
Next, let's make it complete by adding loading
and error
handling for better app management.
...
const slice = createSlice({
name: "users",
initialState: {
users: [],
+ isLoading: false,
+ error: false,
},
reducers: {
+ startLoading: state => {
+ state.isLoading = true;
+ },
+ hasError: (state, action) => {
+ state.error = action.payload;
+ state.isLoading = false;
+ },
usersSuccess: (state, action) => {
state.users = action.payload;
state.isLoading = false;
},
}
});
Update our slice with two new reducers that are gonna control state for loading and errors. We just don't need to create endLoading
reducer because as you can see we end loading in each reducer.
After setting up reducers we have to update our actions.
+ const { usersSuccess, startLoading, hasError} = slice.actions;
export const fetchUsers = () => async dispatch => {
+ dispatch(startLoading());
try {
await api
.get("/users")
.then(response => dispatch(usersSuccess(response.data)));
} catch (e) {
- return console.error(e.message);
+ dispatch(hasError(e.message))
}
};
And here we go, our app is fetching and displaying data now!
Summary
You can still use the old-fashioned way of using Redux but Redux Toolkit simplify things. With createSlice
and useSelector
we can reduce our boilerplate code and make the code easier to develop and read.
I hope you enjoy our tutorial where we have made our first slice. You can find the complete code repository presented in this article at GitHub.