Understanding of React state management and comparison of solutions

1. React status and communication
Why React needs state management
React features:
  1. Focus on the view layer: The characteristics of focusing on the view layer determine that it is not an all-in-one framework. Compared with all-in-one frameworks such as angular, React has simpler and single functions.
  2. UI=render(data)UI=render(data), data is what we call data flow, and render is a pure function provided by react, so the display of the user interface completely depends on the data layer.
  3. State flows from top to bottom, Props are read-only. React renders the page based on state (or props). The state flows from the outside to the inside of the component, and from top to bottom, and the passed props are read-only.
In-component state management
React state

Manage the state through state and update the state using setState to update the UI

export default class Counter extends React.Component {
    
    
  constructor(props) {
    
    
    super(props);
    this.state = {
    
    
      value: 0,
      otherValue: 0
    };
  }

  handleAdd() {
    
    
    this.setState((state, props) => ({
    
    
    ...state,
      value: ++state.value
    }));
  }

  render() {
    
    
    return (
      <div>
        <p>{
    
    this.state.value}</p>
        <button onClick={
    
    () => this.handleAdd}>Add</button>
      </div>
    );
  }
}

Disadvantages of react's own state management solution: even if you only update a certain value in the state, you need to bring other values ​​(otherValue), which is cumbersome to operate.

React Hooks

Hooks have been officially added in React 16.8. Manage the state within components through hooks, which is easy to use and scalable.

motivation:

  • Reusing state logic between components is difficult
  • Complex components become difficult to understand
  • Incomprehensible class
  • The design purpose of React Hooks is to enhance the function component. It can write a fully functional component without using "class" to define the component at all.
import React, {
    
     useState } from "react";

function Counter() {
    
    
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>{
    
    count}</h1>
      <button onClick={
    
    () => setCount(count + 1)}>Add</button>
    </div>
  );
}

export default Counter;

Compared with the Class component, which uses react's own state management method, React Hooks uses the useState method to create state variables, update state methods, and assign initial states in the Function component. This implements a Function component with its own state.

React component communication

There are three data flow management solutions for react itself:

  • Parent-child component communication
  • Sibling component communication
  • Cross-component communication
Parent-child components

Pass from parent to child: Add attribute data to the child component through props, so that the parent component can communicate with the child component.

Insert image description here

Implementation steps

  1. The parent component provides the data to be passed -state

  2. Give the subcomponent label 添加属性value as the data in the state

  3. The child component propsreceives the data passed from the parent component through

    1. Class components use this.props to obtain the props object
    2. Functional components obtain props objects directly through parameters
import React from 'react'

// 函数式子组件
function FSon(props) {
    
    
  console.log(props)
  return (
    <div>
      子组件1
      {
    
    props.msg}
    </div>
  )
}

// 类子组件
class CSon extends React.Component {
    
    
  render() {
    
    
    return (
      <div>
        子组件2
        {
    
    this.props.msg}
      </div>
    )
  }
}
// 父组件
class App extends React.Component {
    
    
  state = {
    
    
    message: 'this is message'
  }
  render() {
    
    
    return (
      <div>
        <div>父组件</div>
        <FSon msg={
    
    this.state.message} />
        <CSon msg={
    
    this.state.message} />
      </div>
    )
  }
}

export default App

props description:

  1. props is a read-only object (readonly). According to the requirements of single data flow, sub-components can only read the data in props and cannot modify it.
  2. props can pass any data numbers, strings, Boolean values, arrays, objects, functions, JSX

Pass from child to parent: The parent component passes the callback function to the child component, and the child component calls

Implementation steps

  1. The parent component provides a callback function - used to receive data
  2. Pass the function as the value of the attribute to the child component
  3. Child components call callback functions through props
  4. Pass the data from the child component as a parameter to the callback function

Insert image description here

import React from 'react'

