Developing modern web applications involves not only UI building but also state management. One of the most widespread libraries for this is Redux. In this tutorial you will learn how to setup Redux using latest libraries and techniques available in 2020 and Redux Toolkit which will simplify your logic and ensure that your setup has good defaults.
Why to choose Redux Toolkit
Redux is a good fundament for the opening but to simplify working it is recommended to use the Redux Toolkit. It was created to help address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
It has functions that build according to Redux best practices . It includes several utility functions that simplify the most common Redux use cases, including store setup, defining reducers, immutable update logic with Immer, and even allows creating entire "slices" of state at once without need to write action creators.
It comes as preconfigured bundle of the most widely used Redux addons, like Redux Thunk for async logic and Reselect for writing selector functions, so that you can use them right away. It also allows your to overwrite all of its settings, for example its very easy to use redux-saga or any other middleware with it.
How to setup Create-React-App With Redux
For this redux tutorial lets start with setup new react application with CRA:
npm install -g create-react-app
create-react-app redux-tutorial
cd redux-tutorial
Next we will add redux with:
npm install --save react-redux @reduxjs/toolkit
Firstly configure store. Create file src/store/index.js containing:
import { configureStore } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'
const reducer = combineReducers({
// here we will be adding reducers
})
const store = configureStore({
reducer,
})
export default store;
configureStore accepts a single object rather that multiple function arguments. It's because under the hood, the store has been configured to allow using the Redux DevTools Extension and has had some Redux middleware included by default.
Then we need to connect our store to the React application. Import it into index.js like this:
...
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Provider wraps the App and the whole application has access to Redux. If you start your application with npm start and open Redux Dev Tools you should see @@INIT. Congratulations you have setup redux!
How To Structure Your Redux
Lets now setup Redux authentication and implement simple login form and logout button shown after authentication. Redux itself does not care about how your application's folders and files are structured. However, co-locating logic for a given feature in one place typically makes it easier to maintain that code. Redux.org recommend that most applications should structure files using a "feature folder" approach (all files for a feature in the same folder) or the "ducks" pattern (all Redux logic for a feature in a single file), rather than splitting logic across separate folders by "type" of code (reducers, actions, etc).
Lets add src/store/user.js store slice:
import { createSlice } from '@reduxjs/toolkit'
// Slice
const slice = createSlice({
name: 'user',
initialState: {
user: null,
},
reducers: {
loginSuccess: (state, action) => {
state.user = action.payload;
},
logoutSuccess: (state, action) => {
state.user = null;
},
},
});
export default slice.reducer
// Actions
const { loginSuccess, logoutSuccess } = slice.actions
export const login = ({ username, password }) => async dispatch => {
try {
// const res = await api.post('/api/auth/login/', { username, password })
dispatch(loginSuccess({username}));
} catch (e) {
return console.error(e.message);
}
}
export const logout = () => async dispatch => {
try {
// const res = await api.post('/api/auth/logout/')
return dispatch(logoutSuccess())
} catch (e) {
return console.error(e.message);
}
}
The store feature file contains createSlice that returns a "slice" object that contains the generated reducer function as a field named reducer, and the generated action creators inside an object called actions.
At the bottom we can import the action creators and export them directly or use them within async actions, like login and logout.
To connect reducer into Redux, we have add add it to the main reducer in store/index.js:
...
import user from './user'
const reducer = combineReducers({
user,
})
Connecting Redux to Components with useDispatch and useSelector
Our redux setup is ready. Now lets configurate Authentication form. For this we will use Formik. Type the following into your terminal:
npm install --save formik
Now we can create following src/App.js component:
...
import {useDispatch, useSelector} from 'react-redux'
import {Field, Form, Formik} from 'formik'
import {login, logout} from './store/user'
function App() {
const dispatch = useDispatch()
const { user } = useSelector(state => state.user)
if (user) {
return (
<div>
Hi, {user.username}!
<button onClick={() => dispatch(logout())}>Logout</button>
</div>
)
}
return (
<div>
<Formik
initialValues={{ username: '', password: '' }}
onSubmit={(values) => { dispatch(login(values)) }}
>
{({ isSubmitting }) => (
<Form>
<Field type="text" name="username" />
<Field type="password" name="password" />
<button type="submit" disabled={isSubmitting}>Login</button>
</Form>
)}
</Formik>
</div>
);
}
Please note that there is no connect! With useDispatch and useSelector we can now integrate Redux with pure components using hooks! We just need to wrap App with Provider and there is much less boilerplate compared to connect.
How Keep User Authenticated on Page Reload
Probably you've noticed that authentication is reseted on every page reload.
That is very easy to fix with localStorage with just a few lines added to src/store/user.js
+const initialUser = localStorage.getItem('user')
+ ? JSON.parse(localStorage.getItem('user'))
+ : null
+
const slice = createSlice({
name: 'user',
initialState: {
- user: null,
+ user: initialUser,
},
reducers: {
loginSuccess: (state, action) => {
state.user = action.payload;
+ localStorage.setItem('user', JSON.stringify(action.payload))
},
logoutSuccess: (state, action) => {
state.user = null;
+ localStorage.removeItem('user')
},
},
});
How to Store Token
My favorite API client library is Axios. I prefer Axios over built-in APIs for its ease of use and extra features like xsrf token support and interceptors.
Here is sample configuration that I often use:
const api = axios.create({
baseURL: '/',
headers: {
'Content-Type': 'application/json'
},
})
api.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Token ${token}`
}
return config
},
error => Promise.reject(error)
)
export default api
How to Redirect after login
The easiest way to redirect user after redux action is to use Redirect component provided by React.
This can be one within Login form component, for example with code like this:
if (user) {
return (
<Redirect to={'/home'} />
)
}
Top comments (1)
Hi, I'm a Redux maintainer. Always glad to see new tutorials showcasing Redux Toolkit!
I do see one small issue I want to point out. In this snippet,
localStorage.setItem()
is technically a side effect:The code will run, but that's not what a reducer is supposed to be doing.
Persistence logic like that should really be not be in a reducer - it should be at the store setup level, or possibly at the UI level. In this case, it could be done with a simple
store.subscribe()
call, in a middleware, or similar.