iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
Modern Web

從Vue學React!不只要會用,還要真的懂~系列 第 22

【Day 22】 深層傳遞state!除了props還有其他方式 - proivde & inject和useContext

  • 分享至 

  • xImage
  •  

還記得前幾天有提到因為Vue和React都是以單向資料流為核心,所以資料的傳遞方向都必須是爺爺傳給爸爸,爸爸再傳給兒子嗎?雖然用props層層傳遞state,沒有什麼大問題,不過當碰到資料都在最上層的狀況,但是需要資料的元件又在很底層的話,使用props來傳遞state,可能就不是那麼方便的方式,那麼還有什麼方法可以解決這個狀況呢?今天就從這個情境開始今天的主題吧!

當我想要把state從祖父層傳給孫子層

https://ithelp.ithome.com.tw/upload/images/20230926/20130914VSjTHUfzT0.png
主要會遇到的狀況,就如同上圖,只能一層層傳遞,無法一次跨好幾層來傳遞。

這裡在用一個具體的例子來看這個情境!

範例情境:實作可以轉換成夜間模式和日間模式的功能

使用props的話

vue的使用情境會如同預想的一樣,要一層層的往下。
先傳給兒子層MainCotent

<template>
  <div class="app" :class="{'dark-theme': isDarkTheme}">
    <div class="container">
      <Button :isDarkTheme="isDarkTheme" @click="handleToggleTheme">Toggle Theme</Button>
      <!-- 傳給兒子層 -->
      <MainContent :isDarkTheme="isDarkTheme" />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import MainContent from './components/MainContent.vue';
import Button from './components/Button.vue';

const isDarkTheme = ref(false);
const handleToggleTheme = () => {
  isDarkTheme.value = !isDarkTheme.value;
};
</script>

再從兒子層往下傳給兒子的兒子層Button

<template>
  <div :class="{'dark-theme': isDarkTheme}">
    <h1>主畫面</h1>
    <p>count: {{ count }}</p>
    <!-- 再從兒子層往下傳給兒子的兒子層 -->
    <Button :isDarkTheme="isDarkTheme" @click="handleAdd">Add Count</Button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Button from './Button.vue';
defineProps({
  isDarkTheme: {
    type: Boolean,
    default: false,
  }
});

const count = ref(0);
const handleAdd = () => {
  count.value ++
};

</script>
<template>
  <button :class="{'dark-theme': isDarkTheme}">
    <slot />
  </button>
</template>

<script setup>
defineProps({
  isDarkTheme: {
    type: Boolean,
    default: false,
  }
});
</script>

react也一樣需要一層層的往下傳遞。

import { useState } from 'react';
import Button from './components/Button';
import MainContent from './components/MainContent';

function App() {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const handleToggleTheme = () => {
    setIsDarkTheme(previousTheme => !previousTheme);
  };
  return (
    <div className={`App ${isDarkTheme ? 'dark-theme' : ''}`}>
      <div className="container">
        <Button isDarkTheme={isDarkTheme} handleClick={handleToggleTheme}>Toggle Theme</Button>
        {/* 傳給兒子層 */}
        <MainContent isDarkTheme={isDarkTheme} />
        {isDarkTheme}
      </div>
    </div>
  );
}
export default App;

兒子層再傳給兒子的兒子。

import { useState } from 'react';
import Button from './Button';

export default function MainContent({isDarkTheme}) {
  const [count, setCount] = useState(0);
  const handleAdd = () => {
    setCount((previousCount) => previousCount + 1)
  };
  return (
    <div className={isDarkTheme ? 'dark-theme' : ''}>
      <h1>主畫面</h1>
      <p>count: { count }</p>
      {/* 再從兒子層往下傳給兒子的兒子層 */}
      <Button isDarkTheme={isDarkTheme} handleClick={handleAdd}>Add Count</Button>
    </div>
  )
}
export default function Button({children, handleClick, isDarkTheme}) {
  return (
    <button className={isDarkTheme ? 'dark-theme' : ''} onClick={handleClick}>{children}</button>
  )
}

這樣的方式的確可以達到我們想達到的目的,但若實際情況不只有我們範例中的三層,而是有四層五層六層以上的話,就有可能讓程式碼變得比較難以維護。

這時候就可以考慮用另外一個方法來處理!

Vue的provide & inject

如果是寫Vue的話,可以使用provide和inject,在使用上主要會需要經歷這幾個步驟。

1. 使用provide把要往下傳遞的state帶入
使用provide時,需要帶入兩個參數,第一個參數是要inject這個state時的key,第二個參數則是要inject到子子孫孫層的state

<template>
  <div class="app" :class="{'dark-theme': isDarkTheme}">
    <div class="container">
      <Button @click="handleToggleTheme">Toggle Theme</Button>
      <MainContent />
    </div>
  </div>
</template>

<script setup>
import { ref, provide } from 'vue';
import MainContent from './components/MainContent.vue';
import Button from './components/Button.vue';

const isDarkTheme = ref(false);
const handleToggleTheme = () => {
  isDarkTheme.value = !isDarkTheme.value;
};

// 使用provide,把要往下傳的state傳入,currentTheme是inject的時候要使用的key,isDarkTheme則是要inject下去的state
provide('currentTheme', isDarkTheme);
</script>

2. 透過inject把傳入provide裡的state帶到要使用的元件中
如果要取得傳入provide的state,需要透過在使用provide時,帶進去的key。

<template>
  <div :class="{'dark-theme': isDarkTheme}">
    <h1>主畫面</h1>
    <p>count: {{ count }}</p>
    <Button @click="handleAdd">Add Count</Button>
  </div>