// 子组件
function Son(props) {
    
    
  function handleClick() {
    
    
    // 调用父组件传递过来的回调函数 并注入参数
    props.changeMsg('this is newMessage')
  }
  return (
    <div>
      {
    
    props.msg}
      <button onClick={
    
    handleClick}>change</button>
    </div>
  )
}


class App extends React.Component {
    
    
  state = {
    
    
    message: 'this is message'
  }
  // 提供回调函数
  changeMessage = (newMsg) => {
    
    
    console.log('子组件传过来的数据:',newMsg)
    this.setState({
    
    
      message: newMsg
    })
  }
  render() {
    
    
    return (
      <div>
        <div>父组件</div>
        <Son
          msg={
    
    this.state.message}
          // 传递给子组件
          changeMsg={
    
    this.changeMessage}
        />
      </div>
    )
  }
}

export default App
Sibling component communication

Brother component: Through the state promotion mechanism, the common parent component is used to achieve sibling communication.

Insert image description here

status improvement

State promotion: that is, the state that needs to be communicated is promoted to the common parent component of both to achieve sharing and reaction.

Insert image description here

Implementation content: A1 and B1 under components A and B need to implement state communication

Insert image description here

State improvement process: Add a Container component on top of A and B, and improve the state that A1 and B1 need to share, define it to the Container, and pass the state and changeState methods through props.

One problem with this method is that if there is a state of component C in the future that needs to communicate with A, another Container component will be added. If the state of A needs to be shared with C, it will be even more devastating. Previously, it was upgraded to The state of Container needs to be improved to another level. For this endless state promotion problem, the communication cost in the later stage is very high, almost rewriting.

Implementation steps

  1. Promote shared state to the nearest public parent component, which manages this state
    • Provide shared status
    • Provides methods for manipulating shared state
  1. Subcomponents that want to receive data status receive data through props
  2. Subcomponents that want to transfer data status receive methods through props and call methods to transfer data.
import React from 'react'

// 子组件A
function SonA(props) {
    
    
  return (
    <div>
      SonA
      {
    
    props.msg}
    </div>
  )
}
// 子组件B
function SonB(props) {
    
    
  return (
    <div>
      SonB
      <button onClick={
    
    () => props.changeMsg('new message')}>changeMsg</button>
    </div>
  )
}

// 父组件
class App extends React.Component {
    
    
  // 父组件提供状态数据
  state = {
    
    
    message: 'this is message'
  }
  // 父组件提供修改数据的方法
  changeMsg = (newMsg) => {
    
    
    this.setState({
    
    
      message: newMsg
    })
  }

  render() {
    
    
    return (
      <>
        {
    
    /* 接收数据的组件 */}
        <SonA msg={
    
    this.state.message} />
        {
    
    /* 修改数据的组件 */}
        <SonB changeMsg={
    
    this.changeMsg} />
      </>
    )
  }
}

export default App
Cross-component Context:

Cross-component Context: Context provides a method to transfer data between component trees without manually adding props to each layer of components.

Implementation steps

1- Create a Context object to export Provider and Consumer objects

const {
    
     Provider, Consumer } = createContext()

2- Use Provider to wrap the upper component to provide data

<Provider value={
    
    this.state.message}>
    {
    
    /* 根组件 */}
</Provider>

3- Components that need to use data use the Consumer package to obtain the data.

<Consumer >
    {
    
    value => /* 基于 context 值进行渲染*/}
</Consumer>

Complete code

import React, {
    
     createContext }  from 'react'

// 1. 创建Context对象 
const {
    
     Provider, Consumer } = createContext()


// 3. 消费数据
function ComC() {
    
    
  return (
    <Consumer >
      {
    
    value => <div>{
    
    value}</div>}
    </Consumer>
  )
}

function ComA() {
    
    
  return (
    <ComC/>
  )
}

// 2. 提供数据
class App extends React.Component {
    
    
  state = {
    
    
    message: 'this is message'
  }
  render() {
    
    
    return (
      <Provider value={
    
    this.state.message}>
        <div className="app">
          <ComA />
        </div>
      </Provider>
    )
  }
}

export default App

