還記得前幾天有提到因為Vue和React都是以單向資料流為核心,所以資料的傳遞方向都必須是爺爺傳給爸爸,爸爸再傳給兒子嗎?雖然用props層層傳遞state,沒有什麼大問題,不過當碰到資料都在最上層的狀況,但是需要資料的元件又在很底層的話,使用props來傳遞state,可能就不是那麼方便的方式,那麼還有什麼方法可以解決這個狀況呢?今天就從這個情境開始今天的主題吧!
主要會遇到的狀況,就如同上圖,只能一層層傳遞,無法一次跨好幾層來傳遞。
這裡在用一個具體的例子來看這個情境!
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
,在使用上主要會需要經歷這幾個步驟。
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,使用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>
)
}
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;
雖然使用props傳遞state在實作上比較費工,會因為會需要一直透過props層層傳遞,而讓state傳到到一些實際上不會使用到這個state的元件中,但也因為它是層層傳遞,所以其實也比較好追蹤props的來源。相反的,使用provide/inject或是useContext雖然省去層層傳遞這樣繁瑣的工,但其實因為需要特別去查找傳入state的地方,所以反而會比較沒那麼好維護。至於該使用props,還是使用provide/inject或useContext的這種作法,還是必須依照實際的情境下去做決定。
Passing Data Deeply with Context