- 原文地址:Redux vs. The React Context API
- 原文做者:Dave Ceddia
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Xuyuey
- 校對者:Minghao, Baddyo
React 在 16.3 版本里面引入了新的 Context API —— 說它是新的是由於老版本的 context API 是一個幕後的試驗性功能,大多數人要麼不知道,要麼就是依據官方文檔所說,儘可能避免使用它。css
可是,如今 Context API 搖身一變成爲了 React 中的一等公民,對全部人開放(不像是以前那樣,如今是被官方所提倡使用的)。前端
React 16.3 版本一發布,宣稱新的 Context API 將要取締 Redux 的文章在網上鋪天蓋地而來。可是,若是你去問問 Redux,我認爲它會說「那些宣告我會死亡的報道實在是言過其實」。react
在這篇文章中,我想向你們介紹一下新的 Context API 是如何工做的,它與 Redux 的類似之處,什麼狀況下可使用 Context API 而不是 Redux,以及爲何不是全部狀況下 Context API 均可以替換 Redux 的緣由。android
若是你只是想了解 Context 的概述,能夠跳轉到這一節。ios
這裏假設你已經瞭解了 React 的基礎知識(props 和 state),可是若是你尚未,你能夠參加個人 5 天免費課程,來學習 React 基礎知識。git
讓咱們看一個可讓大多數人接觸 Redux 的例子。咱們將從一個單純的 React 版本開始介紹,而後看看它在 Redux 中的樣子,最後是 Context。github
在該應用中用戶信息顯示在兩個位置:導航欄的右上角以及主要內容旁邊的側邊欄。ajax
(你可能會注意到它看起來很像 Twitter。這絕對不是碰巧的!磨練 React 技能的最佳方法之一就是經過複製 —— 構建現有應用的副本)。redux
組件結構以下所示:後端
使用純 React(僅僅是常規的 props),咱們須要在組件樹中足夠高的位置存儲用戶信息,這樣咱們才能夠將它向下傳遞給每個須要它的組件。在咱們的例子中,用戶信息必須存儲在 App
中。
接着,爲了將用戶信息向下傳遞給須要它的組件,App 須要先將它傳遞給 Nav 和 Body。而後,再次向下傳遞給 UserAvatar(萬歲!終於到了)和 Sidebar。最後,Sidebar 還要再將它傳遞給 UserStats。
讓咱們來看看代碼是怎麼工做的(爲了方便閱讀,我將全部的內容放在一個文件內,但實際上這些內容可能會按照某種標準結構分紅幾個文件)。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserAvatar = ({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
);
const UserStats = ({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
);
const Nav = ({ user }) => (
<div className="nav">
<UserAvatar user={user} size="small" />
</div>
);
const Content = () => <div className="content">main content here</div>;
const Sidebar = ({ user }) => (
<div className="sidebar">
<UserStats user={user} />
</div>
);
const Body = ({ user }) => (
<div className="body">
<Sidebar user={user} />
<Content user={user} />
</div>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
const { user } = this.state;
return (
<div className="app">
<Nav user={user} />
<Body user={user} />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
複製代碼
在這裏,App
初始化 state 時已經包含了 「user」 對象 —— 可是在真實應用中你可能會須要從服務器上獲取該數據並將它保存在 state 中,以便渲染。
這種 prop drilling(譯者注:屬性的向下傳遞)的方式,並不是糟糕的作法。它工做的還不錯。並非全部狀況下都不鼓勵 「prop drilling」;它是一種完美的有效模式,是支持 React 工做的核心。可是若是組件的層次太深,在你編寫的時候就會有點煩人。特別是當你向下傳遞不止一個屬性,而是一大堆的時候,它會變得更加煩人。
然而,這種 「prop drilling」 策略有一個更大的缺點:它會讓本應該獨立的組件耦合在一塊兒。在上面的例子中,Nav
組件須要接收一個 「user」 屬性,再將它傳遞給 UserAvatar
,即便 Nav
中沒有任何其它的地方須要用到 user
屬性。
緊密耦合的組件(就像那些向它們的子組件傳遞屬性的組件)更加難以被複用,由於不管何時你要在新的地方使用它,你都必須將它們和新的父組件聯繫起來。
讓咱們來看看如何改進。
若是你能夠找到一種方法來合併應用的結構,並利用好 children
屬性,這樣,無需藉助深層次的 prop drilling 或是 Context,或是 Redux,你也可讓代碼結構變得更清晰。
對於那些須要使用通用佔位符的組件,例如本例中的 Nav
、Sidebar
和 Body
,children 屬性是一個很好的解決方案。還要知道,你能夠傳遞 JSX 元素給任意屬性,並不只僅是 「children」 —— 因此若是你想使用不止一個 「slot」 來插入組件時,請記住這一點。
這個例子中 Nav
、Sidebar
和 Body
接收 children,而後按照它們的樣子渲染出來。這樣,組件的使用者不用擔憂傳遞給組件的特定數據 —— 他只須要使用組件內定義的數據,並按照組件的原始需求簡單地渲染組件。這個例子中還說明了怎樣使用任意屬性傳遞 children。
(感謝 Dan Abramov 的這個建議!)
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserAvatar = ({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
);
const UserStats = ({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
);
// 接收並渲染 children
const Nav = ({ children }) => (
<div className="nav">
{children}
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
const Sidebar = ({ children }) => (
<div className="sidebar">
{children}
</div>
);
// Body 須要一個 sidebar 和 content,可是能夠按照這樣的方式寫,
// 它們能夠是任意屬性
const Body = ({ sidebar, content }) => (
<div className="body">
<Sidebar>{sidebar}</Sidebar>
{content}
</div>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
const { user } = this.state;
return (
<div className="app">
<Nav>
<UserAvatar user={user} size="small" />
</Nav>
<Body
sidebar={<UserStats user={user} />}
content={<Content />}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
複製代碼
若是你的應用太複雜了(比這個例子更復雜!),也許很難弄清楚如何調整 children
模式。讓咱們來看看如何用 Redux 替換 prop drilling。
這裏我會快速過一下 Redux 示例,這樣咱們能夠多用點時間深刻地瞭解 Context 的工做原理,因此若是你不是很清楚 Redux,能夠先去看看個人 Redux 簡介(或者觀看視頻)。
咱們使用的仍是上面的 React 應用,這裏咱們將它重構爲 Redux 版本。user
信息被移入了 Redux 存儲,這意味着咱們可使用 react-redux 的 connect
函數,直接將 user
屬性注入到須要它的組件中。
這在解耦方面是一個巨大的勝利。看看 Nav
、Sidebar
和 Body
,你會發現它們再也不接收和向下傳遞 user
屬性了。不用再玩 props 這塊燙手山芋了。固然也不會有更多沒必要要的耦合。
這裏的 reducer 沒有作不少工做;很是的簡單。我在其它地方有更多關於 Redux Reducer 如何工做以及如何編寫其中的不可變代碼的文章,你能夠看看。
import React from "react";
import ReactDOM from "react-dom";
// 咱們須要 createStore、connect 和 Provider:
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
// 建立一個初始 state 爲空的 reducer
const initialState = {};
function reducer(state = initialState, action) {
switch (action.type) {
// 響應 SET_USER 行爲並更新
// 相應的 state
case "SET_USER":
return {
...state,
user: action.user
};
default:
return state;
}
}
// 使用 reducer 建立 store
const store = createStore(reducer);
// 觸發設置 user 的行爲
// (由於 user 初始化時爲空)
store.dispatch({
type: "SET_USER",
user: {
avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
});
// 函數 mapStateToProps 從 state 對象中提取 user 值
// 並將它做爲 `user` 屬性傳遞
const mapStateToProps = state => ({
user: state.user
});
// connect() UserAvatar 以便它能夠直接接收 `user` 屬性,
// 而無需從上層組件中獲取
// 也能夠把它分紅下面 2 個變量:
// const UserAvatarAtom = ({ user, size }) => ( ... )
// const UserAvatar = connect(mapStateToProps)(UserAvatarAtom);
const UserAvatar = connect(mapStateToProps)(({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
// connect() UserStats 以便它能夠直接接收 `user` 屬性,
// 而無需從上層組件中獲取
// (一樣使用 mapStateToProps 函數)
const UserStats = connect(mapStateToProps)(({ user }) => (
<div className="user-stats">
<div>
<UserAvatar />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
));
// Nav 再也不須要知道 `user` 屬性
const Nav = () => (
<div className="nav">
<UserAvatar size="small" />
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
// Sidebar 也再也不須要知道 `user` 屬性
const Sidebar = () => (
<div className="sidebar">
<UserStats />
</div>
);
// body 一樣不須要知道 `user` 屬性
const Body = () => (
<div className="body">
<Sidebar />
<Content />
</div>
);
// App 再也不須要保存 state,
// 因此能夠把它寫成一個無狀態組件
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
// 用 Provider 包裹整個 App,
// 以便 connect() 能夠鏈接到 store
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
複製代碼
如今你可能想知道 Redux 如何能實現這樣神奇的功能。「想知道」是一件好事情。React 不支持跨越多個層級傳遞屬性,那爲什麼 Redux 能夠實現呢?
答案是 Redux 使用了 React 的 context(上下文)特性。不是如今咱們說的 Context API(還不是)—— 而是舊的那個。就是 React 文檔說不要使用的那個,除非你在寫庫文件或者你知道在作什麼。
Context 就像一個在每一個組件背後運行的電子總線:要接收它傳遞的電源(數據),你只須要插入插頭就好。而(React-)Redux 的 connect
函數就是作這件事的。
不過,Redux 的這個功能只是冰山一角。能夠在全部地方傳遞數據只是 Redux 最明顯的功能。如下是你能夠開箱即用的其餘一些好處:
connect
使你的組件很純粹connect
可讓被鏈接的組件很「純粹」,意味着它們只須要在本身的屬性改變時從新渲染 —— 也就是在它們的 Redux 狀態切片發生改變時。這能夠防止沒必要要的重複渲染,使你的應用可以快速運行。DIY 方法:建立一個類繼承 PureComponent
,或是本身實現 shouldComponentUpdate
。
雖然寫 action 和 reducer 有一點複雜,可是咱們可使用它提供給咱們的強大調試能力來平衡這一點。
使用 Redux DevTools 擴展,應用程序執行的每一個操做都會被自動記錄下來。你能夠隨時打開它,查看觸發的操做,有效負載是什麼,以及操做發生先後的 state。
Redux DevTools 提供了另外一個很棒的功能 —— time travel debugging(時間旅行調試),也就是說,你能夠點擊任何過去的動做並跳轉到那個時間點,它基本上能夠重放每個動做,包括如今的那個,但不包括尚未觸發的動做。其原理是每一個動做都會不可變地更新 state,因此你能夠拿到記錄了 state 更新的列表並重放它們,跳轉到你想去的地方,並且沒有任何反作用。
並且目前有像 LogRocket 這樣的工具,能夠爲你的每個用戶在生產環境中提供一個永遠在線的 Redux DevTools。有 bug 報告?不要緊。在 LogRocket 中查找該用戶的會話,你能夠看到他們所作的全部事情以及確切觸發的操做。這一切均可以經過使用 Redux 的操做流來實現。
Redux 支持中間件(middleware)的概念,表明着「每次調度某個動做以前都會運行的函數」。編寫本身的中間件並不像看起來那麼難,它能夠實現一些強大的功能。
例如……
FETCH_
開頭的操做中提交 API 請求?你可使用中間件。這裏有一篇很好的文章,裏面有一些如何編寫 Redux 中間件的示例。
可是,也許你不須要 Redux 全部那些花哨的功能。也許你不關心簡單調試、自定義或是性能的自動化提高 —— 你想作的只是輕鬆地傳遞數據。也許你的應用很小,或者如今你只是須要讓應用運轉起來,之後再去考慮那些花哨的東西。
React 的新 Context API 可能符合你的要求。讓咱們看看它是如何工做的。
若是你更願意看視頻(時長 3:43)而不是讀文章,我在 Egghead 上發佈了一個簡短的 Context API 課程:
Context API 中有 3 個重要的部分:
React.createContext
函數:建立上下文Provider
(由 createContext
返回):在組件樹中構建「電子總線」Consumer
(一樣由 createContext
返回):接入「電子總線」來獲取數據這裏的 Provider
和 React-Redux 的 Provider
很是類似。它接收一個 value
屬性,這個屬性能夠是任何你想要的東西(甚至能夠是一個 Redux store……可是這很傻)。它極可能是一個對象,包括你的數據以及你但願對數據執行的操做。
這裏的 Consumer
工做方式有點像 React-Redux 的 connect
函數,接收數據以供組件使用。
如下是重點:
// 在最開始,咱們建立了一個新的上下文
// 它是一個擁有兩個屬性 { Provider, Consumer } 的對象
// 注意這裏用的是 UpperCase 命名,不是 camelCase
// 這很重要,由於咱們一會要以組件的方式使用它
// 而組件的名稱必須以大寫字母開頭
const UserContext = React.createContext();
// 下面是須要從上下文中獲取數據的組件
// 能夠經過使用 UserContext 的 Consumer 屬性
// Consumer 使用的是 "render props" 模式
const UserAvatar = ({ size }) => (
<UserContext.Consumer>
{user => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
)}
</UserContext.Consumer>
);
// 注意咱們再也不須要 'user' 屬性了
// 由於 Consumer 能夠直接從上下文中獲取
const UserStats = () => (
<UserContext.Consumer>
{user => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
)}
</UserContext.Consumer>
);
// …… 全部其它的組件 ……
// ……(就是那些不會用到 `user` 的組件)……
// 在最下面,App 的內部
// 咱們用 Provider 在整棵樹中傳遞上下文
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<div className="app">
<UserContext.Provider value={this.state.user}>
<Nav />
<Body />
</UserContext.Provider>
</div>
);
}
}
複製代碼
這裏是 CodeSandbox 中的完整示例。
讓咱們來看看它是如何工做的。
記住有 3 個部分:上下文自己(由 React.createContext
建立),以及和它對話的兩個組件(Provider
和 Consumer
)。
Provider 和 Consumer 被捆綁在一塊兒。如影隨行。並且它們只知道如何和對方對話。若是你建立兩個單獨的上下文,例如 「Context1」 和 「Context2」,那麼 Context1 的 Provider 和 Consumer 是不可能和 Context2 的 Provider 和 Consumer 通訊的。
注意上下文沒有本身的 state。它只是數據的管道。你必須將值傳遞給 Provider
,而後這個確切的值會被傳遞給任何知道如何獲取它的 Consumer
(Consumer 和 Provider 綁定的是同一個上下文)。
建立上下文時,能夠傳入一個「默認值」,以下所示:
const Ctx = React.createContext(yourDefaultValue);
複製代碼
當 Consumer
被放在一個沒有 Provider
包裹的樹上時,它會收到這個默認值。若是你沒有傳入默認值,這個值會爲 undefined
。但要注意這是默認值,而不是初始值。上下文不保留任何內容;它只是分發你傳入的數據。
Redux 的 connect
函數是一個高階組件(或簡稱 HoC)。它包裹另一個組件,並將 props 傳遞給它。
上下文的 Consumer
則相反,它指望子組件是一個函數。而後它在渲染的時候調用這個函數,將它從包裹它的 Provider
上得到的值(或上下文的默認值,若是你沒有傳入默認值,那也多是 undefined
)傳給子組件。
它接收 value
屬性,僅此一個值。但請記住這個值能夠是任何東西。在實踐中,若是你想要向下傳遞多個值,你必須建立一個包含這些值的對象,再將這個對象傳遞下去。
這幾乎是 Context API 的最核心的東西。
由於建立上下文爲咱們提供了兩個可使用的組件(Provider 和 Consumer),所以咱們能夠隨意使用它們。這裏有幾個想法。
不喜歡在每一個須要使用 UserContext.Consumer
的地方都添加它的用法?嗯,這是你的代碼!你能夠作任何你想作的事。你是個成年人了。
若是你更願意接收一個做爲屬性的值,你能夠爲 Consumer
寫一個包裹器,像下面這樣:
function withUser(Component) {
return function ConnectedComponent(props) {
return (
<UserContext.Consumer>
{user => <Component {...props} user={user}/>}
</UserContext.Consumer>
);
}
}
複製代碼
而後你能夠重寫你的代碼,好比使用了新 withUser
函數的 UserAvatar
組件:
const UserAvatar = withUser(({ size, user }) => (
<img className={`user-avatar ${size || ""}`} alt="user avatar" src={user.avatar} /> )); 複製代碼
BOOM,上下文能夠像 Redux 的 connect
那樣工做。讓你的組件很純粹。
記住,上下文的 Provider 只是一個管道。它不保留任何數據。但這並不能阻止你製做本身的包裹器來保存數據。
在上面的示例中,我用 App
保存數據,所以這裏你惟一須要瞭解的新事物就是這個 Provider + Consumer 組件。但也許你想寫一個本身的 「store」,等等。你能夠建立一個組件來保存數據,並經過上下文傳遞它們。
class UserStore extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<UserContext.Provider value={this.state.user}> {this.props.children} </UserContext.Provider> ); } } // ……略過中間的內容…… const App = () => ( <div className="app"> <Nav /> <Body /> </div> ); ReactDOM.render( <UserStore> <App /> </UserStore>, document.querySelector("#root") ); 複製代碼
如今,你的用戶數據被很好地包含在它本身的組件中了,這個組件惟一關注的就是用戶數據。很棒。App
又能夠再次變成無狀態組件了。我認爲它看起來更整潔了。
這裏是帶有這個 UserStore 的 CodeSandbox 示例。
記住經過 Provider 傳遞的對象能夠包含你想要的任何東西。這意味着它能夠包含函數。你甚至能夠稱之爲「操做(action)」。
這是一個新例子:一個簡單的房間,帶有一個能夠切換背景顏色的開關 —— 抱歉,個人意思是燈光。
State 被保存在 store 中,store 中還有切換燈光的函數。State 和函數都經過上下文傳遞。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
// 簡單的空上下文
const RoomContext = React.createContext();
// 一個組件
// 惟一的工做就是管理 Room 的 state
class RoomStore extends React.Component {
state = {
isLit: false
};
toggleLight = () => {
this.setState(state => ({ isLit: !state.isLit }));
};
render() {
// 傳遞 state 和 onToggleLight 操做
return (
<RoomContext.Provider
value={{
isLit: this.state.isLit,
onToggleLight: this.toggleLight
}}
>
{this.props.children}
</RoomContext.Provider>
);
}
}
// 從 RoomContext 中接收燈光的 state
// 以及切換燈光的函數
const Room = () => (
<RoomContext.Consumer>
{({ isLit, onToggleLight }) => (
<div className={`room ${isLit ? "lit" : "dark"}`}>
The room is {isLit ? "lit" : "dark"}.
<br />
<button onClick={onToggleLight}>Flip</button>
</div>
)}
</RoomContext.Consumer>
);
const App = () => (
<div className="app">
<Room />
</div>
);
// 用 RoomStore 包裹整個 App
// 它能夠像在 `App` 內那樣工做
ReactDOM.render(
<RoomStore>
<App />
</RoomStore>,
document.querySelector("#root")
);
複製代碼
這裏是 CodeSandbox 中的完整示例。
既然你已經看過兩種方式了 —— 那你應該使用哪一種方式呢?好吧,這裏有一件事會讓你的應用更好而且寫起來更有趣,那就是作決策。我知道你可能只想要「答案」,但我很遺憾地告訴你,「這視狀況而定」。
這取決於你的應用程序有多大或將會變成多大。有多少人會參與其中 —— 只有你仍是有更大的團隊?你或你的團隊對於 Redux 所依賴的函數式概念(如不變性和純函數)的經驗。
在 JavaScript 生態系統中存在的一個巨大的惡性謬論是競爭的概念。有觀點認爲,每一次選擇都是一個零和遊戲;若是你使用庫 A,你就不能使用它的競爭對手庫 B。這個想法是說當出現了一個在某種程度上更好的新庫,它必須取代現有的庫。這是一種「或者……或者……」的感受,你必須選擇目前最好的庫,或者和過去的人一塊兒使用以前的庫。
更好的方法是擁有一個像是你的工具箱同樣的東西,能夠把你的選擇項都放進去。就像是選擇使用螺絲刀仍是衝擊鑽。對於 80% 的工做,使用衝擊鑽擰螺絲都比螺絲刀更快。但對於另外的 20%,螺絲刀其實是更好的選擇 —— 或許由於空間比較狹小,或是物品很精細。當我有一個衝擊鑽時,我並無當即扔掉個人螺絲刀,甚至是個人非衝擊鑽。衝擊鑽沒有取代它們,它只是給了我另一種選擇。另一種解決問題的方法。
React 會「替代」 Angular 或 jQuery,但 Context 不會像這樣「替代」 Redux。哎呀,當我須要快速完成一些事情時,我仍然會使用 jQuery。我有時仍會使用服務器渲染的 EJS 模板,而不是使用整個 React 應用程序。有時 React 比你手上的任務需求更龐大。有時 Redux 裏也會有你不須要的功能。
如今,當 Redux 超出你的需求時,你可使用 Context。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。