question:

  • Context is only implemented by promoting the state to a shared parent component. It seems to be cross-component, but in fact it is still passed level by level to achieve inter-component communication, state synchronization and state sharing.
  • Context also hands over the state control of the bottom sub-component to the top-level component, but when the top-level component status is updated, it will definitely trigger the re-render of all sub-components, which will also cause losses.
Problems caused by cross-component communication:
  • Bloated components: When the business logic of a component is very complex, we will find that the more code we write, the more code we write, because we can only control the data flow inside the component and cannot separate it. Model and View are both placed in the View layer, and the entire component It seems bloated and difficult to maintain.
  • The state is unpredictable and even cannot be traced back: When the data flow is chaotic, an execution action may trigger a series of setState, and the entire data flow becomes unmonitorable.
  • Handling asynchronous data flow: React itself does not provide a variety of solutions for asynchronous data flow management. It is difficult to meet some complex asynchronous flow scenarios with only one setState.
  • The status is constantly improving and the context is not easy to use.
State management definition:

To put it simply, "state management" is to solve the "cross-level" communication between components.

  • A component needs to share state with another component
  • One component needs to change the state of another component
2、Flux —> Redux —> Mobx
The birth of the Flux architectural pattern

Flux is a set of architectural patterns, not a code framework.

[One-way data flow]: Action -> Dispatcher -> Store -> View page interactive data flow, such as the user clicks the button: View -> Create Action -> Dispatcher (enter [One-way data flow] from here)

Four core parts

  • Dispatcher: Responsible for two things, one is to distribute and process Action, and the other is to maintain the Store.
  • Store: data and logic part.
  • views: Page-React component, obtains status (data) from Store, and binds event processing.
  • actions: The interaction is encapsulated as action and submitted to the Store for processing.

[External link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly (img-4Lni0WBa-1687931092907) (C:\Users\changwenqiao\AppData\Roaming\Typora\typora-user-images\ image-20230610100446215.png)]

Redux

Redux is an evolution of Flux. It is a code framework generated under the guidance of the Flux architectural pattern and further carries out architectural constraint design.

Why use Redux?
  1. Independent of components, ignores hierarchical relationships between components, and simplifies communication issues
  2. Single data flow is clear, making it easy to locate bugs
  3. The debugging tools are well equipped and convenient for debugging.
Three principles:

1. Uniqueness of data source: All data in redux is stored in the store, which is a single immutable state tree. The purpose of this is to ensure the uniqueness of the data source. A single state tree also makes it easier to debug or inspect the application.

2. State can only be read-only: state can only be read-only. State can only be changed by triggering an Action. You can get the value in the action, but you cannot change it. The method taken at this time is usually Deep copy the state, return it to a variable, then change the variable, and finally return the value. And the data can only be changed in the reducer. The reducer is a function process that describes what process has happened to the object. The benefit of read-only state is that it ensures that neither views nor network callbacks write directly to the state.

3. Use pure functions to make changes: The essence of reducer is actually a pure function. Each change always returns a new State

Redux data flow architecture

Insert image description here

  • Store: Provides a global store variable to store the public states that we want to extract from the component;
  • action: Provides a common object to describe how you want to change the data, and this is the only way;
  • reducer: provides a pure function to update the state according to the description of the action
Pure Redux implementation of counter

core steps

  1. Create a reducer function and define the relationship between action and state internally.
  2. Call the createStore method of Redux and pass in the defined reducer function to generate a store instance.
  3. Monitor whether the data changes through the subscribe method on the store instance
  4. Click the button to submit the action object through the special dispatch function to implement data update.
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>

<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>

