React 中使用 Redux 的 4 種寫法小結
Redux 是一種狀態容器 JS 庫,提供可預測的狀態管理,經常和 React 配合來管理應用的全局狀態,進行響應式組件更新。
Redux 一般來說並不是必須的,隻有在項目比較復雜的時候,比如多個分散在不同地方的組件使用同一個狀態。對於這種情況,如果通過 props 層層傳遞,代碼會變得不可維護,這時候我們可以考慮使用 Redux 這類狀態管理庫。
不使用 Redux 的寫法
我們創建一個 User 組件,顯示用戶名,並支持設置用戶名。先看看不使用 Redux 的寫法。
import { Component, createRef } from 'react'; class User extends Component { state = { username: '前端西瓜哥' }; inputRef = createRef(); setUsername = () => { this.setState({ username: this.inputRef.current.value }); }; render() { return ( <div> <div>用戶名: {this.state.username}</div> <input ref={this.inputRef} type="text" /> <button onClick={this.setUsername}>設置用戶名</button> </div> ); } } export default User;
下面我們改造一下這個組件,將狀態遷移到 Redux 裡。
最底層的寫法
Redux 是和框架無關的,我們先看看隻用 Redux 庫的寫法。
demo:codesandbox.io/s/redux-pla…
首先我們創建一個 reducer。
// user_reducer.js import { SET_USERNAME } from './constants'; // 初始值 const defaultState = { name: '前端西瓜哥', age: 88 }; // 用於修改 user 狀態的 reducer export const userReducer = (preState = defaultState, action) => { switch (action.type) { case SET_USERNAME: // type 值都統一放到 constants return { ...preState, name: action.payload }; // 這裡還可以根據需要,添加類似 setUserAga 等邏輯 default: return preState; } };
// constants.js export const SET_USERNAME = 'SET_USERNAME';
reducer 是一個用於更新狀態的函數,接收原來的狀態 preState 和一個更新動作對象 action。
action 對象有一個 表示此次操作的描述 type
和 其他數據屬性(通常為 payload)
。payload 會以某種方式去計算出一個新的狀態,替換掉 redux 中原來的 state。
{ type: 'SET_USERNAME', payload: '新用戶名' }
type 通常是一個字符串,比如我們會用 'COUNT_INCREMENT'
來給一個計數器加一,或用 'SET_USERNAME'
來更新用戶名。reducer 會根據不同的 type 來執行不同的更新 state 行為。
action 的構造我們通常會用一個函數幫忙構建,這種函數稱為 Action Creator:
// user_action.js import { SET_USERNAME } from './constants'; export const setUsernameAction = (data) => { return { type: SET_USERNAME, payload: data }; };
有瞭 reducer,我們可以用它們來構建我們的 store。store 可以訪問所有的保存在 redux 狀態:
import { combineReducers, createStore } from 'redux'; import { userReducer } from './user_reducer'; const store = createStore( combineReducers({ user: userReducer }) ); export default store;
combineReducers 可以將多個 reducer 組合在一起,有各自對應的屬性名。比如上面的代碼,我們可以通過 store.getState().user
來拿到用戶對象。
如果你又新增瞭 counter 狀態對象,隻需再加上 counter: counterReducer
,就可以用 store.getState().counter
來拿到這個對象。
createStore 用於創建應用中所有的 state,然後這些 state 都會存放到這個被返回的 store 裡。
現在我們的 User 組件就變成這樣瞭:
import { Component, createRef } from 'react'; import store from '../store/store'; import { setUsernameAction } from '../store/user_action'; class User extends Component { inputRef = createRef(); componentDidMount() { store.subscribe(() => { this.setState({}); }); } setUsername = () => { store.dispatch(setUsernameAction(this.inputRef.current.value)); }; render() { return ( <div> <div>用戶名: {store.getState().user.name}</div> <input ref={this.inputRef} type="text" /> <button onClick={this.setUsername}>設置用戶名</button> </div> ); } } export default User;
-
store.getState()
可以拿到 state 對象,通過它,我們獲取到其下我們需要的對象,比如 user 對象。 -
store.dispatch(action)
派發 action 對象,觸發狀態的更新。 -
store.subscribe(fn)
訂閱狀態的變化,執行回調函數。這裡我們一發現狀態發生瞭變化,就立刻重新渲染組件。
Redux 本質是發佈訂閱模式,狀態集中在一起,狀態可以通過 store.getState()
訪問,通過 store.dispatch(action)
改變狀態,通過 store.subscribe(fn)
訂閱狀態變化(React 組件監聽到變化後,重新渲染組件)。
這種寫法是最原始的寫法,可以用在任何框架中。
缺點很明顯:用到 redux 的組件要訂閱 state 變化,一變化就重新渲染組件。有時候其他組件的 state 變化瞭,當前組件也會進行不必要的重新渲染。
自己去判斷吧,又太繁瑣,容易寫錯,也容易忘記訂閱。對於忘記訂閱的問題,我們也可以直接把讓根組件來監聽和重新渲染,但這樣性能很差。
接下來西瓜哥要講的 React-Redux 庫可以解決這個問題。它能夠在當前組件用到的特定 state 發生改變時,才重新渲染組件。
React-Redux
發現大傢都很喜歡在 React 裡用 Redux,於是 Facebook 出瞭一個 React-Redux 庫,讓大傢能夠更好更正確地在 React 中使用 Redux。
React-Redux 配合 connect 高階組件
我們先看看使用 connect 的寫法。
demo:codesandbox.io/s/react-red…
React-Redux 引入瞭一個容器組件的概念,這個組件專門負責和 redux 打交道 。容器組件其實是一個高階組件,將真正的 UI 組件做一個封裝,在上面做瞭以下工作:
-
將 state 和 dispatch 映射到 props,註入到 UI 組件中
-
監聽 state 變化,必要時重新渲染 UI 組件。
高階組件:一個函數,它會接收組件參數,然後返回一個新的組件。高階組件的作用是對真正的 UI 組件做一些復用的邏輯的封裝,通常用於做功能增強。
隨著 React Hooks 愈發流行,大傢現在更喜歡用 React Hooks 來取代高階函數,寫法更優雅。
const ContainerComponent = connect( mapStateToProps, mapDispatchToProps, )(UIComponent);
現在開始改造項目。
我們創建一個 container 文件夾,裡面放上 User.jsx 文件,裡面寫上如下內容:
// containers/User.jsx import { connect } from 'react-redux'; import UserUI from '../components/User'; import { setUsernameAction } from '../store/user_action'; export default connect( // mapStateToProps (state) => ({ user: state.user }), // mapDispatchToProps (dispatch) => ({ setUsername: (newName) => dispatch(setUsernameAction(newName)) }) )(UserUI);
然後記得在使用該容器的地方,傳入我們的 store 對象,如下:
import UserContainer from './containers/User'; import store from './store/store'; import './styles.css'; export default function App() { return <UserContainer store={store} />; }
當然每個容器組件都要傳入 store 未免太麻煩,我們通常會使用另一種做法:使用 redux-react 提供的 Context Provider,包裹住根組件,如下:
import { Provider } from 'react-redux'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
然後是 UI 組件的改造:
import { Component, createRef } from 'react'; class User extends Component { inputRef = createRef(); render() { return ( <div> <div>用戶名: {this.props.user.name}</div> <input ref={this.inputRef} type="text" /> <button onClick={() => this.props.setUsername(this.inputRef.current.value)} > 設置用戶名 </button> </div> ); } } export default User;
UI 組件的 props 會拿到 user 對象、setUsername 方法以及我們註入的 store 對象(如果用 Context 的方式則取不到)。
使用瞭 connect 後,隻有組件用到的 state 改變瞭,才會觸發組件的更新。
這裡有個需要特別註意的地方,就是你要 保證新的狀態對象和舊狀態不相等,這樣才能觸發組件重新渲染,這在處理對象方法時容易出錯。你需要拷貝一個新的對象作為新的狀態,推薦使用擴展運算符的寫法。
// user_reducer.js // 錯誤的寫法,新的 state 依舊指向原來的對象 preState.name = action.payload; return preState; // 正確的寫法 return { ...preState, name: action.payload };
或者可以考慮使用 immer 這種不可變數據結構庫。
React-Rudex 配合 React Hooks
前面我們用瞭 connect 這麼一個高階組件,是為瞭給 UI 組件增強功能。
說到增強功能,react-redux 也提供瞭現在非常流行的 React Hooks 寫法,寫起來更優雅,也是目前西瓜哥我所在公司的做法。
這裡我們就不需要 connect 高階組件瞭,也就是說不需要容器組件。
demo:codesandbox.io/s/react-red…
// User.js import { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setUsernameAction } from '../store/user_action'; const User = () => { // 獲取狀態 const user = useSelector((state) => state.user); // 獲取 dipatch 方法 const dipatch = useDispatch(); const inputRef = useRef(null); return ( <div> <div>用戶名: {user.name}</div> <input ref={inputRef} type="text" /> <button onClick={() => { const newName = inputRef.current.value; dipatch(setUsernameAction(newName)); }} > 設置用戶名 </button> </div> ); }; export default User;
通過 useSelector 我們可以拿到通過上下文綁定的 state,然後從中獲取我們需要用到的狀態。
const user = useSelector((state) => state.user);
如果有多個,我們可以寫成對象的形式:
const { user, counter } = useSelector((state) => ({ user: state.user, counter: state.counter }));
是不是有點像 connect 的 mapStateToProps。
然後是獲取 dispatch 方法:
const dipatch = useDispatch();
hook 非常優雅,但我也發現,相比 connect 寫法,我們的 redux 狀態邏輯和組件耦合在一起瞭。不過一般我們的組件都是業務組件,還是可以接受的。
Redux Toolkit
我們可以看到,我們要維護一個狀態,我們要寫 reducer 方法、action creator 方法,還要用一個 contants.js 文件集中式管理所有的 actionType 字符串。
你發現你寫瞭非常多的 模板代碼,每加一個 state 就要創建上面這些東西,各個文件裡跑來跑去,人都麻瞭。
於是 redux 又出瞭一個工具集庫 Redux Toolkit,來解決這個問題。
demo:codesandbox.io/s/redux-too…
Redux Toolkit 提供瞭 createSlice 方法,可以幫你用更少的代碼生成配套的 reducer 和 action,而且有很好的可維護性。
// userSlice.js import { createSlice } from '@reduxjs/toolkit'; const userSlice = createSlice({ name: 'user', initialState: { name: '前端西瓜哥', age: 88 }, reducers: { setUsername: (state, action) => { // 因為 Redux Toolkit 內置使用瞭 immer,所以可以直接改。 state.name = action.payload; } } }); // actions export const { setUsername } = userSlice.actions; // 獲取自己需要的 state,用在組件的 userSeletor hook 上。 export const selectUser = (state) => state.user; // reducer export default userSlice.reducer;
createSlice 傳入 name(標識符,生成 actions 要用到)、initialState(初始值)、reducers(變成瞭對象形式)參數,然後返回一個對象。
這個返回的 slice 對象有 actions 對象屬性,比如上面的代碼,actions 下有一個 setUsername 的方法,執行後會返回 {type: "user/setUsername", payload: "新名字"}
。
可以看到 action 的 type 是根據 name 和 reducers 的屬性生產的,確保唯一性。
slice 還有一個 reducer 對象,其實就是將前面傳入的 reducers 配合自動生成的 action 轉換為瞭函數的形式。
createSlice 幹瞭什麼事?createSlice 將原來管理一個狀態但代碼卻是分離的 action 和 reducer 集中在瞭一起,不用自己去起 actionType 的名字。
然後是生成 store 也要改成 configureStore 的寫法:
// store.js import { configureStore } from '@reduxjs/toolkit'; import userReducer from './userSlice'; const store = configureStore({ reducer: { user: userReducer } }); export default store;
總結
簡單總結一下:
-
原始寫法,過於簡陋,需要自己通過
store.subscribe(fn)
來判斷一個組件是否要重新渲染,寫起來麻煩、性能堪憂。 -
配合 Redux React 庫,通過 connect 來註入 redux 狀態,要多寫一個 connect 高階組件生成的容器組件,但降低瞭耦合度。Redux React 會隻在組件需要的狀態改變時,重新渲染組件。這裡要註意改變時,新舊狀態不能相同,尤其是對象的情況,否則重新渲染不會工作。
-
如果你的項目主要使用函數組件,可以不用 connect,直接用 useSelector 來獲取狀態,以及用 userDispatch 來改變狀態。非常優雅。
-
Redux 又推出瞭 Redux Toolkit,解決瞭配置復雜、需要寫太多模板、需要手動安裝大量相關包的問題。
到此這篇關於在 React 中使用 Redux 的 4 種寫法的文章就介紹到這瞭,更多相關React使用 Redux內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 詳解React 和 Redux的關系
- 使用react+redux實現計數器功能及遇到問題
- React Redux使用配置詳解
- 一文搞懂redux在react中的初步用法
- react redux的原理以及基礎使用講解