關於React組件之間如何優雅地傳值的探討


閑話不多說,開篇擼代碼,你可以會看到類似如下的結構:

import React, { Component } from 'react';

// 父組件
class Parent extends Component {
    constructor() {
		super();
      	this.state = { color: 'red' };
    }
  
    render() {
		return <Child1 { ...this.props } />
    }
}

// 子組件1
const Child1 = props => {
  	return <Child2 { ...props } />
}

// 子組件2
const Child2 = props => {
  	return <Child3 { ...props } />
}

// 子組件3
const Child3 = props => {
  	return <div style={{ color: props.color }}>Red</div>
}

See the Pen react props by 糊一笑 (@rynxiao) on CodePen.

當一個組件嵌套了若干層子組件時,而想要在特定的組件中取得父組件的屬性,就不得不將props一層一層地往下傳,我這里只是簡單的列舉了3個子組件,而當子組件嵌套過深的時候,props的維護將成噩夢級增長。因為在每一個子組件上你可能還會對傳過來的props進行加工,以至於你最后都不確信你最初的props中將會有什么東西。

那么React中是否還有其他的方式來傳遞屬性,從而改善這種層層傳遞式的屬性傳遞。答案肯定是有的,主要還有以下兩種形式:

Redux等系列數據倉庫

使用Redux相當於在全局維護了整個應用數據的倉庫,當數據改變的時候,我們只需要去改變這個全局的數據倉庫就可以了。類似這樣的:

var state = {
  	a: 1
};

// index1.js
state.a = 2;

// index2.js
console.log(state.a);	// 2

當然這只是一種非常簡單的形式解析,Reudx中的實現邏輯遠比這個要復雜得多,有興趣可以去深入了解,或者看我之前的文章:用react+redux編寫一個頁面小demo以及react腳手架改造,下面大致列舉下代碼:

// actions.js
function getA() {
  return {
    	type: GET_DATA_A
  };
}

// reducer.js
const state = {
  	a: 1
};

function reducer(state, action) {
  	case GET_DATA_A: 
  		state.a = 2;
  		return state;
  	default:
  		return state;
}

module.exports = reducer;

// Test.js
class Test extends React.Component {
  	constructor() {
    	super();
  	}
  
    componentDidMount() {
		this.props.getA();
    }
}

export default connect(state => {
  	return { a: state.reducer.a }
}, dispatch => {
  	return { getA: dispatch => dispatch(getA()) }
})(Test);

這樣當在Test中的componentDidMount中調用了getA()之后,就會發送一個action去改變store中的狀態,此時的a已經由原先的1變成了2。

這只是一個任一組件的大致演示,這就意味着你可以在任何組件中來改變store中的狀態。關於什么時候引入redux我覺得也要根據項目來,如果一個項目中大多數時候只是需要跟組件內部打交道,那么引入redux反而造成了一種資源浪費,更多地引來的是學習成本和維護成本,因此並不是說所有的項目我都一定要引入redux

context

關於context的講解,React文檔中將它放在了進階指引里面。具體地址在這里:https://reactjs.org/docs/context.html。主要的作用就是為了解決在本文開頭列舉出來的例子,為了不讓props在每層的組件中都需要往下傳遞,而可以在任何一個子組件中拿到父組件中的屬性。

但是,好用的東西往往也有副作用,官方也給出了幾點不要使用context的建議,如下:

  • 如果你想你的應用處於穩定狀態,不要用context
  • 如果你不太熟悉Redux或者MobX等狀態管理庫,不要用context
  • 如果你不是一個資深的React開發者,不要用context

鑒於以上三種情況,官方更好的建議是老老實實使用propsstate

下面主要大致講一下context怎么用,其實在官網中的例子已經十分清晰了,我們可以將最開始的例子改一下,使用context之后是這樣的:

class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { color: 'red' };
    }
  
    getChildContext() {
        return { color: this.state.color }
    }
  
    render() {
		    return <Child1 />
    }
}

const Child1 = () => {
  	return <Child2 />
}

const Child2 = () => {
  	return <Child3 />
}

const Child3 = ({ children }, context) => {
    console.log('context', context);
  	return <div style={{ color: context.color }}>Red</div>
}