<script>
  // 定义reducer函数 
  // 内部主要的工作是根据不同的action 返回不同的state
  function counterReducer (state = {
     
      count: 0 }, action) {
    
    
    switch (action.type) {
    
    
      case 'INCREMENT':
        return {
    
     count: state.count + 1 }
      case 'DECREMENT':
        return {
    
     count: state.count - 1 }
      default:
        return state
    }
  }
  // 使用reducer函数生成store实例
  const store = Redux.createStore(counterReducer)
  
  // 订阅数据变化
  store.subscribe(() => {
    
    
    console.log(store.getState())
    document.getElementById('count').innerText = store.getState().count
    
  })
  // 增
  const inBtn = document.getElementById('increment')
  inBtn.addEventListener('click', () => {
    
    
    store.dispatch({
    
    
      type: 'INCREMENT'
    })
  })
  // 减
  const dBtn = document.getElementById('decrement')
  dBtn.addEventListener('click', () => {
    
    
    store.dispatch({
    
    
      type: 'DECREMENT'
    })
  })
</script>
Redux asynchronous processing
import {
    
     createSlice } from '@reduxjs/toolkit'
import axios from 'axios'

const channelStore = createSlice({
    
    
  name: 'channel',
  initialState: {
    
    
    channelList: []
  },
  reducers: {
    
    
    setChannelList (state, action) {
    
    
      state.channelList = action.payload
    }
  }
})


// 创建异步
const {
    
     setChannelList } = channelStore.actions
const url = 'http://geek.itheima.net/v1_0/channels'
// 封装一个函数 在函数中return一个新函数 在新函数中封装异步
// 得到数据之后通过dispatch函数 触发修改
const fetchChannelList = () => {
    
    
  return async (dispatch) => {
    
    
    const res = await axios.get(url)
    dispatch(setChannelList(res.data.data.channels))
  }
}

export {
    
     fetchChannelList }

const channelReducer = channelStore.reducer
export default channelReducer
import {
    
     useEffect } from 'react'
import {
    
     useSelector, useDispatch } from 'react-redux'
import {
    
     fetchChannelList } from './store/channelStore'

function App () {
    
    
  // 使用数据
  const {
    
     channelList } = useSelector(state => state.channel)
  useEffect(() => {
    
    
    dispatch(fetchChannelList())
  }, [dispatch])

  return (
    <div className="App">
      <ul>
        {
    
    channelList.map(task => <li key={
    
    task.id}>{
    
    task.name}</li>)}
      </ul>
    </div>
  )
}

export default App
When should you use Redux?

When complex application states need to be processed and React itself cannot satisfy them . For example:

  • Need to persist application state so that the application can be restored from local storage or data returned by the server
  • Need to implement undo and redo functions
  • Enable cross-page user collaboration
  • When the application state is complex
  • When the data flow is more complex
  • Many unrelated components need to share and update state
  • external status
Disadvantages of redux:
  • Heavy code templates: Modifying a state may require moving four or five files;
  • State residue in the store: When multiple components share a certain state in the store, attention should be paid to initialization and clearing issues;
  • Brainless publish and subscribe: every time an action is dispatched, all reducers will be traversed and the connections will be recalculated, which is undoubtedly a loss;
  • There will be lags when interactions are frequent: If the store is large and the store is modified frequently, you will obviously see page lags;
  • Typescript is not supported;
Mobx

A centralized state management tool that can work well with React. It solves similar problems as Redux and can perform centralized state management on independent components.

Mobx provides a responsive system similar to Vue. Compared with Redux, Mobx's architecture is easier to understand.

Mobx data flow

Insert image description here

Reactive data . First use @observableto convert the data into 'reactive data', similar to Vue's data. Dependencies can be collected when these data are accessed in some context (such as computed, observer's wrapped React component, reaction), and related dependencies will be notified when these data changes. Two advantages brought by reactive data are ① Simplified data operation methods (compared to redux and setState); ② Precise data binding, the view only needs to be rendered when the data actually changes. The smaller the granularity of component dependencies, the more finely updated the view can be.

core:

Actions change state, and state changes update all affected views

Basic usage

Initialize mobx

Initialization steps

  1. Define data state state
  2. Implement data responsive processing in the constructor makeAutoObservble
  3. Define function action to modify data
  4. Instantiate the store and export it
import {
    
     makeAutoObservable } from 'mobx'

