Opening a new topic... Recording my journey of learning to use Redux, mainly from the Redux Chinese official website, Redux Getting Started video series and the notes and transcripts from the tutorials inside Notes and Transcripts.
This includes the three principles of Redux, Reducer, getState, dispatch, subscribe principles and implementation.
First of all, it is important to clarify that while Redux is a great state management tool, you should consider whether it is suitable for your scenario.
Do not use Redux just because someone said you should; instead, take some time to understand the potential benefits and trade-offs of using it.
This article is based on the first 8 episodes of the teaching video, explaining the three principles of Redux and Reducers in Redux, as well as the principles and implementation of (getState, dispatch, subscribe) and createStore, and implements a simple counter. After watching, you will have a general understanding of Redux.
The reason for learning is mainly that many of the recent projects I have seen use Redux to some extent, if I don't learn it, I won't understand it at all
Redux Getting Started - Video Series
Dan Abramov, the creator of Redux, demonstrates various concepts in 30 short clips (2-5 minutes each). The linked GitHub repository contains notes and transcripts of the videos.
Redux Getting Started Video Series
Notes and Transcripts
Introduction#
What is Redux
Redux is a state container for JavaScript applications, providing predictable state management.
It allows you to develop applications that behave consistently across different environments (client, server, native apps) and are easy to test. Not only that, it also provides a fantastic development experience, such as a time-travel debugger that allows you to edit and preview in real-time.
In addition to being used with React, Redux also supports other UI libraries. It is small and powerful (only 2kB, including dependencies), yet has a robust ecosystem of plugins.
When to Use Redux#
It is important to clarify that while Redux is a great state management tool, you should consider whether it is suitable for your scenario. Do not use Redux just because someone said you should - take some time to understand the potential benefits and trade-offs of using it.
When you encounter the following issues in your projects, it is advisable to start using Redux:
- You have a lot of data that changes over time
- You want the state to have a single source of truth
- You find it unmanageable to keep all state in the top-level component
Redux Three Principles#
Principle 1: Single Immutable State Tree#
Redux: The Single Immutable State Tree from @dan_abramov on @eggheadio
The Single Immutable State Tree, note a few keywords: Single, Immutable, State Tree.
The first principle of Redux is: The entire state of the application will be represented by a single JavaScript object, regardless of whether the application is a simple small app or a complex application with a lot of UI and state changes. This JavaScript object is called the state tree.
Here is a possible state in a Todo application:
"current_state:"
[object Object] {
todos: [[object Object] {
completed: true,
id: 0,
text: "hey",
}, [object Object] {
completed: false,
id: 1,
text: "ho",
}],
visibilityFilter: "SHOW_ACTIVE"
}
Principle 2: State Tree is Read-Only#
Redux: Describing State Changes with Actions from @dan_abramov on @eggheadio
The second principle of Redux is that the state tree is read-only. We cannot directly modify or write to it; we can only modify it by "dispatching Actions".
An Action is a plain JS object that describes a change; it is the minimal representation of the change made to the data, and its structure is entirely up to us. The only requirement is that it must have a bound property type (usually a string, as it is serializable).
Dispatch an Action#
For example, in a Todo application, the component displaying the Todos does not know how to add an item to the list; all it knows is that it needs to dispatch (dispatch) an action with a type of type: "add todo", along with the text of the todo and an index.
If toggling a task, similarly, the component does not know how it happens. All it needs to do is dispatch an action with a type, toggle the todo, and pass in the ID of the todo to be toggled.
As can be seen, the state is read-only and can only be modified through dispatch operations.
Principle 3: To Describe State Changes, You Must Write a Pure Function (Reducer)#
Redux: Pure and Impure Functions | egghead.io
Redux: The Reducer Function | egghead.io
The third principle of Redux is that to describe state changes, you must write a pure function that takes the application's previous state and the dispatched action, and then returns the application's next state. This pure function is called a Reducer.
Understanding Pure Functions vs. Impure Functions#
First, we need to understand what pure and impure functions are, as Redux sometimes requires us to write pure functions.
In a previous blog, I also mentioned: Second Training Camp - "Learning JavaScript with Moonlight" Notes, let's revisit it.
A pure function is one where the return value of a function depends only on its parameters, and it has no side effects during execution, which means it does not affect the outside world.
It can be said with certainty that calling a pure function with the same set of parameters will yield the same return value. They are predictable.
Additionally, pure functions do not modify the values passed to them. For example, a squareAll function that accepts an array does not overwrite the items within this array. Instead, it returns a new array by mapping the items.
Example of a pure function:
function square(x) {
return x*x;
}
function squareAll(items) {
return items.map(square); // Note that this generates a new array instead of directly returning items
}
Example of an impure function:
function square(x) {
updateXInDatabase(x); // Affects x in the database
return x * x;
}
function squareAll(items) {
for (let i = 0; i < items.length; i++) {
items[i] = square(items[i]); // Directly modifies items...
}
}
I learned about the concept of immutability when studying React, which is helpful in many scenarios, such as the time travel feature in the official React documentation's tic-tac-toe implementation. If every step were to change the original object, undoing actions would become very complex.
Reducer#
React pioneered the idea that when the UI layer is described as a pure function of the application's state, it is the most predictable.
Redux complements this approach with another idea: state mutations in the application must be described by pure functions that take the previous state and the action being dispatched and return the next state of the application.
Even in large applications, only one function is needed to compute the new state of the application. It does this based on the previous state of the entire application and the action being dispatched.
However, this operation does not necessarily have to be slow. If certain parts of the state do not change, their references can remain the same. This is what makes Redux fast.
Here is a complete example
Initial State#
In the initial state, todos are empty, and the filter is set to show all.
Adding Todo#
Changes as shown: Initially, the state has no todos, and the filter is set to show all. After dispatching the action, the state has one more todo, and the filter view remains unchanged.
Completing Todo#
Clicking a todo to mark it as complete shows that when dispatching this action, the text of the todos remains unchanged, while the status of complete is set to true...
Changing Filter View#
After adding another todo, clicking the Active filter shows that the only change is that the visibilityFilter state changes from "SHOW_ALL" to "SHOW_ACTIVE", while the content of todos remains unchanged (abcd is not deleted).
Writing a Counter Reducer with Tests#
Redux: Writing a Counter Reducer with Tests | egghead.io
The first function we will write is a reducer for the counter example. We will also use expect
for assertions.
eggheadio-projects/getting-started-with-redux: null - Plunker (plnkr.co)
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
expect(
counter(0, { type: 'INCREMENT' })
).toEqual(1);
expect(
counter(1, { type: 'INCREMENT' })
).toEqual(2);
expect(
counter(2, { type: 'DECREMENT' })
).toEqual(1);
expect(
counter(1, { type: 'DECREMENT' })
).toEqual(0);
expect(
counter(1, { type: 'SOMETHING_ELSE' })
).toEqual(1);
expect(
counter(undefined, {})
).toEqual(0);
As shown, the counter reducer sets up two recognizable types (INCREMENT, DECREMENT), representing counting +1 and -1. When writing the reducer, if the incoming state
is undefined, it needs to return an object representing the initial state (initstate). In this counter example, we return 0 because our count starts from 0
. If the incoming action
is not recognizable by the reducer (SOMETHING_ELSE), we only return the current state
.
Store Methods: getState(), dispatch() and subscribe()#
Redux: Store Methods: getState(), dispatch(), and subscribe() | egghead.io
This section uses built-in functions in Redux. We use ES6 destructuring syntax to introduce createStore
.
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
const { createStore } = Redux; // Redux CDN import syntax
// import { createStore } from 'redux' // npm module syntax
const store = createStore(counter);
createStore
creates a Redux store to hold the application's entire state. There should be only one store in the application.Parameters
reducer
(Function): Accepts two parameters, the current state tree and the action to be processed, and returns a new state tree.- [
preloadedState
] (any): Initial state. In isomorphic applications, you can decide whether to hydrate the state from the server or restore it from a previously saved user session. If you usecombineReducers
to create thereducer
, it must be a plain object that maintains the same structure as the keys passed in. Otherwise, you can freely pass any content that thereducer
can understand.enhancer
(Function): Store enhancer, optional. Can enhance the store with third-party capabilities such as middleware, time travel, and persistence. It is a higher-order function that composes store creators and returns a new enhanced store creator. The only built-in store enhancer in Redux isapplyMiddleware()
.Return Value
(
Store
): An object that holds all the application's state. The only way to change the state is to dispatch an action. You can also subscribe to listen for state changes and then update the UI.
The store
created by createStore
has three important methods.
getState() Get State#
getState()
retrieves the current state stored in Redux. It returns the current state tree of the application. It is the same as the last return value of the store's reducer.
dispatch() Dispatch, Initiate Action#
dispatch()
is the most commonly used method. It dispatches an action. This is the only way to trigger state changes.
It will call the store's reduce function in synchronous mode using the current result of getState()
and the incoming action
. Its return value will be the next state. From now on, this will become the return value of getState()
, and change listeners will be triggered.
subscribe() Register#
Adds a change listener. It will execute every time an action is dispatched, and a part of the state tree may have changed. You can call getState()
in the callback function to get the current state.
You can call dispatch()
inside the change listener, but you need to be aware of the following:
- Calling
dispatch()
in the listener should only happen in response to user actions or under special conditions (for example: dispatching an action when a special field exists in the store). While technically possible to calldispatch()
without any conditions, it may lead to an infinite loop as each call todispatch()
changes the store. - Subscriptions will snapshot the state before each call to
dispatch()
. When you subscribe (subscribe) or unsubscribe (unsubscribe) while calling the listener, it will not affect the currentdispatch()
. However, for the nextdispatch()
, regardless of nesting, it will use the most recent snapshot from the subscription list. - Listeners should not be aware of all state changes; often, due to nested
dispatch()
, the state may change multiple times before the listener is called. Ensure all listeners are registered beforedispatch()
starts, so that when the listener is called, it will receive the latest state at the time of the listener's existence.
This is a low-level API. In most cases, you will not use it directly, but will use some bindings from React (or other libraries). If you want the callback function to use the current state when executed, you can write a custom observeStore
utility. The Store
is also an Observable
, so you can use libraries like RxJS to subscribe
to updates.
To unsubscribe from this change listener, simply execute the function returned by subscribe
.
// ... `counter` reducer as above ...
const { createStore } = Redux;
const store = createStore(counter);
store.subscribe(() => {
document.body.innerText = store.getState();
});
document.addEventListener('click', () => {
store.dispatch({ type : 'INCREMENT' })
});
Using the above method, the initial state will not be updated because rendering occurs after the subscribe callback. After remedying:
const render = () => {
document.body.innerText = store.getState();
};
store.subscribe(render);
render(); // Call once to render the initial state 0, subsequent renders will occur after each dispatch
document.addEventListener('click', () => {
store.dispatch({ type : 'INCREMENT' })
});
The documentation excerpt from the official website may seem difficult to understand, but the following simple implementation will clarify it.
Implementing a Simple Version of createStore~!#
Redux: Implementing Store from Scratch | egghead.io
In previous studies, we explored how to use createStore()
, but to better understand it, let's write it from scratch!
First, let's review the previous case, we know:
-
The
createStore
function accepts areducer
function, which returns the current state and will be called by the internaldispatch
function. -
The
createStore
function creates a store that needs to have two variables:state
Current state, which is a JavaScript object,listeners
Array of listeners, which is an array of functions.
-
The
createStore
function needs to have these three methods:getState
,dispatch
, andsubscribe
- The
getState
method returns the current state. - The
dispatch
function is the only way to change the internal state; it takes an action and calculates the new state by passing the current state and action to thereducer
function (the input parameter of createStore). After updating, we notify each change listener (by calling them) dispatch. - The
subscribe
function takes a listener function as a parameter, adds it to the internal listener array, and to cancel the event listener,subscribe
needs to return a function. Calling this returned function will cancel the listener, and this function internally filters the listeners array to return a new listener array (removing the listener with the same reference).subscribe
- When returning the
store
, we need to fill in the initial state. We need to dispatch a fakeaction
to let thereducer
return the initial value.
- The
const createStore = (reducer) => { // Returns store, can call getState, dispatch, subscribe
let state;
let listeners = [];
const getState = () => state; // External can get current state by calling getState
const dispatch = (action) => {
state = reducer(state, action); // Because reducer is a pure function, the returned state will not modify the original state~
listeners.forEach(listener => listener()); // In the dispatch event, call the listeners after successfully calling the reducer~
}
const subscribe = (listener) => { // Add listener!
listeners.push(listener);
return () => { // To be able to cancel the listener, return a function
listeners = listeners.filter(l => l !== listener);
}
}
dispatch({}); // To let state have an initial state!
return { getState, dispatch, subscribe };
};
Improving the Counter#
Redux: Implementing Store from Scratch | egghead.io
Improved example: Counter (cos)
In the previous text, we implemented a counter reducer; here we will improve it by rendering it with React.
Writing the Counter Component#
Write a Counter component, which is a "dumb" component (dump component). It contains no business logic.
Dumb components only specify how to render the current state to output and how to bind the callback functions passed via props to event handlers.
// Counter Component
const Counter = ({
value,
onIncrement,
onDecrement,
onElse
}) => (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
<button onClick={onElse}>else</button>
</div>
);
// The render function for this component
const render = () => {
console.log("render!");
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => store.dispatch({
type:'INCREMENT'
})}
onDecrement={() => store.dispatch({
type:'DECREMENT'
})}
onElse={() => store.dispatch({
type:'else'
})}
/>, document.getElementById('root')
);
}
When this dumb component is rendered, we specify that its value should come from the current state in the Redux store. When the user presses a button, the corresponding action will be dispatched to the Redux store.
Calling createStore, Adding Listeners#
Call createStore to create a store, and call store.subscribe to add a render listener.
const counter = (state = 0, action) => {
console.log("now state:", state);
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
// store create with counter reducer
const { createStore } = Redux;
const store = createStore(counter);
store.subscribe(render); // Add listener, will re-render on each state update
render();
The reducer specifies how to calculate the next state based on the current state and the incoming action. Finally, we subscribe to the store (passing in the render function), so that the render function runs every time the state changes due to dispatch.
ps: The original text here states, "Finally, we subscribe to the Redux store so our
render()
function runs any time the state changes so ourCounter
gets the current state." It seems inaccurate according to the previous implementation of createStore; even if the state value does not change, it will still trigger render. After testing, it is indeed the case (perhaps it can be understood that Redux requires every dispatch to have the intention of changing the state; I will correct this if it is mentioned in later videos).
Viewing the Process#
Understand this process through the following example:
Try running: Counter (cos)
Initial Value#
At first, doing nothing, you can see that when createStore is called, it dispatches once to set the state to the initial value of 0 through the reducer (i.e., the counter function) and then performs a render. (Note)
Increasing Count#
Clicking + shows that render is called again.
Else#
After clicking else several times, it is found that each time it re-renders, but the value of the state seems unchanged, and the component appears to have no change.
Summary#
First of all, it is important to clarify that while Redux is a great state management tool, you should consider whether it is suitable for your scenario.
- Do not use Redux just because someone said you should; instead, take some time to understand the potential benefits and trade-offs of using it.
After watching these 1-8 episodes, I have a basic understanding of when Redux is better to use and its drawbacks, as well as the three principles of Redux and the Reducer in Redux, (getState, dispatch, subscribe) and the principles and implementation of createStore, and I have implemented a very simple counter (along with understanding what a dumb component is).
The three principles of Redux:
- The entire state of the application will be represented by a single JavaScript object, which is called the state tree.
- The state tree is read-only. It cannot be directly modified or written to; it can only be indirectly modified by "dispatching Actions".
- To describe state changes, a pure function must be written, which takes the application's previous state and the dispatched action, and then returns the application's next state. This pure function is called a Reducer.
The createStore
function accepts a reducer
function, which returns the current state and will be called by the internal dispatch
function.
-
The
createStore
function creates a store that needs to have two variables:state
Current state, which is a JavaScript object,listeners
Array of listeners, which is an array of functions.
-
The
createStore
function also needs to have these three methods:getState
,dispatch
, andsubscribe
.- The
getState
method returns the current state. - The
dispatch
function takes an action and calculates the new state by passing the current state and action to thereducer
function (the input parameter of createStore). After updating, we notify each change listener (by calling them) dispatch. - The
subscribe
function takes a listener function as a parameter, adds it to the internal listener array, and to cancel the event listener,subscribe
needs to return a function. Calling this returned function will cancel the listener, and this function internally filters the listeners array to return a new listener array (removing the listener with the same reference).subscribe
- When returning the
store
, we need to fill in the initial state. We need to dispatch a fakeaction
to let thereducer
return the initial value.
- The