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

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

做为React 16.8的宠儿Hooks,你们也很关心其带来的变化,其中备受关注的一点就是状态管理,以致于你们很容易能够在各个社区搜到这些问题:git

React有了Hooks还须要Redux和Mobx吗?github

React Hooks会取代Redux吗?typescript

Redux有Hooks支持吗?redux

….api

看完这篇文章以后也许你仍是没法给这些问题一个确切的答案,可是有一个问题的答案是确定的,就是基于Hooks Api能够解决状态管理问题。数组

在开始阅读以前,让咱们先看一个demo:浏览器

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

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

下面,让咱们从这个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就更好了😘

相关文章
相关标签/搜索