大家好,我是若川。持續組織了近一年的源碼共讀活動,感興趣的可以?點此掃碼加我微信?ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北
籍前端群,可加我微信進群。
在本文中,我們將了解在 React 應用程序中管理狀態的多種方式。
我們將從討論什么是狀態開始,然后介紹許多用于管理狀態的工具。
我們將了解簡單的 useState hook,并學習更復雜的庫,如 Redux。然后我們將查看最新可用的庫,例如 Recoil 和 Zusand。
目錄
React 中的狀態是什么
如何使用 useState hook
如何使用 useEffect 讀取狀態更新
如何傳遞一個回調給狀態更新函數
管理規模和復雜性
React context
如何使用 useReducer hook
那么 Redux 呢
Redux 的替代品
提到 Redux Thunk 和 Redux Saga
Redux toolkit
Recoil
Jotai
Zustand
總結
React 中的狀態是什么
在現代 React 中,我們使用函數組件構建我們的應用程序。組件本身就是 JavaScript 函數,是獨立且可復用的代碼。
使用組件構建應用程序的目的是使其具有模塊化架構,具有明確的關注點分離。這使代碼更易于理解、更易于維護并且在可能的情況下更易于復用。
而狀態(state)是一個保存有組件信息的對象。普通 JavaScript 函數沒有存儲信息的能力。一旦執行完成,它們中的代碼就會執行并“消失”。
但是有了狀態之后,React 函數組件即使在執行后也可以存儲信息。當我們需要一個組件來存儲或“記住”某些東西,或者根據環境以不同的方式執行時,狀態就是我們所需要的可以讓這些生效的東西。
值得一提的是,在 React 應用程序中的并非所有組件都必須具有狀態,也有無狀態組件,它們只呈現其內容而無需存儲任何信息,這也很好。
另一件重要的事情是狀態變化是使 React 組件重新渲染的兩個原因之一(另一個是 props 的變化)。因此,狀態存儲了組件的信息同時也控制了它的行為。
如何使用 useState hook
為了在我們的組件中實現狀態,React 為我們提供了一個名為 useState 的鉤子(hook)。讓我們看看它是如何與以下示例一起工作的。
我們將使用經典的計數器示例,其中我們將顯示一個數字,并且我們有幾個按鈕用于增加、減少或重置該數字。
這是一個很好的應用程序示例,我們需要存儲一條信息并在每次信息更改時呈現不同的內容。
此應用程序的代碼如下所示:
//?App.js
import?{?useState?}?from?'react'function?App()?{const?[count,?setCount]?=?useState(0)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?setCount(count+1)}>Add?1</button><button?onClick={()?=>?setCount(count-1)}>Decrease?1</button><button?onClick={()?=>?setCount(count+10)}>Add?10</button><button?onClick={()?=>?setCount(count-10)}>Decrease?10</button><button?onClick={()?=>?setCount(0)}>Reset?count</button></div></div>)
}export?default?App
首先我們從 React 導入鉤子(hook):
import { useState } from 'react'
然后我們初始化狀態:
const [count, setCount] = useState(0)
在這里,我們為狀態提供了一個變量名(count
)和一個我們將在每次需要更新該狀態時使用的函數名(setCount
)。最后,我們設置狀態的初始值(0
),這將是應用程序每次啟動時默認加載的值。
最后,如上所述,每次我們想要更新狀態時,都必須使用我們聲明的函數:
setCount
,只需要調用它并將我們想要的新狀態作為參數傳遞給它。也就是說,如果我們想在前一個狀態加 1,我們需要調用setCount(count+1)
。
如前所述,這將導致狀態更新,從而導致組件的重新渲染。在我們的應用程序中我們將在屏幕上看到計數器增加。
如何使用 useEffect 讀取狀態更新
一個需要提到的重要信息是 setState 函數是異步的。因此,如果我們嘗試在更新狀態后立即讀取它,例如:
<button?onClick={()?=>?{setCount(count+1)console.log(count)
}}>Add?1</button>
我們會得到之前的狀態值,并沒有得到更新。
在更新狀態后讀取狀態的正確方法是使用 useEffect hook。它允許我們在每個組件重新渲染后(默認情況下)或在我們聲明更改的任何特定變量之后執行一個函數。
就像這樣:
useEffect(()?=>?console.log(value),?[value])
如何傳遞一個回調給狀態更新函數
在非常頻繁和快速的狀態變更時,異步的 useState 也會產生一些影響。
舉個例子,用戶連續多次按下 ADD 按鈕,或者一個循環中發出一定次數的點擊事件。
通過setCount(count+1)
更新狀態,在下一個事件被觸發時 count
會有不被更新的風險。
例如,假設在開始時 count = 0
,然后調用 setCount(count+1)
異步更新狀態。
但是在狀態更新完成之前再次調用了 setCount(count+1)
。這意味著仍然是count = 0
,這意味著第二個setCount
不會正確更新狀態。
一種比較防守型的方法是向 setCount
傳遞一個回調,如下所示:setCount(prevCount => prevCount+1)
。
這樣可以確保要更新的值是最新的,并使我們遠離上述問題。每次我們對先前的狀態執行更新時,我們都應該使用這種方法。
管理規模和復雜性
到目前為止,狀態管理似乎是小菜一碟。我們只需要一個 hook、一個值和一個函數來更新它,我們就可以開始了。
但是,一旦應用程序開始變得更大更復雜時,僅使用這一種方式可能會開始導致一些問題。
React context
第一個可能出現的問題是當我們有很多嵌套組件時,我們需要許多“兄弟”組件來共享相同的狀態。
顯而易見的答案是“提升”狀態,這意味著父組件將成為持有狀態的組件,并將狀態作為 prop 傳遞給子組件。
這很好用,但是當我們有很多嵌套組件時,可能需要通過許多層級組件傳遞 props。這被稱為 prop drilling,不僅很難看,而且會創建難以維護的代碼。
Prop drilling 還可能導致不必要的重新渲染,這可能會影響我們應用程序的性能。如果在我們的父組件(存儲狀態)和子組件(使用狀態)之間還有其他組件(“中間組件”),我們也需要通過這些中間組件傳遞 prop,即使它們并不需要 prop。
這意味著這些“中間組件”將在 prop 變更時重新渲染,即使它們沒有不同的內容需要渲染。
解決這個問題的一種方法是使用 React context,簡單來說,這是一種創建包裝組件的方法,該組件包裝我們那些想要并且可以直接傳遞 props 的組件組,而且無需 “drill” 通過那些不是必須使用該狀態的組件。
使用 context 時要注意的是,當 context 狀態發生變化時,所有接收該狀態的被包裝組件都將重新渲染。這種情況下,這可能不是必然的,也可能導致性能問題。
因此,我們是否真的需要讓一個狀態對許多組件可用,或者我們是否可以將其保持在單個組件中, 在這兩者之前取一個平衡是非常重要的。如果我們需要讓許多組件都可以使用它,把它放在 context 中真的是一個好主意嗎?或者我們是否可以把它提升一個層次?
Kent C Dodds 有一篇關于這個主題的很酷的文章。
如何使用 useReducer hook
當你使用 useState 時,要設置的新狀態取決于先前的狀態(如我們的計數示例),或者當我們的應用程序中狀態更改非常頻繁,這種情況下可能會出現另一個問題。
在這些情況下,useState 可能會引發一些意想不到的和不可預知的行為。接下來的 reducers 將解決這個問題。
reducer 是一個純函數,它將前一個狀態和一個動作作為參數,并返回下一個狀態。它被稱為 reducer,是因為它與你傳遞給數組的函數類型相同:Array.prototype.reduce(reducer, initialValue)
。
useReducer 是 React 提供的 hook,它讓我們實現 reducer 來管理狀態。使用這個 hook,我們之前的示例應用程序看起來像這樣:
//?App.js
import?{?useReducer?}?from?'react'
import?'./App.scss'function?App()?{function?reducer(state,?action)?{switch?(action.type)?{case?'ADD':?return?{?count:?state.count?+?1?}case?'SUB':?return?{?count:?state.count?-?1?}case?'ADD10':?return?{?count:?state.count?+?10?}case?'SUB10':?return?{?count:?state.count?-?10?}case?'RESET':?return?{?count:?0?}default:?return?state}}const?[state,?dispatch]?=?useReducer(reducer,?{?count:?0?})??return?(<div?className="App"><p>Count?is:?{state.count}</p><div><button?onClick={()?=>?dispatch({type:?'ADD'})}>Add?1</button><button?onClick={()?=>?dispatch({type:?'SUB'})}>Decrease?1</button><button?onClick={()?=>?dispatch({type:?'ADD10'})}>Add?10</button><button?onClick={()?=>?dispatch({type:?'SUB10'})}>Decrease?10</button><button?onClick={()?=>?dispatch({type:?'RESET'})}>Reset?count</button></div></div>)
}export?default?App
我們再次從 React 導入 hook 開始:
import { useReducer } from 'react'
然后我們將聲明一個 reducer 函數,將接收當前的狀態和對其執行的動作(action)作為參數。并且在函數里有一個 switch 語句,該語句將讀取動作類型,對狀態執行相應的動作,并返回更新后的狀態。
通常做法是在 reducer 上使用 switch 語句, 并且使用大寫字母來聲明動作。
function?reducer(state,?action)?{switch?(action.type)?{case?'ADD':?return?{?count:?state.count?+?1?}case?'SUB':?return?{?count:?state.count?-?1?}case?'ADD10':?return?{?count:?state.count?+?10?}case?'SUB10':?return?{?count:?state.count?-?10?}case?'RESET':?return?{?count:?0?}default:?return?state}}
之后,是時候聲明我們的 useReducer hook 了,它看起來與 useState hook 非常相似。我們為我們的狀態聲明一個變量(在我們的例子中是'state'),和一個我們將用來修改這個變量的函數('dispatch'),然后 useReducer 將接收上面的 reducer 函數 作為第一個參數,和一個默認狀態作為第二個參數。
const?[state,?dispatch]?=?useReducer(reducer,?{?count:?0?})
最后,我們不會直接調用 reducer 去更新狀態,而是調用我們剛剛創建的函數('dispatch'),將我們想要執行的對應的動作類型傳遞給它。其中,dispatch 函數將與 reducer 連接并實際修改 state。
<button?onClick={()?=>?dispatch({type:?'ADD'})}>Add?1</button>
這比使用 useState 多了不少模板,但useReducer畢竟沒有那么復雜。
總結一下,我們只需要:
一個 reducer,合并所有可能的狀態變化的函數
一個 dispatch 函數,將修改動作傳遞給 reducer
這里的問題是, UI 元素將不能像以前那樣通過用一個值調用 setState 去直接更新狀態。現在它們需要調用一個動作類型(action type)并通過 reducer,這使得狀態管理更加模塊化和可預測。
那么 Redux 呢
Redux 是一個已經存在很長時間并且在 React 中被廣泛使用的庫。
Redux 是一個工具,它可以解決前面提到的兩個問題(prop drilling 和頻繁和復雜狀態變更時不可預測的狀態行為)。
值得一提的是,Redux 是一個不可知的庫,這意味著它可以在任何前端應用程序上實現,不僅僅是 React。
Redux 工具集與我們剛剛看到的 useReducer 非常相似,但多了一些東西。Redux 中有三個主要的構建塊:
store — 一個保存應用狀態數據的對象
reducer — 一個由動作類型(action type)觸發,并返回一些狀態數據的函數
action — 一個告訴 reducer 如何改變狀態的對象,它必須包含一個 type 屬性,并且它還可以包含一個可選的 payload 屬性
實現 Redux,我們的示例應用程序如下所示:
//?App.js
import?'./App.scss'import?{?Provider,?useSelector,?useDispatch?}?from?'react-redux'
import?{?addOne,?subOne,?addSome,?subSome,?reset?}?from?'./store/actions/count.actions'import?store?from?'./store'function?App()?{const?dispatch?=?useDispatch()const?count?=?useSelector(state?=>?state.count)return?(<Provider?store={store}><div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?dispatch(addOne())}>Add?1</button><button?onClick={()?=>?dispatch(subOne())}>Decrease?1</button><button?onClick={()?=>?dispatch(addSome(10))}>Add?10</button><button?onClick={()?=>?dispatch(subSome(10))}>Decrease?10</button><button?onClick={()?=>?dispatch(reset())}>Reset?count</button></div></div></Provider>)
}export?default?App
此外,我們需要一個新的 store 文件夾,包含相應的 store、reducer 和 actions 文件。
//?index.js?(STORE)
import?{?createStore?}?from?'redux'
import?CountReducer?from?'./reducers/count.reducer'export?default?createStore(CountReducer)
//?count.reducer.js
import?{?ADD,?SUB,?ADDSOME,?SUBSOME,?RESET?}?from?'../actions/count.actions'const?CountReducer?=?(state?=?{?count:?0?},?action)?=>?{switch?(action.type)?{case?ADD:?return?{?count:?state.count?+?1?}case?SUB:?return?{?count:?state.count?-?1?}case?ADDSOME:?return?{?count:?state.count?+?action.payload?}case?SUBSOME:?return?{?count:?state.count?-?action.payload?}case?RESET:?return?{?count:?0?}default:?return?state}
}export?default?CountReducer
//?count.actions.js
export?const?ADD?=?'ADD'
export?const?addOne?=?()?=>?({?type:?ADD?})export?const?SUB?=?'SUB'
export?const?subOne?=?()?=>?({?type:?SUB?})export?const?ADDSOME?=?'ADDSOME'
export?const?addSome?=?(value)?=>?({type:?ADDSOME,payload:?value
})export?const?SUBSOME?=?'SUBSOME'
export?const?subSome?=?(value)?=>?({type:?SUBSOME,payload:?value
})export?const?RESET?=?'RESET'
export?const?reset?=?()?=>?({?type:?RESET?})
這比我們之前看到的還要更多的模板(這也是 Redux 被批評的主要原因),所以讓我們把它分解成幾塊:
正如我提到的,Redux 是一個外部庫,所以在進行任何操作之前,我們需要通過運行
npm i redux react-redux
來安裝它。redux
將帶來管理狀態所需的核心函數,而react-redux
將安裝一些很酷的 hook,可以輕松地從我們的組件中讀取和修改狀態。現在,首先是 store。在 Redux 中,store 是擁有所有應用程序狀態信息的實體。多虧 Redux,我們能夠從任何想要的組件中訪問 store(就像使用 context 一樣)。
為了創建一個 store,我們導入 createStore
函數,并將一個 reducer 函數作為輸入傳遞給它。
要知道,你也可以將不同的 reducers 合并然后傳遞給同一個 store,這樣你就可以將關注點分離到不同的 reducers 中。
import?{?createStore?}?from?'redux'
import?CountReducer?from?'./reducers/count.reducer'export?default?createStore(CountReducer)
然后是 reducer,它的工作方式與之前我們看到的 useReducer 完全相同。它接收默認狀態和一個動作(action)作為參數,然后在它里面有一個 switch 語句來讀取 action type,執行相應的狀態修改,并返回更新后的狀態。
import?{?ADD,?SUB,?ADDSOME,?SUBSOME,?RESET?}?from?'../actions/count.actions'const?CountReducer?=?(state?=?{?count:?0?},?action)?=>?{switch?(action.type)?{case?ADD:?return?{?count:?state.count?+?1?}case?SUB:?return?{?count:?state.count?-?1?}case?ADDSOME:?return?{?count:?state.count?+?action.payload?}case?SUBSOME:?return?{?count:?state.count?-?action.payload?}case?RESET:?return?{?count:?0?}default:?return?state}
}export?default?CountReducer
接著是 actions。actions 用于告訴 reducer 如何更新狀態。在代碼中,你可以看到,對于每個 action,我們都聲明了常量來代替普通的字符串(這是一個可以提高可維護性的好做法),以及一些僅返回一個 type 或者 一個 type 和一個 payload 的函數。這些函數就是我們要從組件中 dispatch 去更改狀態的函數。
請注意,我對這個例子做了一些改變,以顯示在談論 actions 時 payload 的含義。如果我們想在 dispatch action 時從組件傳遞一個參數,payload 就是存放該信息的地方。
在示例中,你可以看到我們在調用 ADDSOME/SUBSOME 時可以直接從組件中傳遞我們想要加/減的數字。
export?const?ADD?=?'ADD'
export?const?addOne?=?()?=>?({?type:?ADD?})export?const?SUB?=?'SUB'
export?const?subOne?=?()?=>?({?type:?SUB?})export?const?ADDSOME?=?'ADDSOME'
export?const?addSome?=?value?=>?({type:?ADDSOME,payload:?value
})export?const?SUBSOME?=?'SUBSOME'
export?const?subSome?=?value?=>?({type:?SUBSOME,payload:?value
})export?const?RESET?=?'RESET'
export?const?reset?=?()?=>?({?type:?RESET?})
最后是我們的組件。這里有 3 件事需要注意:
首先,我們有一個 provider 組件,它接收 store 作為 props。這是對所有被包裝在其中的組件訪問 store 的授權。
然后我們有一個名為 **useDispatch()**(我們將用于 dispatch actions)和另一個名為 useSelector() 的 hook(我們將用于從 store 中讀取狀態)。
最后,請注意我們將要 dispatch 我們在 action 文件中聲明的函數,并傳遞一個匹配的值作為輸入。這個值是 actions 接收作為 payload 的值,以及 reducer 將用來修改狀態的值。
import?'./App.scss'import?{?useSelector,?useDispatch?}?from?'react-redux'
import?{?addOne,?subOne,?addSome,?subSome,?reset?}?from?'./store/actions/count.actions'function?App()?{const?dispatch?=?useDispatch()const?count?=?useSelector(state?=>?state.count)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?dispatch(addOne())}>Add?1</button><button?onClick={()?=>?dispatch(subOne())}>Decrease?1</button><button?onClick={()?=>?dispatch(addSome(10))}>Add?10</button><button?onClick={()?=>?dispatch(subSome(10))}>Decrease?10</button><button?onClick={()?=>?dispatch(reset())}>Reset?count</button></div></div>)
}export?default?App
Redux 是一個很好的工具,它同時解決了兩個問題(prop drilling 和復雜的狀態變化)。不過,它確實產生了很多模板,使狀態管理成為一個更難理解的話題,特別是在處理不同的文件和實體,如 actions、reducers、store......
這里要提到的重要一點是,這些管理狀態的工具或方法并不是相互排斥的,它們可以而且可能應該同時使用,并各自解決它們所擅長的具體問題。
對于 Redux,要解決的問題是處理全局狀態(指影響整個應用程序或其中很大一部分的狀態)。用 Redux 來處理像我們的例子中的計數器或模態的打開和關閉是沒有意義的。
一個好的黃金法則是 useState 用于組件狀態,Redux 用于應用程序狀態。
Redux 的替代品
如果這個主題對你來說還不夠復雜,在過去的幾年里,出現了許多作為 Redux 替代品的新的庫,每個庫都有自己的狀態管理方法。
為了獲得很好的概述,讓我們快速了解它們。
Redux toolkit
Redux toolkit 是一個建立在 Redux 之上的庫,其目的是去除 Redux 產生的一些復雜性和模板。
Redux toolkit 基于兩件事:
store,它的工作方式與普通 Redux store 完全相同
slices 將普通的 Redux actions 和 reducer 壓縮成一個單一的東西
實現 Redux Toolkit,我們的示例應用程序如下所示:
//?App.js
import?'./App.scss'import?{?useSelector,?useDispatch?}?from?'react-redux'
import?{?addOne,?subOne,?addSome,?subSome,?reset?}?from?'./store/slices/count.slice'function?App()?{const?dispatch?=?useDispatch()const?count?=?useSelector(state?=>?state.counter.count)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?dispatch(addOne())}>Add?1</button><button?onClick={()?=>?dispatch(subOne())}>Decrease?1</button><button?onClick={()?=>?dispatch(addSome(10))}>Add?10</button><button?onClick={()?=>?dispatch(subSome(10))}>Decrease?10</button><button?onClick={()?=>?dispatch(reset())}>Reset?count</button></div></div>)
}export?default?App
//?index.js
import?React?from?'react'
import?ReactDOM?from?'react-dom'
import?App?from?'./App'
import?{?Provider?}?from?'react-redux'
import?store?from?'./store/index'ReactDOM.render(<React.StrictMode><Provider?store={store}><App?/></Provider></React.StrictMode>,document.getElementById('root')
)
//?Index.jsx?(STORE)
import?{?configureStore?}?from?'@reduxjs/toolkit'
import?counterReducer?from?'./slices/count.slice'export?const?store?=?configureStore({reducer:?{counter:?counterReducer},
})export?default?store
//?count.slice.jsx
import?{?createSlice?}?from?'@reduxjs/toolkit'const?initialState?=?{?count:?0?}export?const?counterSlice?=?createSlice({name:?'counter',initialState,reducers:?{addOne:?state?=>?{state.count?+=?1},subOne:?state?=>?{state.count?-=?1},addSome:?(state,?action)?=>?{state.count?+=?action.payload},subSome:?(state,?action)?=>?{state.count?-=?action.payload},reset:?state?=>?{state.count?=?0}},
})export?const?{?addOne,?subOne,?addSome,?subSome,?reset?}?=?counterSlice.actionsexport?default?counterSlice.reducer
首先我們需要通過運行
npm install @reduxjs/toolkit react-redux
來安裝它在我們的 store 中,我們從 Redux toolkit 中導入
configureStore
函數,通過調用此函數來創建 store,并將一個帶有 reducer 的對象傳遞給它,該對象本身就是一個包含 slice 的對象。
export?const?store?=?configureStore({reducer:?{counter:?counterReducer},
})
正如我所提到的,slice 是一種將 action 和 reducer 壓縮為同一個的方法。我們從 Redux toolkit 中導入
createSlice
函數,然后聲明初始狀態并初始化 slice。
這個函數將接收 slice 的名稱、初始狀態以及我們將從組件派發以修改狀態的函數作為參數。
注意這里沒有任何 actions。UI 將直接調用 reducer 函數。這就是 Redux toolkit “帶走”的復雜性。
export?const?counterSlice?=?createSlice({name:?'counter',initialState,reducers:?{addOne:?state?=>?{state.count?+=?1},subOne:?state?=>?{state.count?-=?1},addSome:?(state,?action)?=>?{state.count?+=?action.payload},subSome:?(state,?action)?=>?{state.count?-=?action.payload},reset:?state?=>?{state.count?=?0}},
})
在 index.js 中,我們用 provider 組件包裝應用程序,這樣的話我們可以從任何地方訪問狀態。
<Provider?store={store}><App?/></Provider>
最后,我們使用 hooks 從組件中讀取狀態和 dispatch 修改函數,就像使用普通的 Redux 一樣。
function?App()?{const?dispatch?=?useDispatch()const?count?=?useSelector(state?=>?state.counter.count)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?dispatch(addOne())}>Add?1</button><button?onClick={()?=>?dispatch(subOne())}>Decrease?1</button><button?onClick={()?=>?dispatch(addSome(10))}>Add?10</button><button?onClick={()?=>?dispatch(subSome(10))}>Decrease?10</button><button?onClick={()?=>?dispatch(reset())}>Reset?count</button></div></div>)
}
Redux toolkit 旨在成為處理 Redux 的一種更簡單的方法,但在我看來,它仍然是幾乎相同的模板,與普通的 Redux 沒有太大區別。
提到 Redux Thunk 和 Redux Saga
Redux thunk 和 Redux Saga 是兩個與 Redux 一起使用的很流行的中間件庫;
具體來說,Thunk 和 Saga 都是為了處理副作用或異步任務所使用的。
Recoil
Recoil 是一個開源狀態管理庫,專門用于由 Facebook(或 Meta,等等)構建的 React。根據他們的網站,Recoil是為“最小化和響應式”而建立的,在這個意義上,它看起來和感覺都像普通的 React 代碼。
Recoil 基于原子(atom) 的概念。來自他們的文檔,
“一個原子代表一個狀態。原子可以從任何組件讀取和寫入。讀取原子值的組件隱式訂閱了該原子,因此任何原子更新都會導致所有訂閱該原子的組件重新渲染”。
使用 Recoil,我們的示例應用程序將如下所示:
//?App.js
import?countState?from?'./recoil/counter.atom'
import?'./App.scss'import?{?useRecoilState?}?from?'recoil'function?App()?{const?[count,?setCount]?=?useRecoilState(countState)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?setCount(count+1)}>Add?1</button><button?onClick={()?=>?setCount(count-1)}>Decrease?1</button><button?onClick={()?=>?setCount(count+10)}>Add?10</button><button?onClick={()?=>?setCount(count-10)}>Decrease?10</button><button?onClick={()?=>?setCount(0)}>Reset?count</button></div></div>)
}export?default?App
//?index.js
import?React?from?'react'
import?ReactDOM?from?'react-dom'
import?App?from?'./App'
import?{?RecoilRoot?}?from?'recoil'ReactDOM.render(<React.StrictMode><RecoilRoot><App?/></RecoilRoot></React.StrictMode>,document.getElementById('root')
)
//?counter.atom.jsx
import?{?atom?}?from?'recoil'const?countState?=?atom({key:?'countState',?//?唯一?ID?(區別于其他?atoms/selectors)default:?0?//?默認值?(又稱初始值)
})export?default?countState
你可能馬上就能看到,這比 Redux 的模板要少很多。
首先我們通過運行
npm install recoil
來安裝它那些使用 recoil 狀態的組件需要在其父組件的某處使用
RecoilRoot
,所以我們用它來包裝我們的應用程序
<React.StrictMode><RecoilRoot><App?/></RecoilRoot></React.StrictMode>
然后我們聲明我們的 atom,它只是一個包含鍵和默認值的對象:
const?countState?=?atom({key:?'countState',?//?唯一?ID?(區別于其他?atoms/selectors)default:?0?//?默認值?(又稱初始值)
})
最后,在我們的組件中,我們導入
useRecoilState
hook,用它聲明我們的狀態,并將我們剛剛在 atom 中聲明的唯一鍵傳遞給它
const?[count,?setCount]?=?useRecoilState(countState)
正如你所看到的,這看起來與常規的 useState hook 非常相似。
在我們的 UI 中,我們只是調用 setCount
函數來更新我們的狀態。
<button?onClick={()?=>?setCount(count+1)}>Add?1</button>
最小,并且非常易于使用。Recoil 仍然是一種實驗性的,并沒有被廣泛使用,但你可以看到世界各地的開發人員將如何轉向這個工具。
Jotai
Jotai 是一個為 React 構建的開源狀態管理庫,其靈感來自 Recoil。它與 Recoil 的不同之處在于尋找一個更加簡約的 API -- 它不使用字符串鍵,而且是面向 TypeScript 的。
與 Recoil 一樣,Jotai 使用 atoms。atom 代表一片狀態。你只需要指定一個初始值,它可以是原始值,如字符串和數字、對象和數組。然后在你的組件中使用該 atom,在每次 atom 更改時該組件將重新渲染。
使用 Jotai,我們的示例應用程序如下所示:
//?App.js
import?'./App.scss'import?{?useAtom?}?from?'jotai'function?App()?{const?[count,?setCount]?=?useAtom(countAtom)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?setCount(count+1)}>Add?1</button><button?onClick={()?=>?setCount(count-1)}>Decrease?1</button><button?onClick={()?=>?setCount(count+10)}>Add?10</button><button?onClick={()?=>?setCount(count-10)}>Decrease?10</button><button?onClick={()?=>?setCount(0)}>Reset?count</button></div></div>)
}export?default?App
//?counter.atom.jsx
import?{?atom?}?from?'jotai'const?countAtom?=?atom(0)export?default?countAtom
如你所見,它比 Recoil 還要小。
我們通過運行
npm install jotai
來安裝它然后我們聲明一個具有默認值的 atom:
const?countAtom?=?atom(0)
在我們的組件中,通過使用
useAtom
去使用它:
const?[count,?setCount]?=?useAtom(countAtom)
真的很好,很簡單!
Zustand
Zusand 是另一個為 React 構建的開源狀態管理庫。它的靈感來自于在 Redux 出現之前廣泛使用的庫 Flux,它的目標是
“一個小型的、快速的、非觀點性的、可擴展的準系統狀態管理解決方案,具有基于 hooks 的舒適 API,并且幾乎沒有模板”
Zusand 使用 store 的方式與 Redux 類似,但不同之處在于,在 Zusand 中,store 是一個 hook,它需要的模板要少得多。
使用 Zusand,我們的示例應用程序將如下所示:
//?App.js
import?'./App.scss'
import?useStore?from?'./store'function?App()?{const?count?=?useStore(state?=>?state.count)const?{?addOne,?subOne,?add10,?sub10,?reset?}?=?useStore(state?=>?state)return?(<div?className="App"><p>Count?is:?{count}</p><div><button?onClick={()?=>?addOne()}>Add?1</button><button?onClick={()?=>?subOne()}>Decrease?1</button><button?onClick={()?=>?add10()}>Add?10</button><button?onClick={()?=>?sub10()}>Decrease?10</button><button?onClick={()?=>?reset()}>Reset?count</button></div></div>)
}export?default?App
//?Index.jsx?(STORE)
import?create?from?'zustand'const?useStore?=?create(set?=>?({count:?0,addOne:?()?=>?set(state?=>?({count:?state.count?+=?1})),subOne:?()?=>?set(state?=>?({count:?state.count?-=?1})),add10:?()?=>?set(state?=>?({count:?state.count?+=?10})),sub10:?()?=>?set(state?=>?({count:?state.count?-=?10})),reset:?()?=>?set({count:?0})
}))export?default?useStore
我們通過運行
npm install zustand
安裝它我們使用從 Zusand 導入的
create
函數創建了一個 store,在函數里我們設置了默認狀態和我們將用來修改狀態的函數
const?useStore?=?create(set?=>?({count:?0,addOne:?()?=>?set(state?=>?({count:?state.count?+=?1})),subOne:?()?=>?set(state?=>?({count:?state.count?-=?1})),add10:?()?=>?set(state?=>?({count:?state.count?+=?10})),sub10:?()?=>?set(state?=>?({count:?state.count?-=?10})),reset:?()?=>?set({count:?0})
}))
然后在我們的組件中導入剛剛創建的 store,并通過以下方式從中讀取狀態和修改函數:
const?count?=?useStore(state?=>?state.count)const?{?addOne,?subOne,?add10,?sub10,?reset?}?=?useStore(state?=>?state)
在我們的 UI 中可以像這樣調用修改函數:
<button?onClick={()?=>?addOne()}>Add?1</button>
你可以看到來自 Redux 相同概念的 Zusand,卻有一個更干凈、更簡單的方法。
總結
狀態管理是前端開發中最復雜的主題之一。你可以看到有多少人試圖讓它以可預測和可擴展的方式,而且是以干凈和易于使用的方式工作。
特別是在過去的幾年里,出現了很多很好的工具,提供了處理狀態管理的好方法。
不過,作為開發者,我們必須牢記,Redux 和其他庫的創建是為了解決特定的狀態管理問題,特別是在真正的大型、復雜和大量使用的應用程序中。
我認為,如果你沒有遇到這些問題,真的沒有必要增加額外的模板,使你的代碼復雜化。即使使用那些幾乎不添加樣板的現代庫。
React 本身是一個非常強大和可靠的庫,useState、useReducer 和 useContext 等工具足以解決大多數問題。因此,我會堅持基本的東西,除非因為某些原因,基本的東西已經不夠用了。
當需要更具體、更強大的狀態管理庫時,我認為應該在可靠性和簡單性之間做出選擇。
Redux 是最成熟和使用最廣泛的庫,它附帶大量文檔、在線社區以及在每個新版本中發現和解決的以前錯誤。
它的壞處是,作為開發者,它給我們帶來了一些我們必須學習和思考的新概念。我們還需要添加相當多的代碼來使其工作,而且它所增加的復雜性可能超過它所幫助解決的問題。
相反,我們之前所看到的現代庫要簡單得多,而且直截了當,但是它們沒有得到廣泛使用和測試,仍然是一種實驗性的。
但就我們目前所看到的而言,其中一個或一些帶頭成為更廣泛使用的工具似乎只是時間問題。
我希望你喜歡這篇文章并學到了一些新東西。
干杯,下次見!
原文鏈接:https://www.freecodecamp.org/news/how-to-manage-state-in-a-react-app/
作者:Germán Cocca
譯者:Jing Wu
我在阿里招前端,我該怎么幫你?(現在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準備阿里P6/P7前端面試--項目經歷準備篇
大廠面試官常問的亮點,該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
如何準備20K+的大廠前端面試
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助4000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 lxchuan12、拉你進源碼共讀群
今日話題
目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 lxchuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持~