目录

Context + Hooks:状态管理新方案

React 组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

React 组件分为两类,class 组件和函数组件。hooks 的出现让函数组件拥有了状态(state), 因此让自定义 hook 成为了继 render-props 和高阶组件(HOC)之后的第三种状态共享方案。

class 组件的状态共享

render-props

具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它(回调函数)而不是实现自己的渲染逻辑。 react 官网示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img
        src="/cat.jpg"
        style={{ position: "absolute", left: mouse.x, top: mouse.y }}
      />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  }

  render() {
    return (
      <div style={{ height: "100vh" }} onMouseMove={this.handleMouseMove}>
        {/*使用`render`属性来动态确定要渲染的内容。*/}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移动鼠标!</h1>
        {/*将 Mouse 组件中的 state 传递给 Cat 组件*/}
        <Mouse render={(mouse) => <Cat mouse={mouse} />} />
      </div>
    );
  }
}

注意 Mouse 组件中的 this.props.render 是绑定在标签模板上的 render(外部传入)。这样就实现了鼠标位置状态的共享, Cat 组件能够根据鼠标位置动态移动 cat 图片。这个示例实现了 react 组件的理想状态:有状态的组件无渲染,有渲染的组件无状态。 因为 Cat 组件只是一个渲染模板,它也可以替换成如下的函数组件:

1
2
3
4
5
6
7
const Cat = (props) => {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

UI 与状态分离,便于逻辑的复用。

高阶组件(HOC)

高阶组件是参数为组件,返回值为新组件的函数,高阶组件是函数。

1
const EnhancedComponent = higherOrderComponent(WrappedComponent);

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function high(WrappedComponent){
  return class extends React.Component{
    constructor(){
      this.state={
        open:false
      }
    }
    componentDidMount(){
      console.log('haha')
    }
    change=()=>{
      this.setState((state)=>{//用到state需要使用回调函数修改state的值
          return {open:!state.open}
      })
    }
    render(){
      //使用新数据渲染被包装的组件
      return <WrappedComponent open={this.state.open} change={this.change} />
    }
  }
}

class ToggleButton extends Component{//不带有自身的状态能够实现组件的复用
  constructor(props){
    super(props)
  }
  render(){
    let {open,change}=this.props; // 来自 high 的数据
    return <Fragment>
              <button type="primary" onClick={change}>
                toggle Modal
              </button>
              <div>{open}</div> //拿到open值
          </Fragment>
  }
}
// high 是一个高阶组件,传入组件作为参数,组件就能接收 high 的数据
export default high(ToggleButton)

每个经过高阶组件处理过的组件都会复用高阶组件里边的所有逻辑,原则上高阶组件是一个纯函数,不会修改传入的组件,只是返回包装好的新组件。

函数组件的状态共享

Hooks 可以让你在函数组件中使用状态(state)以及其他的 React 特性。

自定义 hook

Hook 是 React 中的一类特殊的 JavaScript 函数。自定义名为 useFriendStatus 的 hook,它通过调用 useState 和 useEffect 来订阅一个好友的在线状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { useState, useEffect } from "react";

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 现在我们可以在下面两个组件中使用它:

1
2
3
4
5
6
7
8
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return "Loading...";
  }
  return isOnline ? "Online" : "Offline";
}
1
2
3
4
5
6
7
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li>
  );
}

这两个组件的 state 是完全独立的,Hook 是一种复用状态逻辑的方式,它不复用 state 本身。传入不同的 props,得到的 state 也不同。同样是实现了 UI 与状态分离,便于逻辑的复用。

但是使用 Hook 会有几个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
  • 不要在普通 Javascript 函数中调用
    • 在 React 的函数组件调用 Hook
    • 在自定义的 Hook 中调用 Hook
  • 自定义的 hook 必须以 “use” 开头

状态管理

实现状态管理的前提是能够状态共享,这就是为什么前面会先说状态共享。不同类型的组件实现状态共享的方法不同,状态管理的方案也不同。下面是一个计数器的状态管理的不同实现方案。

class 组件的状态管理

class 组件的状态管理,通常方案是使用第三方库 Redux,结合 React-Redux 使用:

1
$ npm i redux react-redux -S

Redux 流程图

/img/redux.png
Redux流程图

当 UI 的 state 变化时,组件 dispatch 发送 action 信号, reducer 接收来自 action 的信号更新 state, 然后 store 将新的 state 传递给组件,重新渲染 UI。

先创建 store,接收 reducer 为参数:

1
2
3
4
5
6
7
import { createStore } from "redux";
import reducer from "./reducer";

//创建store
const store = createStore(reducer);

export default store;

再写 action,写 action 之前先了解一下 connect 函数

1
2
// React Redux 的 `connect` 函数
const connect(mapStateToProps, mapDispatchToProps)(Component);

可能看起来有些怪, 这样写你就明白了:

1
2
3
4
//先传递两个参数将 connect 封装成高阶函数
const higherOrderComponent = connect(mapStateToProps, mapDispatchToProps);
//再得到新包装的组件 EnhancedComponent
const EnhancedComponent = higherOrderComponent(Component);

action 就是 dispatch 中的参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// connect.js
import { connect } from "react-redux";

