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就更好了😘

相關文章
相關標籤/搜索