项目学习 3 —— 使用Context和hook做状态管理

使用Context和hook做状态管理

学习过 React Hook,就会知道自定义 Hook 可以封装状态管理逻辑,并达到复用、共享的效果。

现在我们有一个计时器🌰

import React, { useState } from 'react';
import { render } from 'react-dom';

function CounterDisplay() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>You clicked {count} times</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

render(<CounterDisplay />, document.getElementById('root'));

如果我们要复用计数器状态管理这部分代码,我们可以使用自定义 hook:

import React, { useState } from 'react';
import { render } from 'react-dom';

function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return { count, decrement, increment };
}

function CounterDisplay() {
  const { count, decrement, increment } = useCounter();

  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>You clicked {count} times</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

function AnotherCounterDisplay() {
  const { count, decrement, increment } = useCounter();

  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>当前分数:{count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

render(
  <>
    <CounterDisplay />
    <AnotherCounterDisplay />
  </>,
  document.getElementById('root'),
);

通过努力,CounterDisplayAnotherCounterDisplay两个组件共享了计数状态管理逻辑。但如果要求这两个组件共享状态怎么办?这时候,我们可能会想到状态提升,如下所示:

import React, { useState } from 'react';
import { render } from 'react-dom';

interface Counter {
  count: number;
  decrement: () => void;
  increment: () => void;
}

function useCounter(): Counter {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return { count, decrement, increment };
}

function CounterDisplay(props: { counter: Counter }) {
  const { count, decrement, increment } = props.counter;

  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>You clicked {count} times</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

function AnotherCounterDisplay(props: { counter: Counter }) {
  const { count, decrement, increment } = props.counter;

  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>当前分数:{count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

function App() {
  const counter = useCounter();

  return (
    <>
      <CounterDisplay counter={counter} />
      <AnotherCounterDisplay counter={counter} />
    </>
  );
}

render(<App />, document.getElementById('root'));

 如果需要共享状态的组件与共同父组件层级比较深,那么我们可以使用 React Context 简化状态提升需要逐级传输组件属性:

import React, { useState, useContext } from 'react';
import { render } from 'react-dom';

interface Counter {
  count: number;
  decrement: () => void;
  increment: () => void;
}

function useCounter(): Counter {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return { count, decrement, increment };
}

const CounterContext = React.createContext<Counter>(defaultValue);

function CounterDisplay() {
  const { count, decrement, increment } = useContext(CounterContext);
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>You clicked {count} times</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

function AnotherCounterDisplay() {
  const { count, decrement, increment } = useContext(CounterContext);
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>当前分数:{count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

function CounterInfo() {
  const counter = useContext(CounterContext);
  return <div>当前计数:{counter.count}</div>;
}

function Header() {
  return (
    <div>
      <h1>计数器</h1>
      <CounterInfo />
    </div>
  );
}

function App() {
  const counter = useCounter();
  return (
    <CounterContext.Provider value={counter}>
      <div>
        <Header />
        <CounterDisplay />
        <AnotherCounterDisplay />
      </div>
    </CounterContext.Provider>
  );
}
render(<App />, document.getElementById('root'));

⚠️注意:

React.createContext<Counter>(defaultValue)
<Counter> 指的是 传入的参数 defaultValue 是 Counter 类型

现在要求AnotherCounterDisplay的状态单独管理,依然可以用 Context:

function App() {
  const counter = useCounter();
  const anotherCounter = useCounter();

  return (
    <CounterContext.Provider value={counter}>
      <div>
        <Header />
        <CounterDisplay />
        <CounterContext.Provider value={anotherCounter}>
          <AnotherCounterDisplay />
        </CounterContext.Provider>
      </div>
    </CounterContext.Provider>
  );
}

我们也可以将再创建一个组件,专门用来提供计数状态管理的上下文:

function CounterContextProvider({ children }: { children: React.ReactNode }) {
  const counter = useCounter();

  return (
    <CounterContext.Provider value={counter}>
      {children}
    </CounterContext.Provider>
  );
}

⚠️注意:

{ children }: { children: React.ReactNode }
{children}:
  变量是个对象,对象里面是children 
{ children: React.ReactNode }

  变量里面的children 是个 React.ReactNode 类型

  一个 ReactNode 可以是:

    • ReactElement
    • string (aka ReactText)
    • number (aka ReactText)
    • Array of ReactNodes (aka ReactFragment)

  他们被用作其他ReactElements的properties来表示子级.事实上他们创建了一个 ReactElements 的树. 

稍微实践一下,我们会发现:为自定义 hook 创建的上下文这种模式很有用。我们来整理一下这种模式的计数器例子:

interface Counter {
  count: number;
  decrement: () => void;
  increment: () => void;
}

// 首先定义一个React Hook

function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return { count, decrement, increment };
}

// 然后定义一个上下文:

const CounterContext = React.createContext<Counter>(null);

// 之后我们创建一个提供上下文的Provider组件:

function CounterContextProvider({ children }: { children: React.ReactNode }) {
  const counter = useCounter();

  return (
    <CounterContext.Provider value={counter}>
      {children}
    </CounterContext.Provider>
  );
}

// 之后,我们就可以尽情地使用了:

function App() {
  <CounterContextProvider>
    <CounterDisplay />
  </CounterContextProvider>;
}

function CounterDisplay() {
  const counter = useContext(CounterContext);

  return <div>{counter.count}</div>;
}

这种模式的核心点就是需要自定义 hook。然后都会有第二步和第三步,那么我们可以继续提炼一下(第二步和第三步):

                             //func: () => T
  function createContainer<T>(func:Function):T {
    const ContainerContext = React.createContext<T | null>(null);
    const Provider = ({ children }: { children: React.ReactNode }) => {
      const result = func();
  
      return (
        <ContainerContext.Provider value={result}>
          {children}
        </ContainerContext.Provider>
      );
    };
  
    const useContainer = () => {
      return useContext(ContainerContext);
    };
  
    return { Provider, useContainer };
  }

我们使用createContainer来简化自定义 hook 上下文这种模式:

// 首先定义一个React Hook

function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return { count, decrement, increment };
}

// 然后定义计数容器
const CounterContainer = createContainer(useCounter);

// 之后,我们就可以尽情地使用了:

function App() {
  <CounterContainer.Provider>
    <CounterDisplay />
  </CounterContainer.Provider>;
}

function CounterDisplay() {
  const counter = CounterContainer.useContainer();

  return <div>{counter.count}</div>;
}

这里,我们引入了一个container名词,用来表示包装自定义 hook 到上下文,我们姑且称之为“hook 容器”,或者简称为“容器”。

刚刚的createContainer已经由unstated-next实现:

 安装unstated-next  

yarn add unstated-next
import { createContainer } from 'unstated-next';

// 首先定义一个React Hook

function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);

  return { count, decrement, increment };
}

// 然后定义计数容器
const CounterContainer = createContainer(useCounter);

// 之后,我们就可以尽情地使用了:

function App() {
  <CounterContainer.Provider>
    <CounterDisplay />
  </CounterContainer.Provider>;
}

function CounterDisplay() {
  const counter = CounterContainer.useContainer();

  return <div>{counter.count}</div>;
}

页面组件的状态管理与“hook 容器”模式

  将状态管理逻辑与 UI 逻辑分离开 ---- React Hooks。

  对于一个页面组件来说,我们使用组件来处理 UI 渲染,使用 React Hooks 来处理状态。大概率下页面各个部分需要共享状态。这样分析,你会发现“hook 容器”模式非常适合页面组件的开发:将页面级别的状态管理放在页面自定义 hook 中,页面的各个子组件都可以通过上下文快速获取到需要的共享状态。

function useXxxxPage() {
  ....
}

const XxxxPageContainer = createContainer(useXxxxPage);

function XxxxPageHeader() {
  const xxxxPageState = XxxxPageContainer.useContainer();

  //....
}

function XxxxPageContent() {
  const xxxxPageState = XxxxPageContainer.useContainer();

  //...
}

function XxxxPageFooter() {
  const xxxxPageState = XxxxPageContainer.useContainer();

  //...
}

function XxxxPage() {
  return <XxxxPageContainer.Provider>
    <div>
      <XxxxPageHeader />
      <XxxxPageContent />
      <XxxxPageFooter />
    </div>
  </XxxxPageContainer.Provider>
}

强调一点:在页面级别需要共享的数据才需要放到useXxxxPage中。局部状态依然首推在局部组件级别解决。

页面组件也是组件,在 React 中没有任何特殊的设定,只是页面组件往往会面临状态的跨级共享,而且我们在开发应用时,一般会从页面组件开始,所以,我们可以选择一种状态管理模式作为状态管理的参考实现,“hook 容器”就是一种好的模式。但是页面组件的状态管理同样需要遵循组件状态管理的最佳实践,当发现“hook 容器”不适合时,应该考虑其他的最佳实践。

在做应用开发时,遵循以下几个要点:

  • 用组件做 UI 渲染
  • 用组件做 UI 渲染逻辑复用
  • 用 React Hooks 做组件状态管理
  • 用自定义 hook 做状态管理逻辑复用
  • 用自定义 hook 做状态管理逻辑与 UI 渲染逻辑分离
  • 遇到跨级共享状态时,用 React Context
  • 如果用 React Context + custom hooks 做跨级共享状态,可以考虑用 unstated-next

unstated-next 使用要点

  • 要点#1: 保持 Containers 很小
  • 要点#2:组合 Containers
  • 要点#3:优化组件 

 转载自:https://sinoui.github.io/sinoui-guide/docs/context-and-hook

posted @ 2022-01-17 16:11  山海南坪  阅读(352)  评论(0编辑  收藏  举报