React Hooks(一): From Redux to Hooks

現在的 react 的狀態管理工具基本上分爲 redux 和 mobx 兩個流派,mobx 基本上你們都是使用官方的 mobx 庫,可是對於 redux 卻衍生數不勝數的 redux 框架。如redux-saga, dva, mirror, rematch等等,這麼多 redux 的框架一方面說明 redux 是如此流行,另外一方面也代表 redux 自身的先天不足,筆者本人也是從最初的刀耕火種時代一路走來。html

最原始的 redux

// action_constant.js
// action_creator.js
// action.js
// reducer.js
// store.js
// 再加上一堆的middleware

複製代碼

每次改一點業務動輒就須要改四五個文件,着實使人心累,並且不一樣業務對 redux 文件的組織方式也不一樣,用的按照組件進行組織,有的按照功能進行組織,每次看新的業務都得熟悉半天,對異步的支持也基本上就使用 redux-thunk、redux-promise 等,遇到複雜的異步處理,代碼十分的晦澀難懂。react

redux duck

後來社區爲了不每次修改都要修改一堆文件和制定文件規範,推出了 ducks-modular-redux 規範,將每一個子 module 的文件都放置到一個文件裏,這樣大大簡化了平常開發中一些冗餘工做。git

// widgets.js

// Actions
const LOAD   = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';

// Reducer
export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    // do reducer stuff
    default: return state;
  }
}

// Action Creators
export function loadWidgets() {
  return { type: LOAD };
}

export function createWidget(widget) {
  return { type: CREATE, widget };
}

export function updateWidget(widget) {
  return { type: UPDATE, widget };
}

export function removeWidget(widget) {
  return { type: REMOVE, widget };
}

// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
  return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
}

複製代碼

筆者的以前維護的一個老項目至今仍然採用這種方式。github

rematch | dva

duck modular proposal 雖然必定程度上減少了維護成本,但本質上並無減少每次開發業務的代碼量,異步等問題仍然沒有獲得解決,所以開始衍生出了一大堆的基於 redux 的框架,重點在於解決簡化樣板代碼量和複雜異步流程的處理。
樣板代碼簡化的思路基本上是一致的。咱們發現絕大部分的業務 model 都知足以下性質typescript

const model = createModel({
  name: // 全局的key
  state:xxx, // 業務狀態
  reducers:xxx, // 同步的action
  effects:xxxx, // 異步的action
  computed: xxx // state的衍生數據
}

複製代碼

所以絕大部分框架的都採用了相似的定義,區別只在於語法和名稱有所不一樣redux

  • dva
// dva.js
export default {
  namespace: 'products',
  state: [],
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
 effects: {
   *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    }
 } 
};

複製代碼
  • rematch
export const count = {
  state: 0, // initial state
  reducers: {
    // handle state changes with pure functions
    increment(state, payload) {
      return state + payload
    }
  },
  effects: (dispatch) => ({
    // handle state changes with impure functions.
    // use async/await for async actions
    async incrementAsync(payload, rootState) {
      await new Promise(resolve => setTimeout(resolve, 1000))
      dispatch.count.increment(payload)
    }
  })
}

複製代碼

二者的區別主要在於對異步的處理,dva 選擇了用 generator,而 rematch 選擇了用 async/await。
首先咱們回顧一下 redux-thunk 裏是如何處理異步流的promise

const fetch_data = url =>  (dispatch, getState) =>{
  dispatch({
    type: 'loading',
    payload: true
  })
  fetch(url).then((response) => {
    dispatch({
      type: 'data',
      payload: response
    })
    dispatch({
      type: 'loading',
      payload: false
    })
  }).catch((err) => {
    dispatch({
      type: 'error',
      payload: err.message
    })
    dispatch({
      type: 'loading',
      payload: false
    })
  })
}

複製代碼

一個簡單的拉取數據的邏輯就顯得如此繁雜,更別提如何將多個異步 action 組合起來構成更加複雜的業務邏輯了(我已經不知道咋寫了)
async/await 和 generator 的最大優勢在於 1. 其可使用看似同步的方式組織異步流程 2. 各個異步流程可以很容易的組合到一塊兒。具體使用哪個全看我的喜愛了。
如上面一樣的邏輯在 rematch 裏的寫法以下瀏覽器

