iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 26
1
Modern Web

I Want To Know React系列 第 26

I Want To Know React - 初探 Context

在本章中,我們將介紹 React Context。React Context 與 props 相同,是一種資料傳遞的方式,然而 React Context 可以解決某些 props 在特殊情境中遇到的難點。

接著,就蘭更近一步介紹 Context 的詳細功能、解決的問題以及使用情境吧!

React - Context

何謂 Context

React Context 是一種利用向下廣播來傳遞資料的方式。

與 props 不同,context 不必將資料傳遞給中間層的 component 就能夠把資料分享給下層 component。

到這邊為止,讀者可能會想說為何需要 context,為何不用 Props 就好呢?以下先來看看 props 會有哪些缺點吧。

Props 的缺點

Props 有一個特點,就是 props 不可跳層傳遞。

如果要將資料從上層 component 傳遞到較下層的 component 的話,則需要經過每一個中間層 component 的 props 才可送達目標的下層 component。

這種特性是 props 的優點。其資料流路徑明確,只要照著每一層 component 的 props 往上追朔即可找到資料源頭。然而當我們要把一個全域性的資料(e.g. UI 主題、時區、語系 ...etc)傳給許多下層 component 時,使用 props 就會變成一個噩夢。每一個中間層 component 都必須定義此 props 才能把資料送達目的地。這會導致幾個問題:

  • 污染中間層 component。不需要資料的中間層 component 也被迫定義對應的 props

  • 當傳遞層數與傳遞的目標 component 過多時,會造成修改 props 與追朔資料流不易

    此時即使只是微小的修改,也需要修改眾多的中間層 component 才能達到目的。

以下是一個不使用 Context 傳遞 theme 的範例:

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} />;
  }
}

範例中,Toolbar 就算不需要 theme 的資料,也被迫定義 theme prop,以將 theme 資料傳遞給更下層的 ThemeButton

可以想像一下如果網頁中的每個 button 都必須接收 theme 的話,整個專案會變得多混亂,因為資料需要從最上層 component(App)經過每個中間層傳遞到最下層 component(每個 Button)。這幾乎相當於整個專案的 component 都會有 theme 這個 prop 了。

為何需要 Context

在明確知道 props 的缺點後,相信讀者已經很清楚 context 存在的意義了。

Context 就是為了解決 props 必須要逐層傳遞的缺點而存在。

使用 context 就可以讓資料以 "廣播" 的方式傳遞給下層 component。中間層的 component 則完全不需要為了資料做任何的改動。如此一來,就可以避免由最上層 component 傳遞資料到最下層的多個 component 時,需要污染專案所有中間層 component 的問題了。

現在就來使用 context 來修改上一個 props 傳遞的範例:

// 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() {
  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} />;
  }
}

可以看到,中間層 component Toolbar 不再需要為了傳遞 theme 而額外定義一個 prop。更下層的 ThemedButton 仍可以避過中間層收到 theme 的資料。如此就能避免將 theme 逐層傳遞的問題了。

何時使用 Context

然而並不是所有情境都適合使用 context。React 官方推薦只有在以下情境時,才使用 context。

  • 要把全域性資料傳遞給很多 components 時

所謂的全域性資料代表整個 App 都需要的資料,例如使用者資訊、時區設定、語系、UI 主題

...etc。上個段落的 theme 程式就是一很好的範例。

何時不應該使用 Context

需要注意的是,不應該只為了避免傳遞多層 props 而使用 context。

如上一個段落所說,開發者只應該把 context 用在要把全域性資料傳遞很多 component 的狀況下。

濫用 context 則可能造成 component 重用性降低,另外由於 context 可跳層傳遞資料的特性,也可能造成追蹤程式碼不易。

如果只是想避免中間層 component 的 props 被污染的話,可以使用 composition 技巧來解決。我們將在下一個段落詳細說明。

使用 composition 避免傳遞細節 props

在很多時候,當我們想要傳遞一些只有特定(非全域性資料)下層 component 需要的資料時,就會遇到中間層 component 被一併污染的問題。在這種狀況下,隨著下層 component 支援的 props 越來越多,中間層 component 就會被迫傳遞更多自己不需要的 prop 資料。

然而如上個段落提過,開發者不應該只為了避免傳遞多層 props 而使用 context。而一個不使用 context 的可行解法是使用 composition,其技巧為:

  • 把整個下層 component 作為 props 傳遞下去

使用了這種依賴反轉的概念,我們就可以把控制內容的權力轉移到上層 component 上了。中間層的 component 不再需要為下層 component 的每個 prop 都定義一個額外的 prop,只要需要定義一個 prop 負責把整個 React element 傳下去即可。

舉例來說,如果在某個頁面中要顯示使用者的大頭貼 Avatar,然而這個大頭貼的資料儲存在最上層 component 中,需要使用 props 由上傳下去:

<Page user={user} avatarSize={avatarSize} avatarShape={shape} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} avatarShape={shape} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} avatarShape={shape} />
// ... which renders ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} shape={shape} />
</Link>

這時候就可以發現,所有中間層 component 必須要定義 Avatar 的顯示資訊 useravatarSizeavatarShape。這個情況絕對不是我們樂見的,想像今天 Avatar 需要更多的 props 資料,那中間層 component 的 props 甚至會被一些下層 component 的 props 所淹沒。

此時來嘗試使用 composition 解法吧。把 LinkAvatar 整個元素包成一個 userLink React elment 當作 props 往下傳。這樣中間層的 component 就不用知道各種 Avatar 的細節 props 了:

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} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}

然而這個方式會有個缺點:

  • 因為要組合下層 component,所以會讓上層 component 的內容較為複雜

因此讀者還是應該視情境選擇要不要使用此技巧。

小結

在這一章節中,我們學到了 React context 是一種利用向下廣播來傳遞資料的方式。

此方法可以解決 props 必須要一層層向下傳遞的缺點。

而 React 官方推薦使用 context 是在要將全域性資料(e.g. 使用者資訊、時區設定、語系、UI 主題 ...etc)向下傳遞給很多底層 component 時才應該使用。開發者不應該只會了避免傳遞過多層 props 就使用 context。

如果是要避免中間層 component 傳遞過多細節 props 的話,開發者可以使用 composition 的技巧,把整個下層 component 作為 props 傳遞下去即可。

下一章節中,我們將學習如何使用 Context 語法。

參考資料


上一篇
I Want To Know React - Composition vs Inheritance
下一篇
I Want To Know React - Context 語法
系列文
I Want To Know React30

尚未有邦友留言

立即登入留言