React 16.3新的Context API真的那麼好嗎?

React v16.3還沒有正式發佈,但是已經預告了衆多新功能,其中很引人注意的是新的Context API,今天就來聊一聊這個。

關於這個新的Context API有很多表揚的聲音,但是我們可以和老的Context API做一個對比,發現可能並沒有想象的那麼好。

首先,React一直是有Context API的(我們姑且稱爲“老的Context API”),只是React團隊自己都不建議使用,如果你去看官方網站的介紹,可以看見Facebook對“老的Context API”的忠告:

If you want your application to be stable, don’t use context.
If you aren’t familiar with state management libraries like Redux or MobX, don’t use context.
If you’re still learning React, don’t use context.

如果你想要穩定應用,不要用Context。

如果你並不熟悉Redux或者Mobx這樣的狀態管理框架,不要用Context。

如果纔剛剛學React,不要用Context。

這就是Facebook給的建議,總之,能不用Context就不要用Context,Facebook明確說這個Context API只是“試驗性”的,不要用!

這裏真要加一個黑人問號,到底老的Context API有啥問題呢?因爲單獨看老的Context API沒有比較,也看不出來個之乎所以然,所以還是要和新的Context API對比着看。

我們還是先來看新的Context API是怎樣吧,因爲React v16.3還沒有正式發佈,所以不方便直接來玩一玩,但是有人已經做了一個polyfill叫create-react-context,行爲和新的Context API一致,可以直接拿來試一試。

首先,安裝create-react-context,然後代碼中就可以這麼寫。

新版Context API用法

import createReactContext from 'create-react-context';

等到React v16.3正式發佈之後,只需要改這一行,其他代碼不用碰,可以猜測上面這行代碼只需要改成下面這樣。

import {createReactContext} from 'react';

被導入的createReactContext,從名字的形式就知道是一個函數,用於創造一個Context對象,然後這個Context對象又包含兩個屬性,一個叫Provider,另一個叫Consumer,這兩個屬性都是純種的React組件。

在組件樹中,Provider負責提供context,而Consumer用來消費Provider提供的context,而且,它們之間可以隔着任意層級,依然保留心有靈犀,這就是Context的意義。

想象一下,如果沒有Context,如果把頂層組件的數據隔若干層傳給底層組件?

要麼用prop,一層一層地傳,要求每一層都要負責傳遞prop,丟了一個就完蛋了,這種方法當然是十分笨拙的,不可取。

要不然,就要利用一個全局性的對象,比如Redux中的Store,那就面臨如何管理好全局資源的問題,

左右爲難啊!

所以,最理想的方式是有一個——Context。

Context的一個典型應用場景是界面中的“主題”(Theme),包括顏色樣式等內容,主題的設定放在頂層的組件中,在這個頂層組件之下的所有React組件都應該能夠很方便地訪問主題,接下來就讓我們用“新的Context API”來解決這個問題。

簡單一點,我們只創造兩個主題:defaultTheme和fooTheme。

const defaultTheme = {
  background: 'white',
  color: 'black',
};
const fooTheme = {
  background: 'red',
  color: 'green',
}
const ThemeContext = createReactContext(defaultTheme);

有了ThemeContext,然後就可以使用ThemeContext.Provider和ThemeContext.Consumer,這兩者因爲都源於同一個ThemeContext對象,所以數據可以關聯起來。

首先我們看怎麼消費context。

const Banner = ({theme}) => {
  return (<div style={theme}>Welcome!</div>);
};

const Content = () => (
  <ThemeContext.Consumer>
    {
      context => {
        return <Banner theme={context} />
      }
    }
  </ThemeContext.Consumer>
);

注意,ThemeContext.Consumer使用的是render props這種模式,render props模式指的是讓prop可以是一個render函數。

(在我寫《深入淺出React和Redux》的時候,對這種模式還沒有明確說法,書中我稱這種爲child-as-a-function模式,不過,現在業界普遍都認可這種模式叫render props。)

ThemeContext.Consumer的子組件是一個函數,要知道子組件可以認爲是this.props.children,所以也屬於render props模式。

在上面的例子中,子組件就是下面的render函數。

    context => {
       return <Banner theme={context} />
     }

這個函數被調用的是偶,參數就是context,至於如何使用這個context,完全由這個函數來操縱,因爲這個函數中可以包含任意代碼,這種模式擁有相當大的自由度,到底使用context上哪些數據,如何使用這些數據,完全可以由code來定製。

接下來,看如何提供context,我們創造一個通過點擊按鈕切換主題的ThemeProvider。