const todo = createModel({
  effects: ({todo}) => ({
    async fetch_data(url) { 
      todo.setLoading(true);
      try {
        const response = fetch(url);
        todo.setLoading(false);
      }catch(err){
        todo.setLoading(false);
        todo.setError(err.message)
      }
    },
    async serial_fetch_data_list(url_list){
      const result = []
      for(const url of url_list){
        const resp = await todo.fetch_data(url);
        result.push(resp);
      }
      return result;
    }
  })
})

複製代碼

得益於 async/await 的支持,如今不管是異步 action 自己的編寫仍是多個異步 action 的組合如今都不是問題了。安全

咱們如今的絕大部分新業務,基本上都仍是採用 rematch,相比以前純 redux 的開發體驗,獲得了很大的改善,可是仍然不是盡善盡美,仍然存在以下一些問題。markdown

Typescript 支持

9102 年了,Typescript 已經大大普及,稍微上點規模的業務,Typescript 的使用已是大勢所趨,Typescript 的好處就很少贅述,咱們基本上全部的業務都是使用 Typescript 進行開發,在平常開發過程當中基本上碰到的最大問題就是庫的支持。
俗話所說,Typescript 坑不太多(其實也多),庫的坑不太多,可是 Typescript 和庫結合者使用,坑就不少了。很不幸 Dva 和 Rematch 等都缺少對 Typescript 的良好支持,對平常業務開發形成了不小的影響,筆者就曾經針對如何修復 Rematch 的類型問題,寫過一篇文章 zhuanlan.zhihu.com/p/78741920 ,可是這仍然是個 hack 的辦法,dva 的 ts 支持就更差了,generator 的類型安全在 ts3.6 版本才得以充分支持(還有很多 bug),至今也沒看到一個能較完美支持 ts 的 dva 例子。

Batteries Included

redux 能夠說是 Batteries Included 的標準反例了,爲了保證本身的純粹,一方面把異步處理這個髒活,所有交給了中間件,這致使搞出了一堆的第三方的異步處理方案,另外一方面其不肯作更高的抽象,致使須要編寫一堆的 boilerplate code 還致使了各類寫法。所以對於平常的業務開發來說,一個 Batteries Included 庫就足夠重要了,即保證了編碼規範,也簡化了業務方的使用。
Computed State 和 immutable 就是平常開發中很是重要的 feature,可是 rematch 把兩個功能都交給插件去完成,致使平常使用不夠方便和第三方插件的 TS 支持也不盡如人意。

僅支持對 redux 狀態的管理

現在 react 的狀態和業務邏輯基本上存在於三種形態

  • redux: 存放業務領域的狀態,同時存放一些業務更新邏輯
  • context: 主要存放一些全局配置的信息,較少變更或者不變如,主題、語言等信息
  • local: 多存放 UI 相關的狀態,如模態框的展現狀態,loading 狀態等等。在 class 組件裏存放於 this.state 中,在 hook 組件中存放於 useState 裏

rematch 對 redux 的狀態管理方式基本上作到了最簡,可是其僅僅只能用於 redux 狀態的管理,對於 local state 的管理卻迫不得已。

local state 的管理

對於大部分的簡單業務,local state 的管理並不麻煩,基本上就是控制一些彈窗的展現,loading 的展現,在用 class 組件來控制業務邏輯時,處理方式也較爲簡單

class App extends React.Component {
  state = {
    loading: false,
    data: null,
    err: null
  }
  async componentDidMount() {
    this.setState({loading: true})
    try {
      const result = await service.fetch_data() 
      this.setState({
        loading:false
      })
    }catch(err){
      this.setState({loading: false, error: err.message})
    }
  }
  render(){
    if(this.state.loading){
      return <div>loading....</div>
    }else{
      return <div>{this.sstate.data}</div>
    }
  }
}

複製代碼

這裏的組件其實同時扮演了三個角色

  • 狀態容器
state = {
    loading: false,
    data: null,
    err: null
  }

複製代碼
  • 狀態處理
async componentDidMount() {
    this.setState({loading: true})
    try {
      const result = await service.fetch_data() 
      this.setState({
        loading:false
      })
    }catch(err){
      this.setState({loading: false, error: err.message})
    }
  }

複製代碼
  • view
render(){
    if(this.state.loading){
      return <div>loading....</div>
    }else{
      return <div>{this.sstate.data}</div>
    }
  }

