2019,手把手教你用React Hooks解决状态管理(上)

2,485 阅读6分钟

React团队在今年二月发布了React 16.8,想必打开这篇文章的你一定知道这个版本包含了一个令人期待的新特性:Hooks。

作为React 16.8的宠儿Hooks,大家也很关心其带来的变化,其中备受关注的一点就是状态管理,以至于大家很容易可以在各个社区搜到这些问题:

React有了Hooks还需要Redux和Mobx吗?

React Hooks会取代Redux吗?

Redux有Hooks支持吗?

….

看完这篇文章之后也许你还是无法给这些问题一个确切的答案,但是有一个问题的答案是肯定的,就是基于Hooks Api可以解决状态管理问题。

在开始阅读之前,让我们先看一个demo:

TODOMVC CodeSandbox (推荐桌面版或者手机浏览器打开,加载需要几十s,需要点耐心⌛️)

这是一个被用烂的demo - TodoList,但又有些不同,就是它用了是全新的Hooks语法和一个没见过的库解决了状态管理的问题(还用了TypeScript

下面,让我们从这个demo出发,把状态管理安排地明明白白

Image result for 安排的明明白白

TodoList是如何炼成的

首先:声明全局状态

在用Redux的时候,有两个单词是避不开的:Provider,connect,任你render props还是HOC,总要套个东西上去,要不是装饰器,要不是高阶组件

你常常会看到很多开发者写很多boilerplate less的库,其中最出名的就是Rematch,为的就是少写几行代码。但是就算用了是Context Api,Provider你也是跑不了要写。

看一下Hooks的useState api:

const Counter = () => {
    const [state, setState] = useState(0)
    // 一堆UI
}

想想之前

@connect( ... )
class Counter extends PureComponent {
    render() {
        const { ... } = this.props // 状态加载中 ...
        // 一堆UI
    }
}

你可能会想,如果有一天可以这样就好了

const Counter = () => {
    const [state, actions] = useStore('Counter')
    return <button onClick={() => actions.increment(1)}>Increment</button>
}

路人:

)

路人:“对了对了,让我看一眼你的demo怎么在入口components/App初始化的”

import * as React from "react";
import { useStore } from "../models/index";

import Header from "./Header";
// ...
import Filter from "./Filter";

const App = () => {
  const [state, actions] = useStore("Todo");
  const incompletedCount = state.todos.reduce(
    (count, todo) => count + !todo.completed,
    0
  );
  return (
    <div>
       ...
    </div>
  );
};

export default App;

路人撤回了一条消息。

路人:“但我觉得你肯定在../models/index里做了什么见不得人的事才没放出来,拿来看看”

../models/index

import { Model } from "react-model";
import Todo from "./todo";

export const { useStore } = Model({ Todo }); // 这里是支持多model的

路人: "emmmmmm, 那./todo.ts打开"

interface Todo {
 //  *$#x
}

type Filter = "All" | "Active" | "Completed";
// type  *$#x
// type  *$#x
// type  *$#x


const defaultTodos = [
  // ...
];

const model: ModelType<State, ActionParams> = {
  state: {
    todos: defaultTodos,
    filter: "All",
    id: 9
  },
  actions: {
    add: (_, __, title) => {
      // ...
    },
    edit: (_, __, info) => {
      // ...
    },
    done: (_, __, id) => {
      return state => {
        // ...
      };
    },
    allDone: () => {
      // ...
    },
    undo: (_, __, id) => {
      // ...
    },
    // ...
  }
};

export default model;

五秒后...

路人:“这都啥,type、interface的,action参数一堆下划线先不说了,这actions里的方法return有object有function,有啥区别吗?”

type,interface都是TypeScript开发者的需要的类型定义,写这些类型最终就是要产出ModelType<State, ActionParams>来表示model的类型,这样调用useStore返回的stateactions就都是有类型的了

而下划线代表那个参数不用,不写下划线会有lint报错,方法里面的参数如果全部用上的话应该是这样的

