React-redux: React.js 和 Redux 架構的結合
- 2020 年 3 月 14 日
- 筆記
通過Redux 架構理解我們了解到 Redux 架構的 store、action、reducers 這些基本概念和工作流程。我們也知道了 Redux 這種架構模式可以和其他的前端庫組合使用,而 React-redux 正是把 Redux 這種架構模式和 React.js 結合起來的一個庫。
Context
在 React 應用中,數據是通過 props 屬性自上而下進行傳遞的。如果我們應用中的有很多組件需要共用同一個數據狀態,可以通過狀態提升的思路,將共同狀態提升到它們的公共父組件上面。但是我們知道這樣做是非常繁瑣的,而且程式碼也是難以維護的。這時會考慮使用 Context,Context 提供了一個無需為每層組件手動添加 props,就能在組件樹間進行數據傳遞的方法。也就是說在一個組件如果設置了 context,那麼它的子組件都可以直接訪問到裡面的內容,而不用通過中間組件逐級傳遞,就像一個全局變數一樣。
在 App -> Toolbar -> ThemedButton 使用 props 屬性傳遞 theme,Toolbar 作為中間組件將 theme 從 App 組件 傳遞給 ThemedButton 組件。
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { // Toolbar 組件接受一個額外的“theme”屬性,然後傳遞給 ThemedButton 組件。 // 如果應用中每一個單獨的按鈕都需要知道 theme 的值,這會是件很麻煩的事, // 因為必須將這個值層層傳遞所有組件。 return ( <div> <ThemedButton theme={props.theme} /> </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; } }
使用 context,就可以避免通過中間元素傳遞 props 了
// Context 可以讓我們無須明確地傳遍每一個組件,就能將值深入傳遞進組件樹。 // 為當前的 theme 創建一個 context(“light”為默認值)。 const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // 使用一個 Provider 來將當前的 theme 傳遞給以下的組件樹。 // 無論多深,任何組件都能讀取這個值。 // 在這個例子中,我們將 “dark” 作為當前的值傳遞下去。 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // 中間的組件再也不必指明往下傳遞 theme 了。 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } class ThemedButton extends React.Component { // 指定 contextType 讀取當前的 theme context。 // React 會往上找到最近的 theme Provider,然後使用它的值。 // 在這個例子中,當前的 theme 值為 “dark”。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } }
雖然解決了狀態傳遞的問題卻引入了 2 個新的問題。
1. 我們引入的 context 就像全局變數一樣,裡面的數據可以被子組件隨意更改,可能會導致程式不可預測的運行。
2. context 極大地增強了組件之間的耦合性,使得組件的復用性變差,比如 ThemedButton 組件因為依賴了 context 的數據導致復用性變差。
我們知道,redux 不正是提供了管理共享狀態的能力嘛,我們只要通過 redux 來管理 context 就可以啦,第一個問題就可以解決了。
Provider 組件
React-Redux 提供 Provider
組件,利用了 react 的 context 特性,將 store 放在了 context 裡面,使得該組件下面的所有組件都能直接訪問到 store。大致實現如下:
class Provider extends Component { // getChildContext 這個方法就是設置 context 的過程,它返回的對象就是 context,所有的子組件都可以訪問到這個對象 getChildContext() { return { store: this.props.store }; } render() { return this.props.children; } } Provider.childContextTypes = { store: React.PropTypes.object }
那麼我們可以這麼使用,將 Provider 組件作為根組件將我們的應用包裹起來,那麼整個應用的組件都可以訪問到裡面的數據了
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { createStore } from 'redux'; import todoApp from './reducers'; import App from './components/App'; const store = createStore(todoApp); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
展示(Dumb Components)組件和容器(Smart Components)組件
還記得我們的第二個問題嗎?組件因為 context 的侵入而變得不可復用。React-Redux 為了解決這個問題,將所有組件分成兩大類:展示組件和容器組件。
展示組件
展示組件有幾個特徵
1. 組件只負責 UI 的展示,沒有任何業務邏輯
2. 組件沒有狀態,即不使用 this.state
3. 組件的數據只由 props 決定
4. 組件不使用任何 Redux 的 API
展示組件就和純函數一樣,返回結果只依賴於它的參數,並且在執行過程裡面沒有副作用,讓人覺得非常的靠譜,可以放心的使用。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; class Title extends Component { static propTypes = { title: PropTypes.string } render () { return ( <h1>{ this.props.title }</h1> ) } }
像這個 Title 組件就是一個展示組件,組件的結果完全由外部傳入的 title 屬性決定。
容器組件
容器組件的特徵則相反
1. 組件負責管理數據和業務邏輯,不負責 UI 展示
2. 組件帶有內部狀態
3. 組件的數據從 Redux state 獲取
4. 使用 Redux 的 API
你可以直接使用 store.subscribe()
來手寫容器組件,但是不建議這麼做,因為這樣無法使用 React-redux 帶來的性能優化。
React-redux 規定,所有的展示組件都由用戶提供,容器組件則是由 React-Redux 的 connect()
自動生成。
高階組件 Connect
React-redux 提供 connect
方法,可以將我們定義的展示組件生成容器組件。connect 函數接受一個展示組件參數,最後會返回另一個容器組件回來。所以 connect 其實是一個高階組件(高階組件就是一個函數,傳給它一個組件,它返回一個新的組件)。
import { connect } from 'react-redux'; import Header from '../components/Header'; export default connect()(Header);
上面程式碼中,Header 就是一個展示組件,經過 connect 處理後變成了容器組件,最後把它導出成模組。這個容器組件沒有定義任何的業務邏輯,所有不能做任何事情。我們可以通過 mapStateToProps
和 mapDispatchToProps 來定義我們的業務邏輯。
import { connect } from 'react-redux'; import Title from '../components/Title'; const mapStateToProps = (state) => { return { title: state.title } } const mapDispatchToProps = (dispatch) => { return { onChangeColor: (color) => { dispatch({ type: 'CHANGE_COLOR', color }); } } } export default connect(mapStateToProps, mapDispatchToProps)(Title);
mapStateToProps 告訴 connect 我們要取 state 里的 title 數據,最終 title 數據會以 props 的方式傳入 Title 這個展示組件。
mapStateToProps 還
會訂閱 Store,每當 state 更新的時候,就會自動執行,重新計算展示組件的參數,從而觸發展示組件的重新渲染。
mapDispatchToProps 告訴 connect 我們需要 dispatch action,最終 onChangeColor 會以 props 回調函數的方式傳入 Title 這個展示組件。
Connect 組件大概的實現如下
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends Component { static contextTypes = { store: PropTypes.object } constructor () { super() this.state = { allProps: {} } } componentWillMount () { const { store } = this.context this._updateProps() store.subscribe(() => this._updateProps()) } _updateProps () { const { store } = this.context let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props) // 將 Store 的 state 和容器組件的 state 傳入 mapStateToProps : {} // 判斷 mapStateToProps 是否傳入 let dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) // 將 dispatch 方法和容器組件的 state 傳入 mapDispatchToProps : {} // 判斷 mapDispatchToProps 是否傳入 this.setState({ allProps: { ...stateProps, ...dispatchProps, ...this.props } }) } render () { // 將 state.allProps 展開以容器組件的 props 傳入 return <WrappedComponent {...this.state.allProps} /> } } return Connect }
小結
至此,我們就很清楚了,原來 React-redux 就是通過 Context 結合 Redux 來實現 React 應用的狀態管理,通過 Connect 這個高階組件來實現展示組件和容器組件的連接的。