const mapStateToProps = (state) => {
  return { count: state.count, message: state.message };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: (data) => {
      dispatch({ type: "INCREMENT", num: data, message: "Incremented" });
    },
    decrement: (data) => {
      dispatch({ type: "DECREMENT", num: data, message: "Decremented" });
    },
    reset: () => {
      dispatch({ type: "RESET", message: "Reset" });
    },
  };
};
//封装了一个高阶组件,注意高阶组件是函数
export default connect(mapStateToProps, mapDispatchToProps);

最后写 reducer,接收 action 更新 state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const initialState = { count: 0, message: "" };
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {
        count: state.count + action.num,
        message: action.message,
      };
    case "DECREMENT":
      return {
        count: state.count - action.num,
        message: action.message,
      };
    case "RESET":
      return {
        count: 0,
        message: action.message,
      };
    default:
      return state;
  }
};

export default reducer;

创建一个组件测试计数器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from "react";
import connect from "./connect";

class Count extends Component {
  render() {
    let { count, message, increment, decrement, reset } = this.props; //来自 connect

    return (
      <div>
        {count}
        <button onClick={() => increment(1)}>+1</button>
        <button onClick={() => decrement(3)}>-3</button>
        <button onClick={() => reset()}>reset</button>
        {message}
      </div>
    );
  }
}

// 导入的 './connect' 是高阶组件,传入 Count 组件, Count就能接收 store 中的数据
export default connect(Count);

根组件注册 store,并导入 count 组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import Count from "./count";

function App() {
  return (
    <Provider store={store}>
      <Count />
    </Provider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

此时启动项目你发现已经能够计数了,但是我们并没有直接操作 store 啊,其实是 connect 帮我们做了这件事,可以看一下精简版的 connect 源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { Component } from "react";
import PropTypes from "prop-types";

const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object,
    };

    constructor() {
      super();
      this.state = { allProps: {} };
    }

    componentWillMount() {
      const { store } = this.context;
      this._updateProps();
      store.subscribe(this._updateProps);
    }

    _updateProps = () => {
      const { store } = this.context;
      let stateProps = mapStateToProps(store.getState());
      let dispatchProps = mapDispatchToProps(store.dispatch);
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props,
        },
      });
    };

    render() {
      return <WrappedComponent {...this.state.allProps} />;
    }
  }

  return Connect;
};

export default connect;

你会发现 store 实际上是通过 Context 创建的,Context 是 React 中的 API 方法: Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。 有了状态共享方法(高阶组件)和数据传递的方法(Context), 就能让在整个组件树中的各个组件都很方便的读取状态修改状态, 就实现了 React-Redux,下面我会用 hooks 实现类似的全局状态管理。

函数组件的状态管理

实际上 React 已经为我们实现了相应的 hooks, 我们需要做的只是将这些 hooks 灵活的组合在一起。就能够实现状态管理了,还是以实现计数器的为例。 实现一个 React-redux 中的 store 只需以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//store.js
import React, { createContext, useContext, useReducer } from "react";
import reducer from "./reducer";
const StoreContext = createContext();
const initialState = { count: 0, message: "" };

export const StoreProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
};

export const useStore = () => useContext(StoreContext);

useReducer 是 useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,以及初始状态 initialState,返回值是当前的 state 以及与其配套的 dispatch 方法。

useContext 的参数必须是 context 对象,让你能够读取 context 的值以及订阅 context 的变化。调用了 useContext 的组件会在 context 值变化时重新渲染。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

OK, 我们的简版 React-redux 就做好了。

写 action, 我们的状态数据从 useCounter 里获取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//storeApi.js
import { useStore } from "./store";

export const useCounter = () => {
  const { state, dispatch } = useStore();
  return {
    count: state.count,
    message: state.message,
    increment: (data) =>
      dispatch({ type: "INCREMENT", num: data, message: "Incremented" }),
    decrement: (data) =>
      dispatch({ type: "DECREMENT", num: data, message: "Decremented" }),
    reset: () => dispatch({ type: "RESET", message: "Reset" }),
  };
};

写 reducer, 去掉 initialState, 我们已经写在了 useReducer 里, 原因是:

注意
React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// reducer.js
const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {
        count: state.count + action.num,
        message: action.message,
      };
    case "DECREMENT":
      return {
        count: state.count - action.num,
        message: action.message,
      };
    case "RESET":
      return {
        count: 0,
        message: action.message,
      };
    default:
      return state;
  }
};

export default reducer;

写个组件,测试一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//Count.js
import React from "react";
import { useCounter } from "./storeApi";

export const Count = () => {
  const { count, message, increment, decrement, reset } = useCounter();

  return (
    <div>
      {count}
      <button onClick={() => increment(1)}>+1</button>
      <button onClick={() => decrement(3)}>-3</button>
      <button onClick={() => reset()}> Reset</button>
      {message}
    </div>
  );
};

修改根组件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import React from "react";
import ReactDOM from "react-dom";
import { StoreProvider } from "./store";
import { Count } from "./Count";

function App() {
  return (
    <StoreProvider>
      <Count />
    </StoreProvider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

启动服务,发现能够计数成功,我们的状态管理方案成功了。