actions: {
    allNeed: (state, actions, title) => { // 当然这里是可以加 async 在前面滴
      // state 是当前model的state,
      // actions 是当前model里其他的actions
      // 你可以这样调用
      actions.add('titile')
      // return 如果是object的话,会被自动merge在当前的state上,即 return {...state, ...object}
      // 如果返回时function的话,会利用immer库在原state上进行操作,生成下一个immutable的state
      // 这样在深层嵌套属性上会非常好用
      // 返回object
        return {
            // ...state, 这个库已经帮你做了,不用再写了:)
            key: {
                ...state.key,
                deepkey: {
                    ...state.key.deepkey // 路人: 好,你够了,我懂了
                    titles: [...state.key.deepkey, title]
                }
            }
        }
    	// 返回function
    	return state => { 
    	    state.key.deepkey.push(title) // 路人: 行,有点东西
    	}
    },
}

路人:“看起来不错,你接着讲吧”

初始完数据之后,各个函数组件就可以用useStore来订阅各自需要的model(s)了,默认情况下订阅同一个model的函数组件在数据更新时都会触发rerender

components/Filter.tsx

const Filter = () => {
  const [state, actions] = useStore("Todo");
  return (
    <ul className="filters">
        ...
    </ul>
  );
};

components/TodoList.tsx

const TodoList = () => {
  const [state] = useStore("Todo");
  return (
      ...
  )
}

所以你在点击下面的All, Active, Completed的时候 TodoList 展现的内容也会随之变更。

但是当Todo的数量非常非常大的时候,比如说1W条,这种默认订阅的方式会带来很大的性能开销,一条的数据的更新就要2s,所以useStore提供了第二个参数depActions,这个参数是一个数组,只有数组中的action执行的时候这个组件才会rerender,当然这也要搭配React.memo来一起食用,我写了一个简单的Demo,需要的时候可以参考下 :),简单的优化之后单条修改rerender的时间可以缩减到~200ms

路人B:“人家redux还有中间件 (middleware) 呢,这小库有吗”

这得翻翻库的源码

Object.keys(Global.State[modelName].actions).map(
    key =>
      (updaters[key] = async (params: any, middlewareConfig?: any) => {
        const context: Context = {
          modelName,
          setState,
          actionName: key,
          next: () => {},
          newState: null,
          params,
          middlewareConfig,
          consumerActions,
          action: Global.State[modelName].actions[key]
        }
        await applyMiddlewares(actionMiddlewares, context)
      })
  )

这可以看出来 useStore 返回的actions的行为全都是actionMiddlewares决定的。上面提到的组件订阅Store,rerender,以及没提到的redux devtools支持,生产环境下action的try catch等等都是middleware实现的。

如果要对特定的Store做优化或者特殊处理,只需要对actionMiddlewares做操作就可以了。

import { actionMiddlewares, middlewares } from 'react-model'
// production模式下替换原来从actions返回获取新的state的中间件为带超时逻辑的中间件
if (process.env.NODE_ENV === 'production') {
    actionMiddlewares[1] = middlewares.getNewStateWithCache(3000)
}

如果你想自己写中间件也很简单,看下自带的中间件里actions全局tryCatch是怎么做的:

const tryCatch: Middleware<{}> = async (context, restMiddlewares) => {
  const { next } = context
  await next(restMiddlewares).catch((e: any) => console.log(e))
}

接收参数有当前actions执行时可获取的全部上下文以及后续要执行的中间件,通过context.next就可以将接力棒交给下一个中间件。

路人B:“人家redux支持SSR,Hooks怎么做SSR?”

首先Next.js是完全兼容function的用法的,库的Github上也提供了SSR的demo以及文档

(此时路人B悄悄离开

哎呀,今天也不早了,基本用法讲到这里也差不多了,而且反正也是上篇,就在这里收笔吧(有没有(下)我也不知道,讲什么我也还没想

最后的最后,如果喜欢这篇文章,欢迎随手点赞分享评论哦,能给个Star就更好了😘