class CounterStore {
    
    
  count = 0 // 定义数据
  constructor() {
    
    
    makeAutoObservable(this)  // 响应式处理
  }
  // 定义修改数据的方法
  addCount = () => {
    
    
    this.count++
  }
}

const counter = new CounterStore()
export default counter

React uses store

Implementation steps

  1. Import the counterStore instance object in the component
  2. Use the data in the storeStore instance object in the component
  3. Modify the data in the store by calling methods that modify data through events
  4. Let components respond to data changes
// 导入counterStore
import counterStore from './store'
// 导入observer方法
import {
    
     observer } from 'mobx-react-lite'
function App() {
    
    
  return (
    <div className="App">
      <button onClick={
    
    () => counterStore.addCount()}>
        {
    
    counterStore.count}
      </button>
    </div>
  )
}
// 包裹组件让视图响应数据变化
export default observer(App)

Computed properties (derived state)

Some states are calculated (derived) from existing states

Insert image description here

Implementation steps

  1. Life is an existing data
  2. Define computed properties through the get keyword
  3. Mark computed properties in makeAutoObservable method
import {
    
     computed, makeAutoObservable } from 'mobx'

class CounterStore {
    
    
  list = [1, 2, 3, 4, 5, 6]
  constructor() {
    
    
    makeAutoObservable(this, {
    
    
      filterList: computed
    })
  }
  // 修改原数组
  changeList = () => {
    
    
    this.list.push(7, 8, 9)
  }
  // 定义计算属性
  get filterList () {
    
    
    return this.list.filter(item => item > 4)
  }
}

const counter = new CounterStore()

export default counter
// 导入counterStore
import counterStore from './store'
// 导入observer方法
import {
    
     observer } from 'mobx-react-lite'
function App() {
    
    
  return (
    <div className="App">
      {
    
    /* 原数组 */}
      {
    
    JSON.stringify(counterStore.list)}
      {
    
    /* 计算属性 */}
      {
    
    JSON.stringify(counterStore.filterList)}
      <button onClick={
    
    () => counterStore.changeList()}>change list</button>
    </div>
  )
}
// 包裹组件让视图响应数据变化
export default observer(App)

Asynchronous data processing

Implementation steps:

  1. Write an asynchronous request method in mobx to obtain data and store it in state
  2. The execution of the action function is triggered in the component through useEffect + empty dependency
// 异步的获取

import {
    
     makeAutoObservable } from 'mobx'
import axios from 'axios'

class ChannelStore {
    
    
  channelList = []
  constructor() {
    
    
    makeAutoObservable(this)
  }
  // 只要调用这个方法 就可以从后端拿到数据并且存入channelList
  setChannelList = async () => {
    
    
    const res = await axios.get('http://geek.itheima.net/v1_0/channels')
    this.channelList = res.data.data.channels
  }
}
const channlStore = new ChannelStore()
export default channlStore
import {
    
     useEffect } from 'react'
import {
    
     useStore } from './store'
import {
    
     observer } from 'mobx-react-lite'
function App() {
    
    
  const {
    
     channlStore } = useStore()
  // 1. 使用数据渲染组件
  // 2. 触发action函数发送异步请求
  useEffect(() => {
    
    
    channlStore.setChannelList()
  }, [])
  return (
    <ul>
      {
    
    channlStore.channelList.map((item) => (
        <li key={
    
    item.id}>{
    
    item.name}</li>
      ))}
    </ul>
  )
}
// 让组件可以响应数据的变化[也就是数据一变组件重新渲染]
export default observer(App)

Modular

Insert image description here

Implementation steps

  1. Split the module js file and define its own independent state/action in each module
  2. Import the split modules into store/index.js and combine the modules
  3. Use React's context mechanism to export a unified useStore method for use by business components

1- Define task module

import {
    
     makeAutoObservable } from 'mobx'

class TaskStore {
    
    
  taskList = []
  constructor() {
    
    
    makeAutoObservable(this)
  }
  addTask () {
    
    
    this.taskList.push('vue', 'react')
  }
}

