The key concepts of Redux you need to know as a React developer

Redux is the most popular state management library for frontend applications. It can be used with vanilla JavaScript, yet it is often coupled with React. Today we are gonna go over some concepts, that will help you understand the whole Redux flow.
Why do we need Redux?
Redux allows our application to have a single store for its state. Imagine a big object that contains all the information that your components need to reach, or at least most of the state. Some basic state can be stored within the components, such as display password functionality on an input.
This is very convenient when we need to build large applications with a lot of data, we keep it "all" in a single location and we don't have to care about sending props several levels deep in our component tree.
Core parts of Redux
Some parts of the terminology might be a bit confusing for a lot of newcomers, yet after using Redux for a bit, you will get used to it.
Store - The object that holds the global state of our application
Actions - Actions are plain objects with a mandatory type property and potentially a payload property with some additional data (it can be called however we want, payload is pretty common.
Action Creators - Functions that return the action object described above. Used mainly for consistency or additional logic, as they always return the same object, so it is easier to call a function than writing an object literal from scratch.
Reducers - A pure functions that determine how the state in the store should change based on the action they receive.
Dispatch - Dispatch is a method on the store object provided by Redux, which accepts an action and triggers the change of the state
Selectors - Functions that allow us to access certain values on the store, we can get the content of the store with the getState method on the store object
Redux data flow
So we know, that the global state is in our Store. We also know, that a reducer is responsible for changing the state and that actions determine the change as they are dispatched and passed to reducers.
The diagram above shows explains the mentioned events in a more visual fashion.
React with Redux
Now, let's look at some code. To use Redux in our React application we need these two libraries.
npm i redux react-redux
Then we can build a simple counter app.
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { useSelector, useDispatch } from 'react-redux'
const App = () => {
// Hook for using dispatch functionality
const dispatch = useDispatch()
// Hook for using selector functionality
const count = useSelector(state => state.count)
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>Add</button>
<button onClick={() => dispatch(decrement())}>Substract</button>
</div>
)
}
// Action creator for incrementing
const increment = () => ({
type: 'INCREMENT',
})
// Action creator for decrementing
const decrement = () => ({
type: 'DECREMENT',
})
// Initial state
const initialState = {
count: 0,
}
// Our reducer function
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }
case 'DECREMENT':
return { ...state, count: state.count - 1 }
default:
return state
}
}
// Our store
const store = createStore(counterReducer)
ReactDOM.render(
// Redux wrapper
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
As you can see there is a lot of boilerplate for even that simple application. Yet, this structure makes a more complex state manageable.
It is important to note, that the reducer function is run by react-redux even before the first dispatch, so we need to have a default state in our reducer, as it determines the initial state.
Multiple Reducers
In more complex applications we will be managing parts of the state separately, with their own reducers and actions, to do that, we can take advantage of the combineReducers function.
//...
import {combineReducers} from "redux"
const rootReducer = combineReducers({
counter: counterReducer,
//...
})
An important note here is, that the combineReducers passes only the part of the state that belongs to the reducer, not the global state, therefore it does not have to be an object.
Asynchronous actions with Thunk
Our simple app does not talk to any API, therefore the code is synchronous. If we wanted to create an asynchronous action, we will run into trouble.
const users = async () => {
const res = await fetch("someApi/users")
const data = await res.json()
return {
type: 'USERS',
payload: data
}
}
Actions have to return plain objects, we return one at first glance, yet the code is compiled by Babel, where the async/await is translated into a more complex code, that does not fulfill the plain object condition.
To solve it, we can install a piece of middleware called redux-thunk, which allows us to either return a plain object or function, if we return a function we get the option to manually dispatch the object after our request is completed.
// We need to change our store creation
//...
import {createStore, applyMiddleware) from "redux"
import thunk from "redux-thunk"
const store = createStore(rootReducer, applyMiddleware(thunk))
Then our action creator will look like this.
const users = () => async (dispatch) => {
const res = await fetch("someApi/users")
const data = await res.json()
dispatch({
type: 'USERS',
payload: data
})
Redux alternatives
There are other management libraries we can use e.g. MobX or Recoil (experimental library by Facebook). If we want to completely omit Redux, we can achieve similar functionality with the combination of Context API and the useReducer hook.
