React@16.3 全新的Context API進階教程

DoubleDimos發表於2018-04-05

前言

最近看了下React16.3的新文件,發現官方悄悄地改了很多東西了。其中我最感興趣的自然就是這個全新的Context API了。所以寫了這篇文章來總結分享一下。其他的變動在這篇文章裡或許會提及。

本文你可以在我的github上面找到,轉載請標註這個地址就行了。

什麼是Context API

Context API是React提供的一種跨節點資料訪問的方式。眾所周知,React是單向資料流的,Vue裡面的props也借鑑了這一思想。

但是很多時候,這種單向資料流的設定卻變得不是那麼友好。我們往往需要從更高層的節點獲取一些資料,如果使用傳統的prop傳遞資料,就需要每一層都手動地向下傳遞。對於層次很高的元件,這種方法十分地煩人,極大地降低了工作效率。

於是,React使用了Context APIContext API存在已久,但是舊的Context API存在很多問題,並且使用起來也並不是特別方便,官方並不建議使用老版本的Context API。於是很多開發者選擇了Redux之類的狀態管理工具。

受到Redux的影響,React在16.3.0版本中推出了全新的Context API

一些你需要提前知道的東西

  1. 眾所周知,長期起來JavaScript一直沒有模組系統。nodejs使用require作為彌補方法。ECMAScript6之後,引入了全新的import語法標準。import語法標準有個尤為重要的不同(相比較require),那就是:import匯入的資料是引用的。這意味著多個檔案匯入同一個資料,並不是匯入的拷貝,而是匯入的引用。

  2. react@16.3的宣告檔案(d.ts)貌似沒有更新,意味著如果你現在使用Typescript,那麼可能會報錯。

  3. React現在推薦使用render propsrender props為元件渲染的程式碼複用以及程式碼傳遞提供了新的思路,其實本質上就是通過props傳遞HOC函式來控制元件的渲染。

  4. 或許你曾經聽過“Context API是用來替代Redux”之類的傳聞,然而事實並非如此。ReduxContext API解決的問題並不一樣,會造成那樣的錯覺可能是因為他們的使用方法有點兒一樣。

  5. React16.3有幾個新特性,最主要的變化是Context,還有就是廢除了幾個生命週期,比如ComponentWillReceiveProps(說實話,實際專案中,這個生命週期完全可以用ComponentWillUpdate來替換)

  6. React16.3中的refs不再推薦直接傳遞一個函式了,而是使用了全新的React.createRef來替代。當然以前的方法依舊適用,畢竟是為了相容。

開始使用

React.createContext

createContext用來建立一個Context,它接受一個引數,這個引數會作為Context傳遞的預設值。需要注意的是,如果你傳入的引數是個物件,那麼當你更改Context的時候,內部會呼叫Object.is來比較物件是否相等。這會導致一些效能上的問題。當然,這並不重要,因為大部分情況下,這點兒效能損失可以忽略。

我們看下這個例子,這是一個提供主題(Light/Dark)型別的Context

// context.js
import * as React from 'react';
// 預設主題是Light
export const { Provider, Consumer } = React.createContext("Light");

複製程式碼

接下來我們只需要在需要的檔案裡import就行了

Provider

Provider是需要使用Context的所有元件的根元件。它接受一個value作為props,它表示Context傳遞的值,它會修改你在建立Context時候設定的預設值。

import { Provider } from './context';
import * as React from 'react';
import { render } from 'react-dom';
import App from './app';


const root = (
    <Provider value='Dark'>
        <App />
    </Provider>
);

render(root, document.getElementById('root'));


複製程式碼

Consumer

Consumer表示消費者,它接受一個render props作為唯一的children。其實就是一個函式,這個函式會接收到Context傳遞的資料作為引數,並且需要返回一個元件。

// app.jsx

import { Consumer } from './context';
import * as React from 'react';

export default class App extends React.Component {
    render() {
        return (
            <Consumer>
                {
                    theme => <div>Now, the theme is { theme }</div>
                }
            </Consumer>
        )
    }
}
複製程式碼

一些需要注意的地方

多層巢狀

Context為了確保重新渲染的快速性,React需要保證每個Consumer都是獨立的節點。

const ThemeContext = React.createContext('light');
const UserContext = React.createContext();

function Toolbar(props) {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Toolbar />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}
複製程式碼

當層次更加複雜的時候,會變得很煩人。因此推薦當層次超過兩層之後,建立一個自己的render prop或許是個不錯的主意。在實際工程中,其實並不建議多層巢狀。更為適合的時,提供一對ProvierConsumer對,傳遞狀態管理工具對應的例項就行了。

在生命週期中使用

