gnosis23 / hello-world-blog

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

《 Micro State Management With React Hooks 》笔记 #106

Open gnosis23 opened 2 years ago

gnosis23 commented 2 years ago

前三章

上推组件(lift up)避免重复渲染

设计模式:把组件往上层推,来避免因组件渲染而导致所有组件渲染。

例一

下面这个例子,虽然 Parent 里面的 count 增加了,但是 inner 是外面传递进来的,不会重复渲染 (这个例子用了 inner 属性,其实 children 也可以)

const SubComp = () => {...};

const Parent = ({ inner }) => {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(c => c + 1)} >+1</button>
      {inner}
    </>
  )
};

function App() {
  <Parent inner={<SubComp />} />
}

例二

下面例子里有个多 Provider ,也用了这个技巧。上层 Provider 渲染不会导致 下层 Provider 渲染

const Count1Provider = ({ children }) => {
  return (
    <Count1Context.Provider value={useState(0)}>
      {children}
    </Count1Context.Provider>
  );
};

const Count2Provider = ({ children }) => {
  return (
    <Count2Context.Provider value={useState(0)}>
      {children}
    </Count2Context.Provider>
  );
};

const App = () => (
  <Count1Provider>
    <Count2Provider>
      <Parent />
    </Count2Provider>
  </Count1Provider>
);

Context 如何传播

Context for Objects

当使用 object 来作为 context 的值时,会碰到不必要渲染的问题,比如:当点击任意按钮的时候,明明组件 ComponentCount1 只依赖的 count1,但是点击按钮2 的时候它也会重新渲染。

const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <CountContext.Provider value={{ count1, count2 }}>
      <button onClick={() => setCount1(c => c + 1)}>
        {count1}
      </button>
      <button onClick={() => setCount2(c => c + 2)}>
        {count2}
      </button>
      <ComponentCount1 />
      <ComponentCount2 />
    </CountContext.Provider>
  );
};

React.memo

使用 memo 来减少父组件渲染导致的重复渲染

function DummyComponent() {
  return <SomeDiv />
}
const memoedDummyComponent = React.memo(DummyComponent);

将 Context 值拆成多个原始值

当 Context 的 value 为对象,那么每次对象更新,所有 Consumer 都会更新,但并不是所有子组件都需要所有状态值的。 这会造成额外的渲染,可以看这个例子

所以作者推荐将状态拆成几个独立的 Context(如果用 Dispatch 也可以独立做成一个)

const Parent = () => (
  <>
    <MemoedCounter1 />
    <MemoedCounter2 />
  </>
);

const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <CountContext1.Provider value={count1}>
      <CountContext2.Provider value={count2}>
        <button onClick={() => setCount1((c) => c + 1)}>{count1}</button>
        <button onClick={() => setCount2((c) => c + 1)}>{count2}</button>
        <Parent />
      </CountContext2.Provider>
    </CountContext1.Provider>
  );
};

使用工厂模式创建Context

作者提炼了一个创建Context的工厂方法,使用它可以方便的创建。 这个方法还支持自定义状态的hook,比如可以用 useState 或者 useReducer。还支持自定义初始值

const createStateContext = (useValue) => {
    const StateContext = createContext(null);
    const StateProvider = ({ initialValue, children }) => (
        <StateContext.Provider value={useValue(initialValue)}>
            {children}
        </StateContext.Provider>
    );
    const useContextState = () => {
        const value = useContext(StateContext);
        if (value === null) throw new Error("Provider missing");
        return value;
    };
    return [StateProvider, useContextState];
};

const useNumberState = (init) => useState(init || 0);

const [Count1Provider, useCount1] = createStateContext(useNumberState);

const Counter1 = () => {
    const [count1, setCount1] = useCount1();
    return (
        <div>
            Count1: {count1}
            <button onClick={() => setCount1(c => c + 1)}>+1</button>
        </div>
    )
};

Context不是设计用来管理状态的

Context is not fully designed for global states. One of the known limitations is that Context consumers re-render upon updates, which can lead to extra re-renders.

gnosis23 commented 2 years ago

第四章

介绍了:

const createStore = (initialState) => {
  let state = initialState;
  const callbacks = new Set();
  const getState = () => state;
  const setState = (nextState) => {
    state =
      typeof nextState === 'function'
        ? nextState(state)
        : nextState;
    callbacks.forEach((callback) => callback());
  };
  const subscribe = (callback) => {
    callbacks.add(callback);
    return () => {
      callbacks.delete(callback);
    };
  }
  return { getState, setState, subscribe }
};

const store = createStore({ count1: 0, count2: 0 });

const Component1 = () => {
  const state = useSubscription(useMemo(() => ({
    getCurrentValue: () => store.getState().count1,
    subscribe: store.subscribe,
  }), []));
  const inc = () => {
    store.setState(prev => ({ ...prev, count1: prev.count1 + 1 }))
  };
  return (
    <div>
      {state} <button onClick={inc}>+1</button>
    </div>
  )
};
gnosis23 commented 2 years ago

