手寫一個mini版本的React狀態管理工具
手寫一個mini版本的React狀態管理工具
目前在React中,有很多各式各樣的狀態管理工具,如:
每一個狀態管理工具都有著不盡相同的API和使用方式,而且都有一定的學習成本,而且這些狀態管理工具也有一定的複雜度,並沒有做到極致的簡單。在開發者的眼中,只有用起來比較簡單,那麼才會有更多的人去使用它,Vue不就是因為使用簡單,上手快,而流行的嗎?
有時候我們只需要一個全域性的狀態,防治一些狀態和更改狀態的函式就足夠了,這樣也達到了最簡化原則。
下面讓我們一起來實現一個最簡單的狀態管理工具吧。
這個狀態管理工具的核心就使用到了Context API,在瞭解本文之前務必先了解並熟悉使用這個API的用法。
首先我們來看這個狀態管理工具是如何使用的。假設有一個計數器狀態,然後我們通過二個方法分別去修改計數器,也就是做加法和減法,換句話說我們需要用到一個計數器狀態,二個方法來修改這個狀態。在React函式元件中,我們使用useState方法來初始化一個狀態,因此,我們可以很容易的寫出如下程式碼:
import { useState } from 'react' const useCounter = (initialCount = 0) => { const [count,setCount] = useState(initialCount); const increment = () => setCount(count + 1); const decrement = () => setCount(count - 1); return { count, increment, decrement } } export default useCounter;
現在,讓我們建立一個元件來使用這個useCounter鉤子函式,如下:
import React from 'react' import useCounter from './useCounter' const Counter = () => { const { count,increment,decrement } = useCounter(); return ( <div className="counter"> { count } <button type="button" onClick={increment}>add</button> <button type="button" onClick={decrement}>plus</button> </div> ) }
然後在根元件App當中使用,如下:
import React from 'react' const App = () => { return ( <div className="App"> <Counter /> <Counter /> </div> ) }
這樣,一個計數器元件就大功告成了,可是真的只是這樣嗎?
首先,我們應該知道計數器元件的狀態應該是一致的,也就是說我們的計數器元件應該是共享同一個狀態,那麼如何共享同一個狀態?這時候就需要Context出場了。將以上的元件改造一下,我們將狀態放在根元件App當中初始化,並且傳到子元件中去。先修改App根元件的程式碼如下:
新建一個CounterContext.ts檔案,程式碼如下:
const CounterContext = createContext(); export default CounterContext;
import React,{ createContext } from 'react' import CounterContext from './CounterContext' const App = () => { const { count,increment,decrement } = useCounter(); return ( <div className="App"> <CounterContext.Provider value={{count,increment,decrement}}> <Counter /> <Counter /> </CounterContext.Provider> </div> ) }
然後在Counter元件程式碼我們也修改如下:
import React,{ useContext } from 'react' import CounterContext from './CounterContext' const Counter = () => { const { count,increment,decrement } = useContext(CounterContext); return ( <div className="counter"> { count } <button type="button" onClick={increment}>add</button> <button type="button" onClick={decrement}>plus</button> </div> ) }
如此一來,我們就可以共享count狀態,無論是在多深的子元件當中使用都沒有問題,但是這並沒有結束,讓我們繼續。
雖然這樣使用解決了共享狀態的問題,可是我們發現,我們在使用的時候還要額外的傳入一個context名,所以我們需要包裝一下,到最後,我們只需要像如下這樣使用:
const Counter = createModel(useCounter); export default Counter;
const { Provider,useModel } = Counter;
然後我們的App元件就應該是這樣:
import React,{ createContext } from 'react' import counter from './Counter' const App = () => { const { Provider } = counter; return ( <div className="App"> <Provider> <Counter /> <Counter /> </Provider> </div> ) }
繼續修改我們的Counter元件,如下:
import React,{ useContext } from 'react' import counter from './Counter' const Counter = () => { const { count,increment,decrement } = counter.useModel(); return ( <div className="counter"> { count } <button type="button" onClick={increment}>add</button> <button type="button" onClick={decrement}>plus</button> </div> ) }
通過以上程式碼的展示,其實我們也就明白了,我們無非是將useContext和createContext內建到我們封裝的Model裡面去了。
接下來我們就來揭開這個狀態管理工具的神祕面紗,首先要用到React相關的API,所以我們需要匯入進來。如下:
// 匯入型別 import type { ReactNode,ComponentType } from 'react'; import { createContext,useContext } from 'react';
接下來定義一個唯一標識,用於確定傳入的Context,並且這個用來確定使用者使用Context時是正確使用的。
const EMPTY:unique symbol = Symbol();
接下來我們要定義Provider的型別。如下:
export interface ModelProviderProps<State = void> { initialState?: State children: ReactNode }
以上我們定義了context的狀態型別,是一個泛型,引數就是狀態的型別,預設初始化為undefined型別,並且定義了一個children的型別,因為Provider的子節點是一個React節點,所以也就定義成ReactNode型別。
然後就是我們的Model型別,如下:
export interface Model<Value,State = void> { Provider: ComponentType<ModelProviderProps<State>> useModel: () => Value }
這個也很好理解,因為Model暴露了兩個東西,第一個是Provider,第二個就是useContext,只是換了一個名字而已,定義這兩個的型別就夠了。
接下來就是我們的核心函式createModel函式的實現,我們一步一步來,首先當然是定義這個函式,注意型別,如下:
export const createModel = <Value,State = void>(useHook:(initialState?:State) => Value): Model<Value,State> => { //核心程式碼 }
以上函式難以理解的應該是型別的定義,我們createModel函式傳入一個hook函式,hook函式傳入一個狀態作為引數,然後返回值就是我們定義好的Model泛型,引數為型別就是我們定義好的這個函式的泛型。
接下來,我們要做的自然是建立一個context,如下:
//建立一個context const context = createContext<Value | typeof EMPTY>(EMPTY);
然後我們要建立一個Provider函式,本質上也是一個React元件,如下:
const Provider = (props:ModelProviderProps<State>) => { // 這裡使用ModelProvider主要是不能和定義的函式名起衝突 const { Provider:ModelProvider } = context; const { initialState,children } = props; const value = useHook(initialState); return ( <ModelProvider value={value}>{children}</ModelProvider> ) }
這裡也很好理解,實際上就是通過父元件拿到初始狀態和子節點,從context中拿到Provider元件,然後返回即可,注意我們的value是通過傳入的自定義hook函式包裝後的值。
第三步,我們就需要定義一個hook函式拿到這個自定義的Context,如下:
const useModel = ():Value => { const value = useContext(context); // 這裡確定一下使用者是否正確使用Provider if(value === EMPTY){ //丟擲異常,使用者並沒有用Provider包裹元件 throw new Error('Component must be wrapped with <Container.Provider>'); } // 返回context return value; }
這個函式的實現也很好理解,就是獲取context,判斷context是否正確使用,然後返回。
最後我們在這個函式內部返回這2個東西,即返回Provider和useModel兩個函式。如下:
return { Provider,useModel }
把以上程式碼全部合併起來,createModel函式就大功告成啦。
最後,我們把所有程式碼合併起來,這個狀態管理工具也就完成了。
// 匯入型別 import type { ReactNode,ComponentType } from 'react'; import { createContext,useContext } from 'react'; const EMPTY:unique symbol = Symbol(); export interface ModelProviderProps<State = void> { initialState?: State children: ReactNode } export interface Model<Value,State = void> { Provider: ComponentType<ModelProviderProps<State>> useModel: () => Value } export const createModel = <Value,State = void>(useHook:(initialState?:State) => Value): Model<Value,State> => { //建立一個context const context = createContext<Value | typeof EMPTY>(EMPTY); // 定義Provider函式 const Provider = (props:ModelProviderProps<State>) => { const { Provider:ModelProvider } = context; const { initialState,children } = props; const value = useHook(initialState); return ( <ModelProvider value={value}>{children}</ModelProvider> ) } // 定義useModel函式 const useModel = ():Value => { const value = useContext(context); // 這裡確定一下使用者是否正確使用Provider if(value === EMPTY){ //丟擲異常,使用者並沒有用Provider包裹元件 throw new Error('Component must be wrapped with <Container.Provider>'); } // 返回context return value; } return { Provider,useModel }; }
更近一步,我們再匯出一個useModel函式,如下:
export const useModel = <Value,State = void>(model:Model<Value,State>):Value => { return model.useModel(); }
到目前為止,我們的整個狀態管理工具就完成啦,使用起來也很簡單,很多輕量的共享狀態專案當中我們也就再也不需要使用像Redux這樣的比較複雜的狀態管理工具了。
當然這個想法也並不是我本人想的,文末已註明來源,本文對原始碼做了一遍分析。
原始碼地址 。
PS: 本文原始碼來自unstated-next。
- SegmentFault 2022 年社群週報 Vol.9
- 社群精選 | 不容錯過的9個冷門css屬性
- 2022最新版 Redis大廠面試題總結(附答案)
- 手寫一個mini版本的React狀態管理工具
- 【vue3原始碼】十三、認識Block
- 天翼雲全場景業務無縫替換至國產原生作業系統CTyunOS!
- JavaScript 設計模式 —— 代理模式
- MobTech簡訊驗證ApiCloud端SDK
- 以羊了個羊為例,淺談小程式抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 聊聊如何利用管道模式來進行業務編排(下篇)
- 通用ORM的設計與實現
- 如此狂妄,自稱高效能佇列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 列舉」運用題
- 介紹 Preact Signals
- 手把手教你如何使用 Timestream 實現物聯網時序資料儲存和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發程式設計解析 | 基於JDK原始碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 令人困惑的 Go time.AddDate