🛶

Thinking in React Context API

https://reactjs.org/docs/context.html

Context 可以跨层级传数据, 并且直接被 React 支持.

想想这段代码:

import React from 'react';
var obj = null;
var listener = null;
function getObj() { return obj; }
function setListener(l) { listener = l; }
function setObj(o) {
  obj = o;
  if (listener) { listener(o); }
}
function Child1() {
  return (
    <div>{JSON.stringify(getObj())}</div>
  )
}
function Child2() {
  const setRandom = () => {
    setObj({ val: Math.random().toString() });
  }
  return (
    <button onClick={setRandom}>click</button>
  )
}
export default function App() {
  const [, forceUpdate] = React.useReducer(x=>x+1,0);
  React.useEffect(() => {
    setListener((newObj) => {
      forceUpdate();
      console.log(newObj);
    })
  }, [])
  return (
    <div>
      <Child1 />
      <Child2 />
    </div>
  );
}

其实 Context 是一个全局的数据, 方便 Consumer 使用, 并且配合现在的 useContext hook, 使用起来更方便.

但 Context 仅仅是对于数据, 没有人保证数据变化子组件也会变化, 就像 props 一样, 只有父组件 re-render 子组件重新被执行才能获取新的数据. 大部分的使用场景都是配合 setState 来用, setState 使父组件 re-render, 所以子组件会获得最新的数据. 除此之外, 也有其他的使用场景.

上面说法是错误的, 经过试验发现 Context 的 value 更新会触发所有使用 useContext 的组件刷新, 并不会因为组件被 Memo 组件包围而停止刷新, 也就是说 Context 解决了深层子组件 re-render 链被打断的问题, 代码如下:

const Context = createContext()
const Component1 = React.memo(function() {
  console.log('componet1')
  return (
    <div>
      <h1>Component1</h1>
      <Component2 />
    </div>
  )
})
function Component2() {
  console.log('componet2')
  const { val } = useContext(Context);
  return (
    <div>
      <h1>Component2</h1>
      <h2>{val}</h2>
    </div>
  )
}
function App() {
  const [, forceUpdate] = useReducer(x=>x+1,0);
  const [val, setVal] = useState(null);
  return (
    <Context.Provider value={useMemo(()=>({ val, setVal }), [val])}>
      <Component1 />
      <button onClick={forceUpdate}>update</button>
      <button onClick={() => setVal(Date.now().toString())}>setRandom</button>
    </Context.Provider>
  );
}

点击 update 按钮, 子组件并不会 re-render, 而点击 setRandom 按钮, Component1 不会刷新, Component2 会刷新. 注意这里需要用 useMemo 来将 Context value 固定住, 否则每次 update 都会生成新的 object, 则影响结果.

写一个优化版本的 Context+useReducer 的代替方案(其实代替的是 react-redux):

let _obj = null;
let _listener = null;
function _getObj() { return _obj; }
function _setListener(l) { _listener = l; }
function setObj(o) {
  _obj = o;
  if (_listener) { _listener(o); }
}
function useObj() {
  const [, forceUpdate] = React.useReducer(x=>x+1,0);
  React.useEffect(() => { _setListener(() => forceUpdate()) }, [])
  return _getObj();
}
function Component1() {
  return (
    <div>
      <button onClick={() => {setObj(Math.random())}}>setrandom</button>
    </div>
  )
}
function Component2() {
  const o = useObj();
  return (
    <h1>{o}</h1>
  )
}
function App() {
  return (
    <div>
      <Component1 />
      <Component2 />
    </div>
  );
}

使用 useObj 这个 hook 来插一个 listener 到组件里面, 监听到数据变化后会触发 forceUpdate.

react-redux

react-redux 使用了 Context 来传输数据. 当然, 它最核心的是 subscription.

Provider

Provider 是这个库的最顶层, 存储了 ContextValue, 其中有两个主要的数据, 一个是主要的 store, 另一个是 subscription. 整个 react-redux 就是围绕最顶层 subscription 展开的.

Connect

connect 是一个 HOC, 被 connect 的组件会自动继承到 parent 的 subscription. 当 store 数据变化, 会通过 subscription 一个一个通知到整个 tree.

大概是这个流程, 所以就没有用到 setState 或者 forceUpdate 来通知整个 tree 来更新, 这样保证了没有被 connect 的组件避免刷新.

那它是通过什么方式呢? 通过一个神秘的 React API: useSyncExternalStore, 它接受的第一个参数是上面的 subscription, 这样 React 会插入一个 listener 到其中, 当 subscription 更新时候, React 会收到通知, 然后 re-render.


在我们一生中,命运赐予我们每个人三个导师,三个朋友,三名敌人,三个挚爱。但这十二人总是不以真面目示人,总要等到我们爱上他们、离开他们、或与他们对抗时,才能知道他们是其中哪种角色。