複製代碼

這種作法有利有弊,好處在於其足夠的 locality, 由於狀態,狀態處理,渲染這幾部分是緊密關聯的,將它們放在一塊兒,閱讀代碼的看到這段代碼,很天然的就能看懂
可是一個組件放置了太多的功能就致使其複用很困難。
所以衍生出了不一樣的複用方式

容器組件和視圖組件分離:視圖複用

第一種複用方式就是經過狀態容器組件和視圖組件將狀態 && 狀態處理與 view 的邏輯進行分離,
容器組件只負責處理狀態 && 狀態處理,視圖組件只負責展現的邏輯,這樣作法的最大好處在於視圖組件的複用極爲方便。
UI 組件庫可謂是這方面的極致了,咱們將一些經常使用視圖組件提取出來構成組件庫,大部分的 UI 組件,沒有狀態,或者一些非受控的組件有一些內部狀態。這種組件庫極大的簡化了平常的 UI 開發。上面的組件能夠重構以下

// 視圖組件
class Loading extends React.Component {
  render(){
    if(this.props.loading){
      return <div>loading....</div>
    }else{
      return <div>{this.props.data}</div>
    }
  }
}
// 容器組件
class LoadingContainer extends React.Component {
  state = {
    loading: false,
    data: null,
    err: null
  }
  async componentDidMount() {
    this.setState({loading: true})
    try {
      const result = await service.fetch_data() 
      this.setState({
        loading:false
      })
    }catch(err){
      this.setState({loading: false, error: err.message})
    }
  }
  render(){
     return <Loading {...this.state} /> // 渲染邏輯交給視圖組件
  }
}
// app.js
<LoadingContainer>

複製代碼

HOC && renderProps && Hooks: 業務複用

視圖組件的複用很是方便,可是容器組件的複用就沒那麼簡單了。社區中衍生出了 HOC 和 renderProps 來解決狀態 && 狀態操做的複用

  • HOC
// Loading.js
class Loading extends React.Component {
  render(){
    if(this.props.loading){
      return <div>loading....</div>
    }else{
      return <div>{this.props.data}</div>
    }
  }
}
export default withLoading(Loading);

// app.js
<Loading />

複製代碼
  • renderProps
<WithLoading>
  {(props) => {
    <Loading {...props} />
  }}
</WithLoading>

複製代碼

這兩種方式都存在必定的問題
對於高階組件,存在不少須要注意的地方,如 zh-hans.reactjs.org/docs/higher… ,帶來不小的心智負擔,對於新手並不友好,另外一個問題在於 HOC 對於 Typescript 的支持並不友好,實現一個 TS 友好的 HOC 組件有至關大的難度可參考 www.zhihu.com/question/27… 在平常使用第三方的支持高階組件庫也常常會碰到各類 TS 的問題。
而 renderProps 雖然必定程度上拜託了 HOC 存在的問題,可是其會形成 render props callback hell, 當咱們須要同時使用多個 renderprops 的時候, 就會編寫出以下代碼

這種代碼不管是對代碼的閱讀者,仍是調試 element 結構的時候,都會帶來不小的影響。

  • Hooks
    官方爲了解決狀態複用的問題,推出了 react hooks,且解決了 renderProps 和 HOC 帶來的問題,上面組件用 hooks 重寫以下
// hooks.js
function useLoading(){
  const [loading, setLoading] = useState(false);
  const [ error, setError] = useState(null);
  const [ data,setData] = useState(null);
  useEffect(() => {
    setLoading(true);
    fetch_data().then(resp => {
      setLoading(false);
      setData(resp);
    }).catch(err => {
      setLoading(false);
      setError(err.message)
    })
  })
}
// Loading.js
function Loading(){
  const [loading, error, data ] = useLoading();
  
    if(loading){
      return <div>loading....</div>
    }else{
      return <div>{data}</div>
    }
  
}

複製代碼

hooks 的複用性特別強,事實上社區上已經積攢了不少的 hook 能夠直接使用,如能夠直接使用 github.com/alex-cory/u… 這個 hooks 來簡化代碼

function Loading(){ 
   const { error, loading, data} = useHttp(url);
     if(loading){
      return <div>loading....</div>
    }else{
      return <div>{data}</div>
    }
}

複製代碼