Parent.childContextTypes = {
    color: PropTypes.string
};

Child3.contextTypes = {
    color: PropTypes.string
};  

ReactDOM.render(<Parent />, document.getElementById('container'));

可以看到,在子組件中,所有的{ ...props }都不需要再寫,只需要在Parent中定義childContextTypes的屬性類型,以及定義getChildContext鈎子函數,然后再特定的子組件中使用contextTypes接收即可。

See the Pen react context by 糊一笑 (@rynxiao) on CodePen.

這樣做貌似十分簡單,但是你可能會遇到這樣的問題:當改變了context中的屬性,但是由於並沒有影響父組件中上一層的中間組件的變化,那么上一層的中間組件並不會渲染,這樣即使改變了context中的數據,你期望改變的子組件中並不一定能夠發生變化,例如我們在上面的例子中再來改變一下:

// Parent
render() {
  	return (
      	<div className="test">
      	<button onClick={ () => this.setState({ color: 'green' }) }>change color to green</button>  
  		<Child1 />
      </div>
	)
}

增加一個按鈕來改變state中的顏色

// Child2
class Child2 extends React.Component {
    
      shouldComponentUpdate() {
          return true;
      }

      render() {
          return <Child3 />
      }
}

增加shouldComponentUpdate來決定這個組件是否渲染。當我在shouldComponentUpdate中返回true的時候,一切都是那么地正常,但是當我返回false的時候,顏色將不再發生變化。

See the Pen react context problem by 糊一笑 (@rynxiao) on CodePen.

既然發生了這樣的情況,那是否意味着我們不能再用context,沒有絕對的事情,在這篇文章How to safely use React context中給出了一個解決方案,我們再將上面的例子改造一下:

// 重新定義一個發布對象,每當顏色變化的時候就會發布新的顏色信息
// 這樣在訂閱了顏色改變的子組件中就可以收到相關的顏色變化訊息了
class Theme {
    constructor(color) {
        this.color = color;
        this.subscriptions = [];
    }
  
    setColor(color) {
        this.color = color;
        this.subscriptions.forEach(f => f());
    }
  
    subscribe(f) {
      this.subscriptions.push(f)
    }
}

class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { theme: new Theme('red') };
        this.changeColor = this.changeColor.bind(this)
    }
  
    getChildContext() {
        return { theme: this.state.theme }
    }
  
    changeColor() {
        this.state.theme.setColor('green');
    }
  
    render() {
		    return (
            <div className="test">
              <button onClick={ this.changeColor }>change color to green</button>  
              <Child1 />
            </div>
        )
    }
}

const Child1 = () => {
  	return <Child2 />
}

class Child2 extends React.Component {
    
    shouldComponentUpdate() {
        return false;
    }
  
    render() {
        return <Child3 />
    }
}

// 子組件中訂閱顏色改變的信息
// 調用forceUpdate強制自己重新渲染
class Child3 extends React.Component {
    
    componentDidMount() {
        this.context.theme.subscribe(() => this.forceUpdate());
    }
  
    render() {
        return <div style={{ color: this.context.theme.color }}>Red</div>
    }
}

Parent.childContextTypes = {
    theme: PropTypes.object
};

Child3.contextTypes = {
    theme: PropTypes.object
};  

ReactDOM.render(<Parent />, document.getElementById('container'));

看上面的例子,其實就是一個訂閱發布者模式,一旦父組件顏色發生了改變,我就給子組件發送消息,強制調用子組件中的forceUpdate進行渲染。

See the Pen react context problem resolve by 糊一笑 (@rynxiao) on CodePen.

但在開發中,一般是不會推薦使用forceUpdate這個方法的,因為你改變的有時候並不是僅僅一個狀態,但狀態改變的數量只有一個,但是又會引起其他屬性的渲染,這樣會變得得不償失。

另外基於此原理實現的有一個庫: MobX,有興趣的可以自己去了解。

總體建議是:能別用context就別用,一切需要在自己的掌控中才可以使用。

總結

這是自己在使用React時的一些總結,本意是朝着偷懶的方向上去了解context的,但是在使用的基礎上,必須知道它使用的場景,這樣才能夠防范於未然。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM