React高級特性Context萬字詳細解讀
Context提供了一種不需要手動地通過props來層層傳遞的方式來傳遞數(shù)據(jù)。
正文
在典型的React應用中,數(shù)據(jù)是通過props,自上而下地傳遞給子組件的。但是對于被大量組件使用的固定類型的數(shù)據(jù)(比如說,本地的語言環(huán)境,UI主題等)來說,這么做就顯得十分的累贅和笨拙。Context提供了一種在組件之間(上下層級關(guān)系的組件)共享這種類型數(shù)據(jù)的方式。這種方式不需要你手動地,顯式地通過props將數(shù)據(jù)層層傳遞下去。
什么時候用Context?
這一小節(jié),講的是context適用的業(yè)務場景。
Context是為那些可以認定為【整顆組件樹范圍內(nèi)可以共用的數(shù)據(jù)】而設(shè)計的。比如說,當前已認證的用戶數(shù)據(jù),UI主題數(shù)據(jù),當前用戶的偏好語言設(shè)置數(shù)據(jù)等。舉個例子,下面的代碼中,為了裝飾Button component我們手動地將一個叫“theme”的prop層層傳遞下去。 傳遞路徑是:App -> Toolbar -> ThemedButton -> Button
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { // The Toolbar component must take an extra "theme" prop // and pass it to the ThemedButton. This can become painful // if every single button in the app needs to know the theme // because it would have to be passed through all components. return ( <div> <ThemedButton theme={props.theme} /> </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; } }
使用context,我們可以跳過層層傳遞所經(jīng)過的中間組件?,F(xiàn)在我們的傳遞路徑是這樣的:App -> Button。
// Context lets us pass a value deep into the component tree // without explicitly threading it through every component. // Create a context for the current theme (with "light" as the default). const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, we're passing "dark" as the current value. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // A component in the middle doesn't have to // pass the theme down explicitly anymore. function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } class ThemedButton extends React.Component { // Assign a contextType to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". static contextType = ThemeContext; render() { return <Button theme={this.context} />; } }
在你用Context之前
這一小節(jié),講的是我們要慎用context。在用context之前,我們得考慮一下當前的業(yè)務場景有沒有第二種技術(shù)方案可用。只有在確實想不出來了,才去使用context。
Context主要用于這種業(yè)務場景:大量處在組件樹不同層級的組件需要共享某些數(shù)據(jù)。實際開發(fā)中,我們對context要常懷敬畏之心,謹慎使用。因為它猶如潘多拉的盒子,一旦打開了,就造成很多難以控制的現(xiàn)象(在這里特指,context一旦濫用了,就會造成很多組件難以復用)。參考React實戰(zhàn)視頻講解:進入學習
如果你只是單純想免去數(shù)據(jù)層層傳遞時對中間層組件的影響,那么組件組合是一個相比context更加簡單的技術(shù)方案。
舉個例子來說,假如我們有一個叫Page
的組件,它需要將user
和avatarSize
這兩個prop傳遞到下面好幾層的Link
組件和Avatar
組件:
<Page user={user} avatarSize={avatarSize} /> // ... which renders ... <PageLayout user={user} avatarSize={avatarSize} /> // ... which renders ... <NavigationBar user={user} avatarSize={avatarSize} /> // ... which renders ... <Link href={user.permalink}> <Avatar user={user} size={avatarSize} /> </Link>
我們大費周章地將user
和avatarSize
這兩個prop傳遞下去,最終只有Avatar
組件才真正地用到它。這種做法顯得有點低效和多余的。假如,到后面Avatar
組件需要從頂層組件再獲取一些格外的數(shù)據(jù)的話,你還得手動地,逐層地將這些數(shù)據(jù)用prop的形式來傳遞下去。實話說,這真的很煩人。
不考慮使用context的前提下,另外一種可以解決這種問題的技術(shù)方案是:將Avatar
組件作為prop傳遞下去。這樣一來,其他中間層的組件就不要知道user
這個prop的存在了。
function Page(props) { const user = props.user; const userLink = ( <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> ); return <PageLayout userLink={userLink} />; } // Now, we have: <Page user={user} /> // ... which renders ... <PageLayout userLink={...} /> // ... which renders ... <NavigationBar userLink={...} /> // ... which renders ... {props.userLink}
通過這個改動,只有最頂層的組件Page
需要知道Link
組件和Avatar
組件需要用到“user”和“avatarSize”這兩個數(shù)據(jù)集。
在很多場景下,這種通過減少需要傳遞prop的個數(shù)的“控制反轉(zhuǎn)”模式讓你的代碼更干凈,并賦予了最頂層組件更多的控制權(quán)限。然而,它并不適用于每一個業(yè)務場景。因為這種方案會增加高層級組件的復雜性,并以此為代價來使得低層家的組件來變得更加靈活。而這種靈活性往往是過度的。
在“組件組合”這種技術(shù)方案中,也沒有說限定你一個組件只能有一個子組件,你可以讓父組件擁有多個的子組件?;蛘呱踔两o每個單獨的子組件設(shè)置一個單獨的“插槽(slots)”,正如這里所介紹的那樣。
function Page(props) { const user = props.user; const content = <Feed user={user} />; const topBar = ( <NavigationBar> <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> </NavigationBar> ); return ( <PageLayout topBar={topBar} content={content} /> ); }
這種模式對于大部分需要將子組件從它的父組件中分離開來的場景是足夠有用的了。如果子組件在渲染之前需要與父組件通訊的話,你可以進一步考慮使用render props技術(shù)。
然而,有時候你需要在不同的組件,不同的層級中去訪問同一份數(shù)據(jù),這種情況下,還是用context比較好。Context負責集中分發(fā)你的數(shù)據(jù),在數(shù)據(jù)改變的同時,能將新數(shù)據(jù)同步給它下面層級的組件。第一小節(jié)給出的范例中,使用context比使用本小節(jié)所說的“組件組合”方案更加的簡單。適用context的場景還包括“本地偏好設(shè)置數(shù)據(jù)”共享,“UI主題數(shù)據(jù)”共享和“緩存數(shù)據(jù)”共享等。
相關(guān)API
React.createContext
const MyContext = React.createContext(defaultValue);
該API是用于創(chuàng)建一個context object(在這里是指Mycontext)。當React渲染一個訂閱了這個context object的組件的時候,將會從離這個組件最近的那個Provider
組件讀取當前的context值。
創(chuàng)建context object時傳入的默認值只有組件在上層級組件樹中沒有找到對應的的Provider組件的時候時才會使用。這對于脫離Provider組件去單獨測試組件功能是很有幫助的。注意:如果你給Provider組件value屬性提供一個undefined值,這并不會引用React使用defaultValue作為當前的value值。也就是說,undefined仍然是一個有效的context value。
Context.Provider
<MyContext.Provider value={<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->/* some value */}>
每一個context object都有其對應的Provider組件。這個Provider組件使得Consumer組件能夠訂閱并追蹤context數(shù)據(jù)。
它接受一個叫value的屬性。這個value屬性的值將會傳遞給Provider組件所有的子孫層級的Consumer組件。這些Consumer組件會在Provider組件的value值發(fā)生變化的時候得到重新渲染。從Provider組件到其子孫Consumer組件的這種數(shù)據(jù)傳播不會受到shouldComponentUpdate(這個shouldComponentUpdate應該是指Cousumer組件的shouldComponentUpdate)這個生命周期方法的影響。所以,只要父Provider組件發(fā)生了更新,那么作為子孫組件的Consumer組件也會隨著更新。
判定Provider組件的value值是否已經(jīng)發(fā)生了變化是通過使用類似于Object.is算法來對比新舊值實現(xiàn)的。
注意:當你給在Provider組件的value屬性傳遞一個object的時候,用于判定value是否已經(jīng)發(fā)生改變的法則會導致一些問題,見注意點。
Class.contextType
譯者注:官方文檔給出的關(guān)于這個API的例子我并沒有跑通。不知道是我理解錯誤還是官方的文檔有誤,讀者誰知道this.context在new context API中是如何使用的,麻煩在評論區(qū)指教一下。
class MyClass extends React.Component { componentDidMount() { let value = this.context; /* perform a side-effect at mount using the value of MyContext */ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* render something based on the value of MyContext */ } } MyClass.contextType = MyContext;
組件(類)的contextType靜態(tài)屬性可以賦值為一個context object。這使得這個組件類可以通過this.context來消費離它最近的context value。this.context在組件的各種生命周期方法都是可訪問的。
注意:
使用這個API,你只可以訂閱一個context object。如果你需要讀取多個context object,那么你可以查看Consuming Multiple Contexts。
如果你想使用ES7的實驗性特征public class fields syntax,你可以使用static關(guān)鍵字來初始化你的contextType屬性:
class MyClass extends React.Component { static contextType = MyContext; render() { let value = this.context; /* render something based on the value */ } }
Context.Consumer
<MyContext.Consumer> {value => /* render something based on the context value */} </MyContext.Consumer>
Consumer組件是負責訂閱context,并跟蹤它的變化的組件。有了它,你就可以在一個function component里面對context發(fā)起訂閱。
如上代碼所示,Consumer組件的子組件要求是一個function(注意,這里不是function component)。這個function會接收一個context value,返回一個React node。這個context value等同于離這個Consumer組件最近的Provider組件的value屬性值。假如Consumer組件在上面層級沒有這個context所對應的Provider組件,則function接收到的context value就是創(chuàng)建context object時所用的defaultValue。
注意:這里所說的“function as a child”就是我們所說的render props模式。
示例
1. 動態(tài)context
我在這個例子里面涉及到this.context的組件的某個生命周期方法里面打印console.log(this.context),控制臺打印出來是空對象。從界面來看,DOM元素button也沒有background。
這是一個關(guān)于動態(tài)設(shè)置UI主題類型的context的更加復雜的例子:
theme-context.js
export const themes = { light: { foreground: '#000000', background: '#eeeeee', }, dark: { foreground: '#ffffff', background: '#222222', }, }; export const ThemeContext = React.createContext( themes.dark // default value );
themed-button.js
import {ThemeContext} from './theme-context'; class ThemedButton extends React.Component { render() { let props = this.props; let theme = this.context; return ( <button {...props} style={{backgroundColor: theme.background}} /> ); } } ThemedButton.contextType = ThemeContext; export default ThemedButton;
app.js
import {ThemeContext, themes} from './theme-context'; import ThemedButton from './themed-button'; // An intermediate component that uses the ThemedButton function Toolbar(props) { return ( <ThemedButton onClick={props.changeTheme}> Change Theme </ThemedButton> ); } class App extends React.Component { constructor(props) { super(props); this.state = { theme: themes.light, }; this.toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); }; } render() { // The ThemedButton button inside the ThemeProvider // uses the theme from state while the one outside uses // the default dark theme // 以上注釋所說的結(jié)果,我并沒有看到。 return ( <Page> <ThemeContext.Provider value={this.state.theme}> <Toolbar changeTheme={this.toggleTheme} /> </ThemeContext.Provider> <Section> <ThemedButton /> </Section> </Page> ); } } ReactDOM.render(<App />, document.root);
2. 在內(nèi)嵌的組件中更新context
組件樹的底層組件在很多時候是需要更新Provider組件的context value的。面對這種業(yè)務場景,你可以在創(chuàng)建context object的時候傳入一個function類型的key-value,然后伴隨著context把它傳遞到Consumer組件當中:
theme-context.js
// Make sure the shape of the default value passed to // createContext matches the shape that the consumers expect! export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, });
theme-toggler-button.js
import {ThemeContext} from './theme-context'; function ThemeTogglerButton() { // The Theme Toggler Button receives not only the theme // but also a toggleTheme function from the context return ( <ThemeContext.Consumer> {({theme, toggleTheme}) => ( <button onClick={toggleTheme} style={{backgroundColor: theme.background}}> Toggle Theme </button> )} </ThemeContext.Consumer> ); } export default ThemeTogglerButton;
app.js
import {ThemeContext, themes} from './theme-context'; import ThemeTogglerButton from './theme-toggler-button'; class App extends React.Component { constructor(props) { super(props); this.toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); }; // State also contains the updater function so it will // be passed down into the context provider this.state = { theme: themes.light, toggleTheme: this.toggleTheme, }; } render() { // The entire state is passed to the provider return ( <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); } } function Content() { return ( <div> <ThemeTogglerButton /> </div> ); } ReactDOM.render(<App />, document.root);
3. 同時消費多個context
為了使得context所導致的重新渲染的速度更快,React要求我們對context的消費要在單獨的Consumer組件中去進行。
// Theme context, default to light theme const ThemeContext = React.createContext('light'); // Signed-in user context const UserContext = React.createContext({ name: 'Guest', }); class App extends React.Component { render() { const {signedInUser, theme} = this.props; // App component that provides initial context values // 兩個context的Provider組件嵌套 return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ); } } function Layout() { return ( <div> <Sidebar /> <Content /> </div> ); } // A component may consume multiple contexts function Content() { return ( // 兩個context的Consumer組件嵌套 <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext.Consumer> )} </ThemeContext.Consumer> ); }
但是假如兩個或以上的context經(jīng)常被一同消費,這個時候你得考慮合并它們,使之成為一個context,并創(chuàng)建一個接受多個context作為參數(shù)的render props component。
注意點
因為context是使用引用相等(reference identity)來判斷是否需要re-redner的,所以當你給Provider組件的value屬性提供一個字面量javascript對象值時,這就會導致一些性能問題-consumer組件發(fā)生不必要的渲染。舉個例子,下面的示例代碼中,所有的consumer組件將會在Provider組件重新渲染的時候跟著一起re-render。這是因為每一次value的值都是一個新對象。
class App extends React.Component { render() { return ( // {something: 'something'} === {something: 'something'}的值是false <Provider value={{something: 'something'}}> <Toolbar /> </Provider> ); } }
為了避免這個問題,我們可以把這種引用類型的值提升到父組件的state中去:
class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'}, }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } }
遺留的API
React在先前的版本中引入了一個實驗性質(zhì)的context API。相比當前介紹的這個context API,我們稱它為老的context API。這個老的API將會被支持到React 16.x版本結(jié)束前。但是你的app最好將它升級為上文中所介紹的新context API。這個遺留的API將會在未來的某個大版本中去除掉。
到此這篇關(guān)于React高級特性Context萬字詳細解讀的文章就介紹到這了,更多相關(guān)React Context內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決React報錯React?Hook?useEffect?has?a?missing?dependency
這篇文章主要為大家介紹了解決React報錯React?Hook?useEffect?has?a?missing?dependency,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12reactjs學習解決unknown at rule @tailwind css
這篇文章主要介紹了reactjs學習解決unknown at rule @tailwind css問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02React+TS+IntersectionObserver實現(xiàn)視頻懶加載和自動播放功能
通過本文的介紹,我們學習了如何使用 React + TypeScript 和 IntersectionObserver API 來實現(xiàn)一個視頻播放控制組件,該組件具有懶加載功能,只有在用戶滾動頁面且視頻進入視口時才開始下載視頻資源,需要的朋友可以參考下2023-04-0430分鐘精通React今年最勁爆的新特性——React Hooks
這篇文章主要介紹了30分鐘精通React今年最勁爆的新特性——React Hooks,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03