使用TypeScript開發React應用(五) - 元件狀態管理
繼續前面的文章ofollow,noindex" target="_blank">使用TypeScript開發React應用(四) 介紹了React+TypeScript應用的Jest單元測試
下面繼續分享如何給元件新增狀態管理
新增狀態管理
此時,如果您使用React只獲取一次資料並顯示它,您可以認為自己完成了。 但是,如果您正在開發一個更具互動性的應用程式,那麼您可能需要新增狀態管理。
一般的狀態管理
React本身就是一個用於建立可組合檢視的有用庫。 但是,React沒有規定在整個應用程式中同步資料的任何特定方法。 就React元件而言,資料通過您在子元素上指定的道具向下流動。 其中一些道具可能是以這種或那種方式更新狀態的功能,但是如何發生這是一個懸而未決的問題。
由於React本身並不專注於應用程式狀態管理,因此React社群使用Redux和MobX等庫。
Redux 依賴於通過集中且不可變的資料儲存來同步資料,對該資料的更新將觸發我們的應用程式的重新呈現。 通過傳送必須由稱為reducers的函式處理的顯式操作訊息,以不可變的方式更新狀態。 由於具有明確的性質,通常更容易推斷某個操作將如何影響您的程式狀態。
MobX 依賴於功能反應模式,其中狀態通過可觀察物件包裹並作為道具傳遞。 通過簡單地將狀態標記為可觀察狀態來保持狀態完全同步以用於任何觀察者。 作為一個很好的獎勵,該庫已經用TypeScript編寫。
兩者都有各種優點和權衡。 通常Redux傾向於看到更廣泛的用法,因此為了本教程的目的,我們將專注於新增Redux; 但是,你應該感到鼓勵去探索兩者。
以下部分可能有一個陡峭的學習曲線。 我們強烈建議您通過其文件熟悉Redux 。
為行動做準備
除非我們的應用程式狀態發生變化,否則新增Redux是沒有意義的。 我們需要一個可以觸發更改的操作源。 這可以是計時器,也可以是UI中的某個按鈕。
出於我們的目的,我們將新增兩個按鈕來控制Hello元件的enthusiasmLevel。
安裝Redux
要新增Redux,我們首先將redux和react-redux及其型別安裝為依賴項。
npm install -S redux react-redux @types/react-redux
在這種情況下,我們不需要安裝@types/redux,因為Redux已經附帶了自己的定義檔案(.d.ts檔案)。
定義我們的應用程式狀態
我們需要定義Redux將儲存的狀態的形狀。 為此,我們可以建立一個名為src/types/index.tsx的檔案,該檔案將包含我們可能在整個程式中使用的型別的定義。
export interface StoreState { enthusiasmLevel: number; name: string; }
我們的意圖是enam將是這個應用程式名稱,而enthusiasmLevel的管理將會有所不同。 當我們編寫第一個容器時,我們會理解為什麼我們故意使我們的狀態與我們的道具略有不同。
新增actions
讓我們從建立一組訊息型別開始,我們的應用程式可以在src/constants/index.tsx中響應。
export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM'; export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM; export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM'; export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;
這種const/type模式允許我們以易於訪問和可重構的方式使用TypeScript的字串文字型別。
接下來,我們將建立一組可以在src/actions/index.tsx中建立這些操作的操作和函式。
import * as constants from '../constants'; export interface IncrementEnthusiasm { type: constants.INCREMENT_ENTHUSIASM; } export interface DecrementEnthusiasm { type: constants.DECREMENT_ENTHUSIASM; } export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm; export function incrementEnthusiasm(): IncrementEnthusiasm { return { type: constants.INCREMENT_ENTHUSIASM, } } export function decrementEnthusiasm(): DecrementEnthusiasm { return { type: constants.DECREMENT_ENTHUSIASM, } }
我們建立了兩種型別來描述增量操作和減量操作應該是什麼樣子。 我們還建立了一個型別(EnthusiasmAction)來描述一個動作可以是增量或減量的情況。 最後,我們製作了兩個函式來實際製作我們可以使用的動作,而不是寫出龐大的物件文字。
這裡有明顯的樣板,所以一旦你掌握了一些東西,你就可以隨意檢視像redux-actions 這樣的庫。
新增一個reducer
我們準備好寫第一個reducer了! Reducers只是通過建立應用程式狀態的修改副本來生成更改的函式,但沒有副作用。 換句話說,它們就是我們所謂的純函式。
我們的reducer將在src/reducers/index.tsx下。 它的功能是確保增量將enthusiasmLevel提高1,而減量將enthusiasmLevel降低1,但水平從不低於1。
import { EnthusiasmAction } from '../actions'; import { StoreState } from '../types/index'; import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index'; export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState { switch (action.type) { case INCREMENT_ENTHUSIASM: return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1, }; case DECREMENT_ENTHUSIASM: return { ...state, enthusiasmLevel: Math.max(state.enthusiasmLevel - 1, 1), }; } return state; }
請注意,我們正在使用物件展開(...state)
,它允許我們建立狀態的淺賦值,同時替換enthusiasmLevel。 重要的是,enthusiasmLevel屬性是最後的,否則它將被舊狀態的屬性覆蓋。
您可能想為reducers編寫一些測試。 由於reducer是純函式,因此可以傳遞任意資料。 對於每個輸入,可以通過檢查其新生成的狀態來測試reducers。 考慮一下Jest的toEqual方法來實現這一點。
使用Redux編寫時,我們經常會編寫元件和容器。 元件通常與資料無關,並且主要在表示級別工作。 容器通常包裝元件並向其提供顯示和修改狀態所需的任何資料。 您可以在Dan Abramov的文章演示和容器元件上 閱讀有關此概念的更多資訊。
首先讓我們更新src/components/Hello.tsx
,以便它可以修改狀態。 我們將為名為onIncrement和onDecrement的Props新增兩個可選的回撥屬性:
export interface Props { name: string; enthusiasmLevel?: number; onIncrement?: () => void; onDecrement?: () => void; }
然後我們將這些回撥繫結到兩個我們將新增到元件中的新按鈕。
function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) { if (enthusiasmLevel <= 0) { throw new Error('You could be a little more enthusiastic.'); } return ( <div className="hello"> <div className="greeting"> Hello {name + getExclamationMarks(enthusiasmLevel)} </div> <div> <button onClick={onIncrement}>+</button> <button onClick={onDecrement}>-</button> </div> </div> ); }
一般來說,為onIncrement編寫一些測試並在單擊各自的按鈕時觸發onDecrement是個好主意。 試一試為您的元件編寫測試。
現在我們的元件已更新,我們已準備好將其包裝到容器中。 讓我們建立一個名為src/containers/Hello.tsx的檔案,並從以下匯入開始。
import Hello from '../components/Hello'; import * as actions from '../actions/'; import { StoreState } from '../types'; import { connect } from 'react-redux'; import { Dispatch } from 'redux';
這裡真正的兩個關鍵部分是原始的Hello元件以及react-redux的connect函式。 connect將能夠實際使用我們的原始Hello元件並使用兩個函式將其轉換為容器:
- mapStateToProps,用於傳遞當前Sore中的資料到我們元件所需形狀的一部分。
- mapDispatchToProps建立回撥屬性,使用給定的排程函式將操作傳送到我們的store。
如果我們回想一下,我們的應用程式狀態包含兩個屬性:name和enthusiasmLevel。 另一方面,我們的Hello元件需要一個名字和一個enthusiasmLevel的管子。 mapStateToProps將從store獲取相關資料,並在必要時針對我們元件的屬性進行調整。 讓我們繼續寫下來吧。
export function mapStateToProps({ enthusiasmLevel, name }: StoreState) { return { enthusiasmLevel, name, } }
請注意,mapStateToProps僅建立Hello元件期望的4個屬性中的2個。 也就是說,我們仍然希望傳入onIncrement和onDecrement回撥。 mapDispatchToProps是一個採用排程程式功能的函式。 此排程程式功能可以將操作傳遞到我們的store以進行更新,因此我們可以建立一對將根據需要呼叫排程程式的回撥。
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) { return { onIncrement: () => dispatch(actions.incrementEnthusiasm()), onDecrement: () => dispatch(actions.decrementEnthusiasm()), } }
最後,我們準備呼叫connect。 connect將首先獲取mapStateToProps和mapDispatchToProps,然後返回另一個我們可以用來包裝我們元件的函式。 我們生成的容器使用以下程式碼行定義:
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
完成後,我們的檔案應如下所示:
import Hello from '../components/Hello'; import * as actions from '../actions/'; import { StoreState } from '../types/index'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; export function mapStateToProps({ enthusiasmLevel, name }: StoreState) { return { enthusiasmLevel, name, } } export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) { return { onIncrement: () => dispatch(actions.incrementEnthusiasm()), onDecrement: () => dispatch(actions.decrementEnthusiasm()), } } export default connect(mapStateToProps, mapDispatchToProps)(Hello);
建立store我們回到src/index.tsx。 要把這些放在一起,我們需要建立一個具有初始狀態的store,並使用我們所有的reducer進行設定。
import { createStore } from 'redux'; import { enthusiasm } from './reducers/index'; import { StoreState } from './types/index'; import { EnthusiasmAction } from './actions/index'; const store = createStore<StoreState, EnthusiasmAction, any, any>(enthusiasm, { enthusiasmLevel: 1, name: 'Durban', });
正如您可能已經猜到的那樣,store是我們應用程式全域性狀態的中央store。
接下來,我們將把./src/components/Hello與./src/containers/Hello交換使用,並使用react-redux的Provider將我們的道具與我們的容器連線起來。 我們將匯入每個:
-import Hello from './components/Hello'; +import Hello from './containers/Hello'; import { Provider } from 'react-redux';
並將我們的store傳遞給Provider的屬性:
ReactDOM.render( <Provider store={store}> <Hello /> </Provider>, document.getElementById('root') as HTMLElement );
請注意,Hello不再需要props,因為我們使用connect函式來調整我們的應用程式狀態,以便包裝Hello元件的props。
完整程式碼請跳轉到這裡下載ts-react-app