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

閒話很少說,開篇擼代碼,你能夠會看到相似以下的結構:javascript

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.html

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

那麼React中是否還有其餘的方式來傳遞屬性,從而改善這種層層傳遞式的屬性傳遞。答案確定是有的,主要還有如下兩種形式:react

Redux等系列數據倉庫

使用Redux至關於在全局維護了整個應用數據的倉庫,當數據改變的時候,咱們只須要去改變這個全局的數據倉庫就能夠了。相似這樣的:git

var state = {
    a: 1
};

// index1.js
state.a = 2;

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

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

// 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。redux

這只是一個任一組件的大體演示,這就意味着你能夠在任何組件中來改變store中的狀態。關於何時引入redux我以爲也要根據項目來,若是一個項目中大多數時候只是須要跟組件內部打交道,那麼引入redux反而形成了一種資源浪費,更多地引來的是學習成本和維護成本,所以並非說全部的項目我都必定要引入redux函數

context

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

可是,好用的東西每每也有反作用,官方也給出了幾點不要使用context的建議,以下:this

  • 若是你想你的應用處於穩定狀態,不要用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的,可是在使用的基礎上,必須知道它使用的場景,這樣纔可以防範於未然。

相關文章
相關標籤/搜索