hooks 幾乎完美解決了狀態複用的問題,可是 hooks 自己也帶來了一些問題,
hooks 的心智負擔並不比 HOC 要少,zh-hans.reactjs.org/docs/hooks-… FAQ 的長度可見一斑,另外一個問題是 hook 只能使用在 function 裏,這意味着咱們須要在 function 裏組織業務代碼了

Function && Class 誰更適合業務邏輯

剛剛從 class 組件轉移到 hook 組件時,大部分人最早碰到的問題就是如何組織業務邏輯
class 裏的 method 自然的幫咱們作好了業務隔離

import React from 'react';
class App extends React.Component {
  biz1 = () =>{
  }
  biz2= () =>{
    this.biz3()
  }
  biz3= () =>{
  }
  render(){
    return (
      <div>
        <button onClick={() => this.biz1()}>dobiz1</button>
        <button onClick={() => this.biz2()}>dobiz2</button>
      </div>
    )
  }
}

複製代碼

可是到了 function 裏,已經缺少 method 的這個抽象來幫咱們作業務隔離了,頗有可能寫成以下這種代碼

function App (){
  const [state1, setState] = useState();
  function biz1(){

  }
  biz1();
  const [state2, setState2] = useState();
  const biz2 = useCallback(() => {
    biz3();
  },[state1,state2])
  biz2();
  return (
      <div>
        <button onClick={() => biz1()}>dobiz1</button>
        <button onClick={() => biz2()}>dobiz2</button>
      </div>
    )
  function biz3(){

  }
}

複製代碼

基本上是你想怎麼來就怎麼來,能夠有無數種寫法,本身寫的還好,其餘讀代碼的人就是一頭霧水了,想理清一段業務邏輯,就得反覆橫跳了。

固然也能夠指定一些編寫 hook 的規範如

function APP(){
  // 這裏放各類hook
 // 同步的業務邏輯
 // render邏輯
 // 業務邏輯定義
}

複製代碼

按照這種規範,上述代碼以下

function App (){
  const [state1, setState] = useState();
  const [state2, setState2] = useState(); 
  biz0();
  return (
      <div>
        <button onClick={() => biz2()}>dobiz1</button>
        <button onClick={() => biz2()}>dobiz2</button>
      </div>
    )
  function biz0(){
    // 同步代碼
  }
  function biz1(){
    // 異步代碼
  }
  function biz2(){
    // 異步代碼
    biz3()
  }
  function biz3(){
    // utilty
  }
}

複製代碼

這樣組織代碼的可讀性就好不少,可是這只是人爲約定,也沒有對應的 eslint 作保證,並且 biz 的定義也無法使用 useCallback 等工具了,仍然存在問題。

編寫 local state 存在的問題

上面的討論咱們能夠看出,儘管 hooks 解決了狀態複用的問題,可是其代碼的組織和維護存在較多問題,如何解決 hooks 代碼的維護問題就成了個問題

狀態全放在 rematch 裏

rematch 的狀態管理比較規整,咱們所以能夠考慮將 local state 的狀態管理頁存放到全局的 redux 裏,但這樣會帶來一些問題

  • 有些狀態自己不太適合放在全局,如 A 頁面的一些 UI 狀態切換到 B 頁面時,咱們指望丟棄掉 A 頁面的狀態,若是狀態放置到 A 的組件裏,隨着 A 組件的卸載,狀態天然而然丟棄掉,而若是放置到全局,則須要手動的進行清理
  • 全局狀態的泛濫:將一些局部狀態放置到全局會形成全局狀態的泛濫,致使難以辨別核心的業務邏輯
  • 違反了局部性的原則:業務邏輯放在全局,致使閱讀組件代碼時,須要頻繁的在組件和全局狀態內進行切換

model 和 view 的分離

咱們雖然不能將狀態放在全局,咱們仍然能夠效仿 rematch 的方式,將組件拆分爲 view 和 model,view 負責純渲染,model 裏存放業務邏輯,藉助於 hooks,比較容易實現該效果,大體代碼結構以下

// models.ts
const model = {
  state:{
    data: null,
    err: null,
    loading: false
  },
  setState: action((state,new_state) => {
     Object.assign(state,new_state)
  }),
  fetch_data: effects(async (actions) => {
     const { setState } = actions;
     setState({loading: true});
     try {
       const resp = await fetch();
       setState({
	       loading: false,
           data:resp
       })
     }catch(err){
	     setState({
	     loading: false,
	     err: err.mssage
	  })
    }
  })
}