const task = new TaskStore()


export default task

2- Define counterStore

import {
    
     makeAutoObservable } from 'mobx'

class CounterStore {
    
    
  count = 0
  list = [1, 2, 3, 4, 5, 6]
  constructor() {
    
    
    makeAutoObservable(this)
  }
  addCount = () => {
    
    
    this.count++
  }
  changeList = () => {
    
    
    this.list.push(7, 8, 9)
  }
  get filterList () {
    
    
    return this.list.filter(item => item > 4)
  }
}

const counter = new CounterStore()

export default counter

3- Combined modules export unified methods

import React from 'react'

import counter from './counterStore'
import task from './taskStore'


class RootStore {
    
    
  constructor() {
    
    
    this.counterStore = counter
    this.taskStore = task
  }
}


const rootStore = new RootStore()

// context机制的数据查找链  Provider如果找不到 就找createContext方法执行时传入的参数
const context = React.createContext(rootStore)

const useStore = () => React.useContext(context)
// useStore() =>  rootStore  { counterStore, taskStore }

export {
    
     useStore }

4- Component uses data from module

import {
    
     observer } from 'mobx-react-lite'
// 导入方法
import {
    
     useStore } from './store'
function App() {
    
    
  // 得到store
  const store = useStore()
  return (
    <div className="App">
      <button onClick={
    
    () => store.counterStore.addCount()}>
        {
    
    store.counterStore.count}
      </button>
    </div>
  )
}
// 包裹组件让视图响应数据变化
export default observer(App)
Defects of mobx
  • No state backtracking capability: mobx directly modifies the object reference, so it is difficult to do state backtracking; (the advantage of redux is instantly reflected)
  • No middleware: Like redux, mobx does not have a good way to handle asynchronous data flow, and there is no way to control the data flow more finely.
  • Too many stores: As the number of stores increases, maintenance costs will also increase, and data sharing and mutual references between multiple stores will also be error-prone.
  • Side effects: mobx directly modifies data, which is contrary to the pure functions emphasized by the functional programming model. This also leads to many unknowns in the data.
Comparison between Mobx and Redux

redux

  • Redux stores data in a single store
  • Redux uses plain object to save data, and you need to manually handle the changed operations.
  • Redux uses immutable state, which means that the state is only read-only and cannot be modified directly. Instead, a new state should be returned and pure functions should be used.
  • Redux will be more complicated, because the functional programming ideas are not so easy to master, and a series of middleware is needed to handle asynchronous and side effects.
  • Data flow: dispatch(action) -> call the reducer in the store, compare the current state with the received action, calculate the new state -> the state changes, the store monitors the change -> start re-rendering

mobx

  • mobx stores data in multiple independent stores, divided by module application
  • mobx uses observable to save data and automatically handles response operations after data changes.
  • The state in mobx is mutable and can be modified directly
  • mobx is relatively simple and has a lot of abstractions. mobx uses more object-oriented thinking.
  • Data flow: action -> modify state -> trigger changes

Summarize

  • If it is a small project and there is not much state that needs to be shared, then there is no need for state management. The props or context of react itself can meet the requirements.
  • If you need to manually control state updates, one-way data flow is a suitable choice, for example: redux
  • If you need simple automatic updates, two-way binding state management is the best choice, for example: mobx
  • In small projects or projects with a small number of developers, MobX can be used, which will be more efficient.
  • For large projects or projects with multiple people assisting, consider using Redux, which will lower subsequent maintenance costs.
  • If it is simple data sharing between two or more components, then atomization may be a suitable choice:, for example: jotai, recoil
  • If the state has complex data flow processing, please use rxjs
  • If you are managing complex business states, you can use a finite state machine to manage state transitions, for example: xstate
  • If there is a need to subscribe and operate status in a non-react context, tools such as jotai and recoil are not good choices.

Guess you like

Origin blog.csdn.net/qq_43641110/article/details/131435161