class ThemeProvider extends React.Component {
  state = {
    theme: defaultTheme
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        <Content/>
        <div>
          <button onClick={() => {
            this.setState(state => ({
              theme: state.theme === defaultTheme ? fooTheme : defaultTheme
            }))
          }}>
            Toggle Theme
          </button>
        </div>
      </ThemeContext.Provider>
    );
  }
}

當Toggle Theme按鈕被點擊的時候,ThemeProvider的state被改變,state的改變引起ThemeProvider的重新渲染,重新渲染引起render函數被調用,從而引起ThemeContext.Provider的重新渲染,傳遞給value屬性的是最新的state,這樣,就把新的context值應用上了。

點擊Toggle Theme按鈕,Banner的色調會來回切換。

老的Context API用法

接下來,我們看老的Context API如何來做。

在老的Context API中,沒有所謂“Context對象”,無論消費還是提供context,都由React組件自己搞定。

先說消費者角度,一個組件如果要訪問context,要通過contextTypes聲明自己要訪問什麼樣的context,然後,就可以通過this.context訪問對應的context,對於本身就是一個函數的React組件,沒有this的概念,通過函數的第二個參數訪問context,像下面這樣。

const Banner = ({}, context) => {
  return (<div style={context.theme}>Welcome!</div>);
};

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

const Content = () => (
  <Banner />
);

嘿,當這套“試驗性”老的Context API被創造的時候,PropTypes還是React核心庫的一部分,現在PropTypes已經獨立出來了,從這個意義上說,這套API看起來真有點過時了。

從context提供角度來看,同樣,需要提供者明確聲明自己要提供什麼樣的context。

同樣是ThemeProvider,利用childContextType聲明context長得什麼樣子,通過getChildContext來提供context的實際值。

class ThemeProvider extends React.Component {
  state = {
    theme: defaultTheme
  }

  getChildContext() {
    return this.state;
  }

  render() {
    return (
      <div>
        <Content/>
        <div>
          <button onClick={() => {
            this.setState(state => {
              return {
                theme: state.theme === defaultTheme ? fooTheme : defaultTheme
              };
            })
          }}>
            Toggle Theme
          </button>
        </div>
      </div>
    );
  }
}

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

每當ThemeProvider被渲染的時候,它的getChildContext就會被調用,返回值就是它所提供的context。

新老兩代Context API的比較

好了,現在體會到新老Context API如何來用,可以開始吐槽了。

新的Context API獲得廣泛讚譽,我在社交網絡上瀏覽了這些讚譽之詞,發現無外乎兩個原因:

  1. 應用了render props模式,感覺好牛逼;
  2. React宣稱遷移到新的Context API之後,就會摘掉“試驗性”的帽子,感覺好牛逼。

雖然感覺好牛逼,但是並不表示沒有問題。

新的Context API通過創建一個Context對象來完成,在實際項目中,Provider和Consumer往往都存在於不同的源代碼文件中,如何讓他們訪問同一個Context對象呢?

一個最直接的方式,是在一個文件中定義Context對象,然後,這個文件被被其他文件來import。

可是,使用老的Context API並不需要這樣啊!

老的Context API雖然老,但是Provider和Consumer之間不需要共同依賴一個什麼對象來工作,只要都依賴React(這是當然)就足夠了。

新的Context API貌似用render props這種模式優雅地解決了context的問題,但是卻引入新的問題——如何管理context對象。

對於只有一個Context對象的場景,還比較好處理,但是,加入需要多個同種類的Context,那就麻煩了。比如,對於一個列表應用,列表中可以有任意多個列表項組件,如果要求每個列表項都要有自己的Context,那怎麼讓列表項之下的組件找到所屬的Context呢?

我腦子裏已經想出了一個解決方法,但是還是感覺麻煩,下次再來介紹,大家也可以想一想自己的解法。

新的Context有一個明顯實用好處,可以很清晰地讓多個Context交叉使用,比如組件樹上可以有兩個Context Provider,而Consumer總是可以區分開哪個context是哪個。

<Context1.Consumer>
  {
     context1 => (
       <Context2.Consumer>
         {
           context2 => {
              // 在這裏可以通過代碼選擇從context1或者context2中獲取數據
              return ...;
           }
         }
       </Context2.Consumer> 
     )
  }
</Context1.Consumer>

總結

總之,我個人的觀點就是:不要被對新事物的鼓吹衝昏頭腦,對於新東西,不光要看到它的好處,也要看到它帶來的麻煩。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章