// hooks.ts
import model from './model';
export const useLoading = createLocalStore(model);

// loading/ index.ts
import {useLoading} from './hooks';
export default () => {
  const [state, actions] = useLoading();
  return (<Loading {...state} {...actions} />)
}
const Loading = ({
   err,
   data,
   loading,
   fetch_data
}) => {
  if(loading) return (<div>loading...</div)
  if(err) return (<div>error:{err}</div>)
  return <div onClick={fetch_data}>data:{data}</div>
}

複製代碼

代碼主要有三部分組成
model: 業務邏輯(狀態及狀態變化)
hooks: 根據 model 生成 useLoding hooks,實際控制的是從何處去獲取狀態
view: 使用根據 useLoading hooks 的返回的 state 和 action 進行渲染

這樣咱們的代碼組織就比較清晰,不太可能出現以前 hook 出現的混亂的狀況了

重要的是 model 而非 local 或者全局

咱們發現至此咱們組件不管是 local state 仍是全局 state,寫法幾乎一致了,都是劃分爲了 modle 和 view,區別只在於狀態是存在全局仍是 local,若是咱們全局和 local 的 model 定義徹底一致,那麼將很容易實現狀態全局和 local 的切換,這實際上在業務中也比較常見,尤爲是在 spa 裏,剛開始某個頁面裏的狀態是 local 的,可是後來新加了個頁面,須要和這個頁面共享狀態,咱們就須要將這個狀態和新頁面共享,這裏能夠先將狀態提高至兩個頁面的公共父頁面裏(經過 Context), 或者直接提取到全局。因此此時對於組件,差異僅僅在於咱們的狀態從何讀取而已。
咱們經過 hook 就隔離了這種區別,當咱們須要將狀態切換至全局或者 context 或者 local 時並不須要修改 model,僅僅需修改讀取的 hook 便可

// hook.ts
import model from './model';
const useLocalLoading = createLocalStore(model); // 從local讀取狀態
const useConextLoading = createContextStore(model); // 從context讀取狀態
const useGlobalLoading = createStore(model); // 從redux裏讀取狀態

// loading.ts
export default ()  => {
  const [state, actions] = useLocalLoading(); // 這裏能夠選用從何處讀取狀態
  return <Loading {...state} {...actions} />
}

複製代碼

此時咱們的組件不管是狀態複用、UI 複用、仍是代碼組織上都達到了比較合理的水平,mobx 裏實際上已經採用了相似作法

依賴注入

咱們在編寫 model 的過程當中,effects 裏不可避免的須要調用 service 來獲取數據,這致使了咱們的 model 直接依賴了 service,這通常不會出現問題,可是當咱們作同構或者時就會出現問題。
由於瀏覽器端和服務端以及測試端的 service 差異很大,如瀏覽器端的 service 一般是 http 請求,而服務端的 service 則有多是 rpc 服務,且調用過程當中須要打日誌和一些 trace 信息而測試端多是一些 mock 的 http 服務。這致使了若是 model 直接依賴於 service 將沒法構建通用於服務端和瀏覽器端的 model,更好的處理方式應該是將 service 經過依賴注入的方式注入到 model,在建立 strore 的時候將 service 實際的進行注入

上面說的這些問題包括 Typescript 支持、Batteries Included、localStore 的支持、依賴注入的支持等,rematch| dva 等庫受限於歷史緣由,都不太可能支持,很幸運的是 github.com/ctrlplusb/e… 對上述均作了很好的支持。具體例子可參考 github.com/hardfist/ha…

easy-peasy 簡介

disclaimer: 我和這庫沒啥關係,只是發現很符合個人需求,因此推薦一下
easy-peasy 的使用方式和 rematch 類似,但區別於 rematch 缺少對 hook 的內置支持(雖然也能支持 react-redux 的 hook 用法),且須要兼容 react-redux 的寫法,
easy-peasy 內置了對 hook 的支持且並不依賴 react-redux,而僅僅是對 react-redux 的用法作簡單兼容,致使了其能夠擺脫 rematch 現存的種種問題。

typescript 的 first class 支持

