banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

The Journey of Learning Redux (1) The Three Principles of Redux, the Principle and Implementation of createStore

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.

Please add image description

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.

Please add image description

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...
Please add image description

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).

Please add image description

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

  1. reducer (Function): Accepts two parameters, the current state tree and the action to be processed, and returns a new state tree.
  2. [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 use combineReducers to create the reducer, it must be a plain object that maintains the same structure as the keys passed in. Otherwise, you can freely pass any content that the reducer can understand.
  3. 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 is applyMiddleware().

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:

  1. 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 call dispatch() without any conditions, it may lead to an infinite loop as each call to dispatch() changes the store.
  2. 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 current dispatch(). However, for the next dispatch(), regardless of nesting, it will use the most recent snapshot from the subscription list.
  3. 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 before dispatch() 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 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 needs to have these three methods: getState, dispatch, and subscribe

    • 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 the reducer 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 fake action to let the reducer return the initial value.
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 our Counter 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).

Redux: React Counter example |egghead.io

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)

Please add image description

Increasing Count#

Clicking + shows that render is called again.

Please add image description

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.
Please add image description

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, and subscribe.

    • 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 the reducer 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 fake action to let the reducer return the initial value.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.