Redux - The Nuts and Bolts
Overview
Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
Note, this is a general overview of Redux and not specific to React. Redux can be used with any JavaScript framework or library.
Three core concepts
- A store is an object that holds the state of your application
- An action describes the changes in the state of the application
- A reducer which actually carries out the state transition depending on the action
Three principles
- Single source of truth: The application's state is stored in a single store's object tree.
- State is read-only: To modify the state, you must dispatch an action, which describes the change.
- Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.
graph LR A[Store] --> B[Action] B --> C[Reducer] C -->|New State| A
Actions
In Redux, actions are simply JavaScript objects that carry data updates from your
application to the central store. Actions must have a type
property that indicates the
type of action being performed, then you are free to include any other data you want in
the action object.
In the following example below, we have a payload
property that contains a an object
with the data we want to send to the store, and someProperty
which is just an example
to show you can add what you want.
const NAME_OF_ACTION = 'NAME_OF_ACTION';
{
type: NAME_OF_ACTION,
someProperty: 'Some value',
payload: {
id: 1,
title: 'Some title',
// other data you want to send...
}
}
Action Creators
As the name implies, action creators are functions that create actions. They are functions that return an action object (like the one shown above).
Here is a simple example building a counter:
// actionCreators.js
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
function increment() {
return {
type: INCREMENT
}
}
function decrement() {
return {
type: DECREMENT
}
}
Confused? Why isn't there a payload
in these action
objects?
In this example, we don't need to send any data to the store, we just want to increment or decrement the counter.
Still Confused? Me too! This is a chicken or the egg situation where the action creator is created before the reducer is created to handle the action. This will start to make more sense in the next section when we see how the action is used in the reducer.
Reducers
Reducers are like the brain of Redux. They are pure functions that receive the current application state and an action as arguments. Based on the action type and payload (if present), the reducer determines how to update the state and returns the new state object.
Key points to remember about Reducers include they:
- specify how the application's state changes in response to an action sent to the store.
- are pure functions that accepts the previous state and an action, and return the next state.
- must not mutate the state. Instead, they should return a new state object.
- should be written in a way that they are easy to test.
// counterReducer.js
import { INCREMENT, DECREMENT } from './actionCreators';
const initialState = {
count: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return {
count: state.count + 1
};
case DECREMENT:
return {
count: state.count - 1
};
default:
return state;
}
}
Whats happening here?
- The reducer receives the current state and an action.
- The reducer checks the action type and updates the state accordingly.
- The reducer returns a new state object.
- The new state object is then used to update the store. (We will get to this next)
Digging a little deeper
In the previous example we only have a single property in the state object so it is safe to return a new object with just the property we are updating. In a real application you would have multiple properties in the state object.
To handle multiple properties in the state object, you would use the spread operator to copy the existing state object and then update the property you want to change. For example:
// counterReducer.js
const initialState = {
count: 0,
name: 'Jimbo'
};
function 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;
}
}
Store
In Redux, the store acts as the central hub for managing your application's state. Imagine it as a secure vault that holds the single source of truth for all your application's data. This data is represented by a JavaScript object that encapsulates all the information your app needs to function.
The store has three important methods:
-
getState()
: Returns the current state of the store -
dispatch(action)
: Dispatches an action to update the state -
subscribe(listener)
: Adds a change listener to the store
Things to remember about the store:
- There is only ever one store in a Redux application which is the single source of truth
- The store holds the application state
- The store is created by passing a reducer to the
createStore
function - The store exposes three important methods:
getState()
,dispatch()
, andsubscribe()
Creating a store
The store is created by passing a reducer function to the createStore
function from
Redux. If you remember, previously we created a reducer function called counterReducer
that had an object called initialState
that contained the initial state of our
application. Now we can create a store and pass the counterReducer
to it.
// store.js
import { createStore } from 'redux';
import counterReducer from './counterReducer';
// first, create the store by passing the reducer to the createStore function
const store = createStore(counterReducer);
Bringing it all together
Now that your head is spinning, let's bring it all together to see how actions, reducers, and the store work together to manage your application's state in Redux.
Sequence diagram
Consider our counter example where a user clicks a button to increment or decrement the counter. Here's the sequence of events:
sequenceDiagram participant Component participant Store participant counterReducer Component->>Store: Dispatch action (INCREMENT/DECREMENT) Store->>counterReducer: Call with current state and action counterReducer->>Store: Return new state Store->>Component: Notify state change (optional)
Action Dispatched: A component in your application dispatches an action to the store. This action is a JavaScript object with a type property (e.g., "INCREMENT" or "DECREMENT") indicating the desired change.
Store Receives Action: The store receives the dispatched action.
Reducer Called: The store calls the counterReducer
function you assigned to it when
creating the store. The counterReducer
receives two arguments: the current state of
the application and the dispatched action.
Reducer Updates State: Based on the action's type, the counterReducer
determines how
to update the state. If the action type is "INCREMENT", the counterReducer
increases
the count
value in the state object. If the action type is "DECREMENT", the
counterReducer
decreases the count
value in the state object. Importantly, the
counterReducer
returns a new state object reflecting the changes.
Store Updates Internal State: The store takes the new state object returned by the
counterReducer
and updates its internal state with this new object.
Components Re-render (Optional): If any components in your application are subscribed to
changes in the store's state (using store.subscribe()
), they will be notified and can
re-render themselves with the updated state.
Implementation
// Subscribe to the store to listen for changes
const unsubscribe = store.subscribe(() => {
console.log('State has changed:', store.getState());
});
// Dispatch actions to the store to update the state
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(decrement());
// Unsubscribe from the store when you no longer want to listen for changes
unsubscribe();
Binding action creators
In the example above, we are dispatching actions directly to the store. In a real application, you would bind the action creators to some click event in the UI. For example:
import { bindActionCreators } from 'redux';
import { increment, decrement } from './actionCreators';
// Get a reference to the dispatch function
const dispatch = store.dispatch;
// Use bindActionCreators to bind dispatch to your action creators
const boundActionCreators = bindActionCreators({ increment, decrement }, dispatch);
document.getElementById('increment').addEventListener('click', () => {
boundActionCreators.increment();
});
document.getElementById('decrement').addEventListener('click', () => {
boundActionCreators.decrement();
});
Full example
Copy and paste the following code snippet into a file and run it with Node.js to see how actions, reducers, and the store work together in Redux.
Expected output:
State has changed: { count: 1 }
State has changed: { count: 2 }
State has changed: { count: 1 }
State has changed: { count: 2 }
Note we are using the older CommonJS syntax for this example so you can run directly in Node.js.
const redux = require('redux');
const bindActionCreators = redux.bindActionCreators;
const createStore = redux.createStore;
// ----- ACTION CREATORS ------------------------------------------
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
function increment() {
return {
type: INCREMENT
};
}
function decrement() {
return {
type: DECREMENT
};
}
// ----- REDUCER --------------------------------------------------
const initialState = {
count: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return {
count: state.count + 1
};
case DECREMENT:
return {
count: state.count - 1
};
default:
return state;
}
}
// ----- STORE ----------------------------------------------------
// first, create the store by passing the reducer to the createStore function
const store = createStore(counterReducer);
// next, subscribe to the store to listen for changes
const unsubscribe = store.subscribe(() => {
console.log('State has changed:', store.getState());
});
// ----- BIND ACTIONS ---------------------------------------------
const actions = bindActionCreators({ increment, decrement }, store.dispatch);
actions.increment();
actions.increment();
actions.decrement();
actions.increment();
// At some point in the future, you can stop listening for changes
// by calling the function returned by store.subscribe()
unsubscribe();