一、如何使用redux
首先要明确為什麼要使用redux,這一點很重要,如果不知道為什麼使用redux,那麼在開發的過程中肯定不能合理的使用redux.首先來看redux的本質:
redux做為一款狀态管理工具,主要是為了解決元件間通信的問題。
既然是元件間的通信問題,那麼顯然将所有頁面的狀态都放入redux中,是不合理的,複雜度也很高。
(1)全量使用redux
筆者在早期也犯了這個問題,在應用中,不管什麼狀态,按頁面級路由拆分,全部放在redux中,頁面任何狀态的更改,通過react-redux的mapState和mapDispatch來實作。
redux中的狀态從狀态更新到回報到視圖,是一個過程鍊太長,從dispatch一個action出發,然後走reducer等邏輯,一個完整的鍊路包含:
建立action,建立redux中間件,建立相應type的reducer函數,建立mapState和mapDispatch等。
如果将所有狀态都儲存在redux中,那麼每一個狀态必須走這幾步流程,及其繁瑣,毫無疑問增加了代碼量
(2)減少局部狀态和redux狀态的不合理混用
全量使用redux的複雜度很高,我們當然考慮将一部分狀态放在redux中,一部分狀态放在local state中,但是這種情況下,很容易産生一個問題,就是如果local State跟redux中的state存在狀态依賴。
舉例來說,在redux中的狀态中有10個學生
//redux
students = [{name:"小明",score:70},{name:"小紅",score:50}....]
在local state中我們儲存了分數在60分以上的學生
// local state
state = [{name:"小明",score:70}]
如果redux中的學生改變了,我們需要從redux中動态的擷取students資訊,然後改變局部的state.結合react-redux,我們需要在容器元件中使用componentWillReceivedProps或者getDerivedStateFromProps這個聲明周期,來根據props改變局部的local state.
componentWillReceivedProps這裡不讨論,為了更高的安全性,在react中用靜态的getDerivedStateFromProps代替了componentWillReceivedProps這裡不讨論,而getDerivedStateFromProps這個聲明周期函數在props和state變化的時候都會去執行,是以如果我們需要僅僅在props的改變而改變局部的local state,在這個聲明周期中會存在着很複雜的判斷邏輯。
redux中的狀态和local state中的狀态相關聯的越多,getDerivedStateFromProps這個聲明周期函數就越複雜
給我們的啟示就是盡可能的減少getDerivedStateFromProps的使用,如果實在是redux和local state有關聯性,用id會比直接用對象或者數組好,比如上述的例子,我們可以将學生分組,并給一個組号,每次在redux中的學生資訊發生改變的時候會改變相應的組号。
這樣在getDerivedStateFromProps隻需要判斷組号是否改變即可:
class Container extends React.Component{
state = {
group_id:number
}
static getDerivedStateFromProps(props,state){
if(props.group_id!==state.group_id){
... 更新及格的學生
}else{
return null
}
}
}
這裡推薦https://github.com/paularmstrong/normalizr,state關聯性強,可以先将資料範式化,範式化後的資料類似于給一個複雜結構一個id,這樣子會簡化getDerivedStateFromProps的邏輯.
(3)本節小結
如何使用redux,必須從redux的本質出發,redux的本質是為了解決元件間的通信問題,是以元件内部獨有的狀态不應該放在redux中,此外如果redux結合class類元件使用,可以将資料範式化,簡化複雜的判斷邏輯。
二、react hooks管理local state
前面将了應該如何使用redux,那麼如何維護local state呢,React16.8中正式增加了hooks。通過hooks管理local state,簡單易用可擴充。
在hooks中的局部狀态常見的有3種,分别是useState、useRef和useReducer
(1) useState
useState是hooks中最常見的局部狀态,比如:
const [hide, setHide] = React.useState(false);
const [name, setName] = React.useState('BI');
了解useState必須明确,在react hooks中:
每一次渲染都有它自己的 Props and State
一個經典的例子就是:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
如果我按照下面的步驟去操作:
- 點選增加counter到3
- 點選一下 “Show alert”
- 點選增加 counter到5并且在定時器回調觸發前完成
猜猜看會alert出什麼?結果是彈出了3,alert會“捕獲”我點選按鈕時候的狀态,也就是說每一次的渲染都會有獨立的props和state.
(2) useRef
在react hooks中,我們知道了每一次的渲染都會有獨立的props和state,那麼如果我們需要跟類元件一樣,每次都能拿到最新的渲染值時,應該怎麼做呢?此時我們可以用useRef
useRef提供了一個Mutable可變的資料
我們來修改上述的例子,來是的alert為5:
function Counter() {
const [count, setCount] = useState(0)
const late = useRef(0)
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + late.current)
}, 3000)
}
useEffect(() => {
late.current = count
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
)
}
如此修改以後就不是alert3 而是彈出5
(3) useReducer
react hooks中也提供了useReducer來管理局部狀态.
當你想更新一個狀态,并且這個狀态更新依賴于另一個狀态的值時,你可能需要用useReducer去替換它們。
同樣的用例子來說明:
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => {
dispatch({
type: 'step',
step: Number(e.target.value)
});
}} />
</>
);
}
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
解釋上面的結果主要來看useEffect部分:
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
在state中的count依賴與step,但是使用了useReducer後,我們不需要在useEffect的依賴變動數組中使用step,轉而用dispatch來替代,這樣的好處就是減少不必要的渲染行為.
此外:局部狀态不推薦使用 useReducer ,會導緻函數内部狀态過于複雜,難以閱讀。 useReducer 建議在多元件間通信時,結合 useContext 一起使用。
三、react hooks如何解決元件間的通信
react hooks中的局部狀态管理相比于類元件而言更加簡介,那麼如果我們元件采用react hooks,那麼如何解決元件間的通信問題。
(1) UseContext
最基礎的想法可能就是通過useContext來解決元件間的通信問題。
比如:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
在這個例子中通過createContext和useContext,可以在App的子元件CounterDisplay中使用context,進而實作一定意義上的元件通信。
如果context太多,那麼如何維護這些context
也就是說在大量元件通信的場景下,用context進行元件通信代碼的可讀性很差。這個類元件的場景一緻,context不是一個新的東西,雖然用了useContext減少了context的使用複雜度。
(2) Redux結合hooks來實作元件間的通信
hooks元件間的通信,同樣可以使用redux來實作。也就是說:
在React hooks中,redux也有其存在的意義
在hooks中存在一個問題,因為不存在類似于react-redux中connect這個高階元件,來傳遞mapState和mapDispatch, 解決的方式是通過redux-react-hook或者react-redux的7.1 hooks版本來使用。
redux-react-hook:在redux-react-hook中提供了StoreContext、useDispatch和useMappedState來操作redux中的store,比如定義mapState和mapDispatch的方式為:
import {StoreContext} from 'redux-react-hook';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root'),
);
import {useDispatch, useMappedState} from 'redux-react-hook';
export function DeleteButton({index}) {
// Declare your memoized mapState function
const mapState = useCallback(
state => ({
canDelete: state.todos[index].canDelete,
name: state.todos[index].name,
}),
[index],
);
// Get data from and subscribe to the store
const {canDelete, name} = useMappedState(mapState);
// Create actions
const dispatch = useDispatch();
const deleteTodo = useCallback(
() =>
dispatch({
type: 'delete todo',
index,
}),
[index],
);
return (
<button disabled={!canDelete} onClick={deleteTodo}>
Delete {name}
</button>
);
}
react-redux 7.1的hooks版:這也是官方較為推薦的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()這3個主要方法,分别對應與mapState、mapDispatch以及直接拿到redux中store的執行個體.
簡單介紹一下useSelector,在useSelector中除了能從store中拿到state以外,還支援深度比較的功能,如果相應的state前後沒有改變,就不會去重新的計算.
舉例來說,最基礎的用法:
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
實作緩存功能的用法:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.isDone).length
)
export const DoneTodosCounter = () => {
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{NumOfDoneTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<DoneTodosCounter />
</>
)
}
在上述的緩存用法中,隻要todos.filter(todo => todo.isDone).length不改變,就不會去重新計算.
四、總結
react中完整的狀态管理分為全局狀态和局部狀态,而react hooks簡化了局部狀态,使得管理局部狀态以及控制局部渲染極其友善,但是react hooks本質上還是一個視圖元件層的,并沒有完美的解決元件間的通信問題,也就是說,redux等狀态管理機和react hooks本質上并不沖突。
在我的實踐中,用redux實作元件間的通信而react hooks來實作局部的狀态管理,使得代碼簡單已讀的同時,也減少了很多不必要的redux樣闆代碼.