前言
最近看了下React16.3的新文件,發現官方悄悄地改了很多東西了。其中我最感興趣的自然就是這個全新的Context API了。所以寫了這篇文章來總結分享一下。其他的變動在這篇文章裡或許會提及。
本文你可以在我的github上面找到,轉載請標註這個地址就行了。
什麼是Context API
Context API
是React提供的一種跨節點
資料訪問的方式。眾所周知,React是單向資料流的,Vue
裡面的props
也借鑑了這一思想。
但是很多時候,這種單向資料流的設定卻變得不是那麼友好。我們往往需要從更高層的節點獲取一些資料,如果使用傳統的prop
傳遞資料,就需要每一層都手動地向下傳遞。對於層次很高的元件,這種方法十分地煩人,極大地降低了工作效率。
於是,React使用了Context API
。Context API
存在已久,但是舊的Context API
存在很多問題,並且使用起來也並不是特別方便,官方並不建議使用老版本的Context API
。於是很多開發者選擇了Redux
之類的狀態管理工具。
受到Redux
的影響,React
在16.3.0版本中推出了全新的Context API
。
一些你需要提前知道的東西
-
眾所周知,長期起來JavaScript一直沒有模組系統。
nodejs
使用require
作為彌補方法。ECMAScript6
之後,引入了全新的import
語法標準。import
語法標準有個尤為重要的不同(相比較require
),那就是:import
匯入的資料是引用的。這意味著多個檔案匯入同一個資料,並不是匯入的拷貝,而是匯入的引用。 -
react@16.3的宣告檔案(d.ts)貌似沒有更新,意味著如果你現在使用Typescript,那麼可能會報錯。
-
React現在推薦使用
render props
,render props
為元件渲染的程式碼複用以及程式碼傳遞提供了新的思路,其實本質上就是通過props
傳遞HOC函式來控制元件的渲染。 -
或許你曾經聽過“
Context API
是用來替代Redux
”之類的傳聞,然而事實並非如此。Redux
和Context API
解決的問題並不一樣,會造成那樣的錯覺可能是因為他們的使用方法有點兒一樣。 -
React16.3有幾個新特性,最主要的變化是Context,還有就是廢除了幾個生命週期,比如
ComponentWillReceiveProps
(說實話,實際專案中,這個生命週期完全可以用ComponentWillUpdate
來替換) -
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
或許是個不錯的主意。在實際工程中,其實並不建議多層巢狀。更為適合的時,提供一對Provier
和Consumer
對,傳遞狀態管理工具對應的例項就行了。
在生命週期中使用
在之前的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
只暴露在Consumer
的render prop
裡面。個人覺得這是這個版本API的一個缺點。所以只有採用上面這種折中的方式,再包裝一個函式元件來封裝到props裡面去。相比較而言,還是麻煩了一點兒。在元件樹裡面多了一個函式元件,也是一個缺點。
Consumer封裝
當一個Context
的值多個元件都在使用的時候,你需要手動地每次都寫一次Consumer
和redner 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。這個用法就像是狀態提升
一樣。