在之前的Context API中,在一些宣告週期中會暴露一個context的引數,以供開發者更為方便的訪問。新版API並沒有這個引數傳遞了,更為推薦的方式是直接把Context的值通過props傳遞給元件。具體來說,就像下面這個官方的例子這樣。

class Button extends React.Component {
  componentDidMount() {
    // ThemeContext value is this.props.theme
  }

  componentDidUpdate(prevProps, prevState) {
    // Previous ThemeContext value is prevProps.theme
    // New ThemeContext value is this.props.theme
  }

  render() {
    const {theme, children} = this.props;
    return (
      <button className={theme ? 'dark' : 'light'}>
        {children}
      </button>
    );
  }
}

export default props => (
  <ThemeContext.Consumer>
    {theme => <Button {...props} theme={theme} />}
  </ThemeContext.Consumer>
);
複製程式碼

不像以前那樣,可以直接通過this.context訪問,新版本的Context只能在render方法裡面訪問。因為Context只暴露在Consumerrender prop裡面。個人覺得這是這個版本API的一個缺點。所以只有採用上面這種折中的方式,再包裝一個函式元件來封裝到props裡面去。相比較而言,還是麻煩了一點兒。在元件樹裡面多了一個函式元件,也是一個缺點。

Consumer封裝

當一個Context的值多個元件都在使用的時候,你需要手動地每次都寫一次Consumerredner prop。這是很煩的,程式設計師都是很懶的(至少我是這樣),因此這個時候利用一下React的HOC來封裝一下來簡化這個過程。

const ThemeContext = React.createContext('light');

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {theme => <button className={theme} {...props} />}
    </ThemeContext.Consumer>
  );
}
複製程式碼

接下來,當你需要使用Context的時候,就不需要在寫什麼Consumer

export default props => (
    ThemeButton(props)
);
複製程式碼

轉發refs

當你封裝完一個Consumer之後,或許你想要用ref來獲取Consumer裡面根元件的例項或者對應的DOM。如果直接在Consumer上使用ref,是得不到想要的結果的。於是在React16.3裡面,使用了一種全新的技術(不確定是不是16.3才引入的),叫做轉發refs 。不僅僅用在Context裡面,實際上,在任何你想要把ref傳遞給元件內部的子元件的時候,你都可以使用轉發refs

具體來說,你需要使用一個新的API:React.forwardRef((props, ref) => React.ReactElement),以下面這個為例:

class FancyButton extends React.Component {}

// Use context to pass the current "theme" to FancyButton.
// Use forwardRef to pass refs to FancyButton as well.
export default React.forwardRef((props, ref) => (
  <ThemeContext.Consumer>
      {
        theme => (
            <FancyButton {...props} theme={theme} ref={ref} />
        )
      }
  </ThemeContext.Consumer>
));
複製程式碼

React.forwardRef()接受一個函式作為引數。實際上,你可以將這個函式當做一個函式元件,它的第一個引數和函式元件一樣。不同的地方在於,它多了一個ref。這意味著如果你在React.forwardRef建立的元件上使用ref的話,它並不會直接被元件消化掉,而是向內部進行了轉發,讓需要消化它的元件去消化。

如果你覺得難以理解,其實這種方法完全可以用另一種方法替代。我們知道,在React中,ref並不會出現在props中,它被特殊對待。但是換個名字不就行了嗎。

需要提一下的是,以前我們獲取ref是傳遞一個函式(不推薦使用字串,這是一個歷史遺留的問題,ref會在某些情況下無法獲取到正確的值。vuejs可以使用,不要搞混了)。但是這個過程很煩的,我們只需要把例項或者DOM賦值給對應的變數就行了,每次都寫一下這個一樣模板的程式碼,很煩人的好嗎。“千呼萬喚”中,React終於聽到了。現在只需要React.createRef就可以簡化這個過程了。

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    render() {
        return <div ref={this.myRef} />;
    }
}
複製程式碼

使用方法就這麼簡單,沒什麼特別的地方。

回到上面的話題,現在我們用props來實現轉發refs的功能。

class Input extends React.Component {

    reder() {
		return (
			<label>Autofocus Input:</label>
			<input ref={this.props.forwardRef} type="text" />
		)
    }

}

function forwardRef(Component, ref) {
	return (<Component forwardRef={ref} />);
}

// 使用forwardRef
let input = React.createRef();

forwardRef(Input, input);


// 當元件繫結成功之後
 input.current.focus();

複製程式碼

React.createRef返回的值中,current屬性表示的就是對應的DOM或者元件例項。forwardRef並沒有什麼特殊的含義,就是一個簡單的props。這個用法就像是狀態提升一樣。

相關文章