Redux in React.js — Intro
Welcome to my first in a series of posts I’ll be making on learning Redux.
To start, what is Redux?
Redux is a state management tool that allows us to access and modify state across our app without having to pass it along to components as props. As our apps grow, our component trees can become very deep, and passing a piece of state down as props through several components can become very hard to maintain and debug. Redux creates a global state (called the ‘store’) that can easily be accessed throughout the project. This grants us the ability to use a single source of truth. Additionally, we can update data in the store using dispatch actions.
How do we access state?
We will wrap our project with react-redux
’s Provider
, which makes our store available throughout the app. Then, we will be able to use Redux functions to map state as props in components, which can be read as though they’re receiving them from a parent component.
How will we update state?
To update our state
, or any piece of it, we will send an action, which will be defined by us. An action is a plain JavaScript object that has a .type
attribute. The type
should be a descriptive string, like “count/incrementBy1,”
“ingredients/boilPotato,”
or “users/addNewUser.”
The first part is the feature or category the action belongs to, and the second part is what we want to happen.
const incrementBy1Action = {
type: 'count/incrementBy1'
}
Where are we sending our action to update state?
We send our actions to a reducer. This is a function that receives two arguments: our current state
, and an action object. It will use a switch case
, if else
, or loops
to update the state
the way our action wants it to, and returns the new state
:
counterReducer(state, action) => newState
Reducers must follow some rues:
- They should only calculate the new
state
value based on thestate
and action arguments - They are not allowed to modify the existing
state
. We will typically be using the spread operator(…state)
to copy the existingstate
and then make the changes necessary, returning the newstate
- They must not do any asynchronous logic, calculate random values, or cause other “side effects” (Reducers are pure functions)
Here’s an example of a simple reducer, which is able to increment our count
by 1:
function counterReducer( state = {count: 0}, action ) {
if (action.type === 'count/incrementBy1') {
return { ...state, count: state.count + 1 }
}
return state
}
So, we pass state
and an action into our reducer (note that we’ve set a default/initial state count value of 0). The reducer uses an if
statement to see if our action is asking it to increment by 1. If so, it returns the existing state
with state.count
incremented by 1. If the action we passed it wasn’t to increment by 1, it just returns state
as-is. We can add more possible actions here, and could use a switch case
instead of a series of if
statements.
Now that we have an understanding of how a reducer works, let’s re-visit our store. Remember, the store is the object where our application’s state
lives. With our App
wrapped in <Provider />
, it’s accessible throughout the app. But how is it created? The store is created by passing in a reducer. Let’s use the one we just built as an example:
import { configureStore } from '@reduxjs/toolkit'const store = configureStore({ reducer: counterReducer })
Now, store
has a method called getState
that returns the current state
value.
The store
also has a method called dispatch
. To update state
and persist the changes, we will call store.dispatch()
and pass in an action object. The store runs its reducer function, updating the new value, and then we can call store.getState()
to get the updated value:
console.log(store.getState())
// { count : 0 }store.dispatch({ type: 'count/incrementBy1' })console.log(store.getState())
// { count : 1 }
To summarize:
- We create the store using a reducer function. Upon creation, the store calls the reducer and saves the return value as its initial
state
. - When something happens in the app, like the user clicking in increment button, our code dispatches an action to the store, like
store.dispatch({ type: 'count/incrementBy1' })
- The store runs the reducer again, using the previous
state
and the current action as arguments, then assigns the return value as the newstate
. - The store then notifies all parts of the UI that are connected to it that
state
has been updated, triggering each of those components to re-render using the newstate
data.
Hopefully, this gives you a basic understanding of the information flow in Redux. Stay tuned for more exploration into Redux and its uses in our projects, and please reach out if you have any comments, tips, or questions!