</template>

<script setup>
import { ref, inject } from 'vue';
import Button from './Button.vue';
// 用設定的key來inject要往下傳的state
const isDarkTheme = inject('currentTheme');

const count = ref(0);
const handleAdd = () => {
  count.value ++
};

</script>

只要是在父層底下的元件,即使是孫子層,例如MainContent元件裡面的Button元件,也可以透過inject取得傳入provide的state。

<template>
  <button :class="{'dark-theme': isDarkTheme}">
    <slot />
  </button>
</template>

<script setup>
import { inject } from 'vue';
// 一樣透過inject取得isDarkTheme
const isDarkTheme = inject('currentTheme');
</script>

需要注意的是provide的那一層一定要是父層,無法寫在子層給父層拿。
這樣調整之後,就不需要把isDarkTheme層層傳遞下去了。

React的useContext

寫React的話,則可以使用useContext,使用useContext的話,可以透過以下這幾個步驟來輕鬆地把state往深層傳遞。
1. 透過createContext創建一個context

import { createContext } from 'react';
export const ThemeContext = createContext(false);

2. 把context的state從最外層帶上。
把前面創建好的context import進父層,並且拿context的provider包在最外層,並用value把要傳下去的值帶上。

import { useState } from 'react';
import Button from './components/Button';
import MainContent from './components/MainContent';
import { ThemeContext } from './ThemeContext';

function App() {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const handleToggleTheme = () => {
    setIsDarkTheme(previousTheme => !previousTheme);
  };
  return (
    <ThemeContext.Provider value={isDarkTheme}>
      <div className={`App ${isDarkTheme ? 'dark-theme' : ''}`}>
        <div className="container">
          <Button handleClick={handleToggleTheme}>Toggle Theme</Button>
          <MainContent />
          {isDarkTheme}
        </div>
      </div>
    </ThemeContext.Provider>
  );
}
export default App;

3. 在子層用useContext取得state。
在要使用父層state子元件中,把創建好的context帶入useContext來取得provider傳下來的state。

import { useState, useContext } from 'react';
import Button from './Button';
import { ThemeContext } from '../ThemeContext';

export default function MainContent() {
  const [count, setCount] = useState(0);
  // 用useContext取得isDarkTheme
  const isDarkTheme = useContext(ThemeContext);
  const handleAdd = () => {
    setCount((previousCount) => previousCount + 1)
  };
  
  return (
    <div className={isDarkTheme ? 'dark-theme' : ''}>
      <h1>主畫面</h1>
      <p>count: { count }</p>
      <Button handleClick={handleAdd}>Add Count</Button>
    </div>
  )
}

這樣改寫後,會被放在最底層使用的button一樣也可以透過useContext取得我們要的isDarkTheme,不需要透過props取得isDarkTheme。

import React, { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';

export default function Button({children, handleClick}) {
  // 使用useContext取得isDarkTheme
  const isDarkTheme = useContext(ThemeContext);
  return (
    <button className={isDarkTheme ? 'dark-theme' : ''} onClick={handleClick}>{children}</button>
  )
}

https://i.imgur.com/VeCA5i5.gif

巢狀結構使用context的進階的運用概念 - 自動找上一層最近的資料

useContext除了可以把值深度往下傳外,它還有一個特性就是當使用情境為巢狀結構時.會自動使用最近的外層資料來使用。這裡一樣透過一個使用範例來看看是什麼樣的情境。

首先一樣也需要準備一個context。

import { createContext } from "react";
// 預設值為0
export const CountContext = createContext(0);

再來準備一個用provider包起來的子元件SectionWithCount。

import { useContext } from "react";
import { CountContext } from "./CountContext";

export default function SectionWithCount({ children }) {
  const count = useContext(CountContext);
  return (
    <section>
      <CountContext.Provider value={count + 1}>
        {children}
      </CountContext.Provider>
    </section>
  )
}

這樣SectionWithCount裡面的children就可以使用到useContext給的state。

// 這是預計要包在SectionWithCount裡面使用的子元件ContentText
import { useContext } from "react";
import { CountContext } from "./CountContext";

export default function ContentText() {
  const count = useContext(CountContext);
  return (
    <div>this layer count: {count} </div>
  )
}

實際要使用的時候,就可以這樣一層包著一層使用,但不用每一層都寫上一個value,因為當這樣一層層包著使用的時候,都會自動往上找最近一層的value來使用。

import SectionWithCount from './SectionWithCount';
import ContentText from './ContentText';

const App = () => {
  return (
    <div className="container">
      <SectionWithCount>
        <ContentText/>
        <SectionWithCount>
          <ContentText/>
        </SectionWithCount>
      </SectionWithCount>
    </div>
  );
};

export default App;

https://ithelp.ithome.com.tw/upload/images/20230926/20130914Kc7IGGu3Rh.png

無需層層傳遞state的用法是否必要?

雖然使用props傳遞state在實作上比較費工,會因為會需要一直透過props層層傳遞,而讓state傳到到一些實際上不會使用到這個state的元件中,但也因為它是層層傳遞,所以其實也比較好追蹤props的來源。相反的,使用provide/inject或是useContext雖然省去層層傳遞這樣繁瑣的工,但其實因為需要特別去查找傳入state的地方,所以反而會比較沒那麼好維護。至於該使用props,還是使用provide/inject或useContext的這種作法,還是必須依照實際的情境下去做決定。

參考資料

Passing Data Deeply with Context


上一篇
【Day 21】管理邏輯複雜的狀態 - useReducer
下一篇
【Day 23】利用useReducer + useContext管理複雜的狀態邏輯
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言