9102 年了,對 typescript 的支持對於一個庫應該成了基本需求,easy-peasy 很好的作到了這一點,其專門爲 TS 設計了一套 API,用於解決 TS 的支持問題 (內部使用了 ts-boolbelt 來解決類型推斷問題),簡單的使用 TS 定義一個 model 以下

export interface TodosModel {
  todo_list: Item[]; // state
  filter: FILTER_TYPE; // 同上
  init: Action<TodosModel, Item[]>; // 同步action
  addTodo: Action<TodosModel, string>; // 同上
  setFilter: Action<TodosModel, FILTER_TYPE>; // 同上
  toggleTodo: Action<TodosModel, number>;
  addTodoAsync: Thunk<TodosModel, string>; // 異步
  fetchTodo: Thunk<TodosModel, undefined, Injections>; // 異步並進行service的依賴注入
  visible_todo: Computed<TodosModel, Item[]>; // computed state
}

複製代碼

定義好 model 的結構後,咱們在編寫 model 時藉助於 contextual typing 能夠享受到自動補全和類型檢查的功能了

業務中使用 model 也再也不是經過 HOC 的方式經過 connect 來讀取 state 和 action,而是直接經過內置的 hook 來解決狀態讀取問題,避免了對 connect 的類型兼容問題(rematch 對這裏的兼容很坑爹), 且保證了類型安全

內置 computed 和 immer

區別於 rematch,easy-peasy 經過 immer 實現了對 immutable 的支持,同時內置了對 computed state 的支持,簡化了咱們業務的編寫

export const todo: TodosModel = {
  todo_list: [
    {
      text: 'learn easy',
      id: nextTodoId++,
      completed: false
    }
  ],
  filter: 'SHOW_ALL' as FILTER_TYPE,
  init: action((state, init) => {
    state.todo_list = init;
  }),
  addTodo: action((state, text) => {
    // 看似mutable,實際是immutable,經過immer實現了經過mutable的寫法,來實現了immutable結構
    state.todo_list.push({
      text,
      id: nextTodoId++,
      completed: false
    });
  }),
  setFilter: action((state, filter) => {
    state.filter = filter;
  }),
  toggleTodo: action((state, id) => {
    const item = state.todo_list.filter(x => x.id === id)[0];
    item.completed = !item.completed;
  }),
  addTodoAsync: thunk(async (actions, text) => {
    await delay(1000);
    actions.addTodo(text);
  }),
  fetchTodo: thunk(async function test(actions, payload, { injections }) {
    const { get_todo_list } = injections;
    const {
      data: { todo_list }
    } = await get_todo_list();
    actions.init(todo_list);
  }),
  // 內置對computed的支持
  visible_todo: computed(({ todo_list, filter }) => {
    return todo_list.filter(x => {
      if (filter === 'SHOW_ALL') {
        return true;
      } else if (filter === 'SHOW_COMPLETED') {
        return x.completed;
      } else {
        return !x.completed;
      }
    });
  })
};

複製代碼

一樣的方式編寫 local 和全局的 state

easy peasy 的 model 定義不只適用於全局,也適用於 context 和 local,只須要經過 hook 進行切換便可

export const ContextCounter = () => {
  const [state, actions] = useContextCounter();
  return renderCounter(state, actions);
};
export const LocalCounter = () => {
  const [state, actions] = useLocalCounter();
  return renderCounter(state, actions);
};
export const ReduxCounter = () => {
  const [state, actions] = useReduxCounter();
  return renderCounter(state, actions);
};

複製代碼

依賴注入支持

easy peasy 同時經過 thunk 實現了依賴注入,且保證了依賴注入的類型安全

  • 構造 store 時注入 service
// src/store/index.ts
import {get_todo_list } from 'service'
export interface Injections {
  get_todo_list: typeof get_todo_list;
} //定義注入的類型,供後續使用

export const store = createStore(models, {
  injections: { // 注入service
    get_todo_list
  }
});

複製代碼
  • 定義 model 時,聲明要注入的類型
import { Injections } from '../store';
// 導入須要注入的類型

export interface TodosModel {
  items: string[];
  addTodo: Action<TodosModel, string>;
  saveTodo: Thunk<TodosModel, string, Injections>; // 類型注入
}

複製代碼
  • 使用注入的 service, 這裏是類型安全的

相關文章
相關標籤/搜索