gnosis23 / hello-world-blog

还是 issues 里面写文章方便
https://bohao.work
0 stars 0 forks source link

组件级 React 状态管理 #86

Open gnosis23 opened 2 years ago

gnosis23 commented 2 years ago

对于组件级别的状态共享,通过 ProvideruseContext 就能实现状态共享,但是还是有些问题

Provider 中的 value 不能每次都改变,不然会触发额外的渲染

const [value, setValue] = useState(1);

// ❌ 错误
return (
  <Provider value={{ count: value }}>
    <App />
  </Provider>
)

好一点的做法是用 useMemo 等包裹一下

const [value, setValue] = useState(1);
const globalValue = useMemo(() => { return { count: value } }, [value]);

// ✅ 正确
return (
  <Provider value={global}>
    <App />
  </Provider>
)

光有属性还不行,还要把 setter 也放进去

const [value, setValue] = useState(1);
const globalValue = useMemo(() => { 
  return { count: value, setCount: setValue } 
}, [value]);

// ✅ 正确
return (
  <Provider value={global}>
    <App />
  </Provider>
)

那么当状态多了以后,放很多 get 和 set 就很麻烦。于是就想到了 useReducer ,这样你就不得不写一大堆代码

对于一个组件来说,是不是杀鸡用牛刀了...

所以需要一个轻量级的组件共享方案

gnosis23 commented 2 years ago

Jotai

Jotai 是个受 recoil 启发而开发的状态管理库,它能够用较少的代码做到组件中状态共享。

用法上和普通的 useState 很像,比如

import React from 'react';
import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

export default function(props) {
  const [count, setCount] = useAtom(countAtom);

  return (
    <section>
      <h1>
        {count}
        <button onClick={() => setCount(c => c + 1)}>one up</button>
      </h1>
    </section>
  )
}

注意到上面的 useAtom 函数里需要用到一个 atom 函数。什么是 atom 函数 ? 官方是这么解释的

An atom represents a piece of state.

也不用太过纠结,就当是个 model 定义方法。通过这种方式,在任意组件内部都能访问到这个变量,很方便 atoms

Atom

定义一个 computed atom 。

const doubledCountAtom = atom((get) => get(countAtom) * 2)

const count1 = atom(1)
const count2 = atom(2)
const count3 = atom(3)
const sum = atom((get) => get(count1) + get(count2) + get(count3))

定义一个写方法的 atom

const decrementCountAtom = atom(
  (get) => get(countAtom),
  (get, set, _arg) => set(countAtom, get(countAtom) - 1),
)

function Counter() {
  const [count, decrement] = useAtom(decrementCountAtom)
  return (
      <button onClick={decrement}>Decrease</button>
  )
}

配合 Immer

假设组件里的状态多了,一个个定义 atom 会很麻烦,这时候可以考虑使用 object 来当状态。

更新的时候可以考虑搭配 immer 来使用,Jotai 默认就支持 immer 。

import { useAtom } from 'jotai'
import { atomWithImmer } from 'jotai/immer'

const countAtom = atomWithImmer(0)

const Controls = () => {
  const [count, setCount] = useAtom(countAtom)
  // setCount === update : (draft: Draft<Value>) => void
  const inc = () => setCount((c) => (c = c + 1))
  return ( 
    <div>
      {count} <button onClick={inc}>+1</button>
    </div>
  )
}
gnosis23 commented 2 years ago

Jotai原理分析

发布订阅模式

当使用 useAtom 的时候,底层会订阅相关 atom 的事件,一旦 atom 更新就强制刷新。 这里使用了 useReducer 以及它的第二个参数 dispatch(好处是为了免费记以前的值?)

// useAtom.ts
const [[value, atomFromUseReducer], forceUpdate] = useReducer(
  useCallback(
    (prev) => {
      const nextValue = getAtomValue()
      if (Object.is(prev[0], nextValue) && prev[1] === atom) {
        return prev // bail out
      }
      return [nextValue, atom]
    },
    [getAtomValue, atom]
  ),
  undefined,
  () => {
    const initialValue = getAtomValue()
    return [initialValue, atom]
  }
)

if (atomFromUseReducer !== atom) {
  forceUpdate()
}

useEffect(() => {
  const unsubscribe = store[SUBSCRIBE_ATOM](atom, forceUpdate)
  forceUpdate()
  return unsubscribe
}, [store, atom])
gnosis23 commented 2 years ago

Zustand

使用订阅模式和selector来实现状态管理,能按需更新组件,不会重复渲染。

下面是一个 TodoList 的例子:

import create from 'zustand';
import {memo, useState} from "react";

let nextId = 0;

// 先定义一个 store 的 hook
const useStore = create((set) => ({
  todos: [],
  addTodo: (title) => {
    set(prev => ({
      todos: [
        ...prev.todos,
        { id: ++nextId, title, done: false },
      ]
    }))
  },
  removeTodo: (id) => {
    set(prev => ({
      todos: prev.todos.filter(todo => todo.id !== id)
    }))
  },
  toggleTodo: id => {
    set(prev => ({
      todos: prev.todos.map(todo => todo.id === id ? {...todo, done: !todo.done} : todo)
    }))
  }
}));

// 定义好selector
const selectTodos = state => state.todos;
const selectAddTodo = state => state.addTodo;
const selectRemoveTodo = state => state.removeTodo;
const selectToggleTodo = state => state.toggleTodo;

const TodoItem = ({ todo }) => {
  // 订阅更新,理论上方法是不会变的
  const removeTodo = useStore(selectRemoveTodo);
  const toggleTodo = useStore(selectToggleTodo);
  return (
    <div>
      <input type="checkbox" checked={todo.done} onChange={() => toggleTodo(todo.id)} />
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.title}</span>
      <button onClick={() => removeTodo(todo.id)}>Delete</button>
    </div>
  )
};

const MemoedTodoItem = memo(TodoItem);

const TodoList = () => {
  // 当 todos 更新了才会渲染
  const todos = useStore(selectTodos);
  return (
    <div>
      {todos.map(todo => (
        <MemoedTodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  )
};

const NewTodo = () => {
  const addTodo = useStore(selectAddTodo);
  const [text, setText] = useState('');
  const onClick = () => {
    addTodo(text);
    setText('');
  };
  return (
    <div>
      <input type="text" value={text} onChange={e => setText(e.target.value)} />
      <button onClick={onClick} disabled={!text}>Add</button>
    </div>
  )
};

export default function () {
  return (
    <>
      <TodoList />
      <NewTodo />
    </>
  )
}