第五章

第四章的订阅模式有个问题:它的状态是唯一的,如果自组件要自己的状态,就会有问题。 这章介绍了利用 Context 来创建独立的空间

const StoreContext = createContext(createStore({ count: 0, text: 'hello' }));

const StoreProvider = ({ initialState, children }) => {
  const storeRef = useRef();
  if (!storeRef.current) {
    storeRef.current = createStore(initialState);
  }
  return (
    <StoreContext.Provider value={storeRef.current}>
      {children}
    </StoreContext.Provider>
  );
};

const useSelector = (selector) => {
  const store = useContext(StoreContext);
  return useSubscription(
    useMemo(
      () => ({
        getCurrentValue: () => selector(store.getState()),
        subscribe: store.subscribe
      }),
      [store, selector]
    )
  )
};

const useSetState = () => {
  const store = useContext(StoreContext);
  return store.setState;
};

const selectCount = (state) => state.count;

const Component = () => {
  const count = useSelector(selectCount);
  const setState = useSetState();
  const inc = () => {
    setState(prev => ({ ...prev, count: prev.count + 1 }))
  };
  return (
    <div>
      {count} <button onClick={inc}>+1</button>
    </div>
  )
};
gnosis23 commented 2 years ago

第六章

介绍了减少重复渲染有那几种方式:

使用 Selector

手动指定跟踪哪个属性变化

const Component = () => {
  const value = useSelector(state => state.a.b);
  return <>{value}</>
}

属性访问检查

自动检测需要监控的状态

const Component = () => {
  const trackedState = useTrackedState();
  return (
    <>
      <p>{trackedState.b.c}</p>
    </>
  );
};

atoms

没看懂

const globalState = {
  a: atom(1),
  b: atom(2),
}
const Component = () => {
  const value = useAtom(globalState.a);
  return <>{value}</>
};
gnosis23 commented 2 years ago

第八章: Jotai

selector 从大的状态中选取部分状态,atom更像是自底向上构建,通过小的 atom 组成大的 atom。 里面的状态总是需要的

比如:

const firstNameAtom = atom('React');
const lastNameAtom = atom('Hooks');
const ageAtom = atom(3);

const personAtom = atom((get) => ({
  firstName: get(firstNameAtom),
  lastName: get(lastNameAtom),
  age: get(ageAtom),
}));

const PersonComponent = () => {
  const [person] = useAtom(personAtom);
  return <>{person.firstName} {person.lastName}</>
}
gnosis23 commented 2 years ago

第九章:valtio

valtio用代理跟踪变化,记住:

Create a local snapshot that catches changes. Rule of thumb: read from snapshots, mutate the source.

import { memo, useState } from "react";
import { proxy, useSnapshot } from "valtio";

let nanoid = 0;

const state = proxy({
  todos: [],
});

const createTodo = (title) => {
  state.todos.push({
    id: nanoid++,
    title,
    done: false,
  });
};

const removeTodo = (id) => {
  const index = state.todos.findIndex((item) => item.id === id);
  state.todos.splice(index, 1);
};

const toggleTodo = (id) => {
  const index = state.todos.findIndex((item) => item.id === id);
  state.todos[index].done = !state.todos[index].done;
};

const TodoItem = ({ id }) => {
  const todoState = state.todos.find((todo) => todo.id === id);
  if (!todoState) {
    throw new Error("invalid todo id");
  }
  const { title, done } = useSnapshot(todoState);
  return (
    <div>
      <input type="checkbox" checked={done} onChange={() => toggleTodo(id)} />
      <span
        style={{
          textDecoration: done ? "line-through" : "none",
        }}
      >
        {title}
      </span>
      <button onClick={() => removeTodo(id)}>Delete</button>
    </div>
  );
};

const MemoedTodoItem = memo(TodoItem);

const TodoList = () => {
  const { todos } = useSnapshot(state);
  const todoIds = todos.map((todo) => todo.id);
  return (
    <div>
      {todoIds.map((todoId) => (
        <MemoedTodoItem key={todoId} id={todoId} />
      ))}
    </div>
  );
};

const NewTodo = () => {
  const [text, setText] = useState("");
  const onClick = () => {
    createTodo(text);
    setText("");
  };
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={onClick} disabled={!text}>
        Add
      </button>
    </div>
  );
};
gnosis23 commented 2 years ago

渲染次数统计

这个 useRef 和 useEffect 来统计渲染次数

const Counter1 = () => {
  const { count1 } = useContext(CountContext);
  const renderCount = useRef(1);
  useEffect(() => {
    renderCount.current += 1;
  });
  return (
    <div>Count1: {count1}</div>
  );
};