因为想要用 React.useContext 来组织 Vue 的状态,所以我们第一步要先在 Vue 中实现一个 React.useContext
vc-state
Easily to compose scoped state in Vue.js
我们先用这个 React.useContext 的思路来实现这个方案。
- useContext 只会读取到最近的一个 Provider 的提供的值
- useContext 只能在 Provider 后代中使用
- Provider 可以由 props 控制默认值
- Provider 可以嵌套使用
Provider 可以由 props 控制默认值
因为想在 createContext 的时候可以通过 hooks 来组织状态,所以实现时参考了 constate 的实现方式,不过与 constate 不一样的是,创建出来的 context 是由初始化 context 和 hooks 组合起来的。
// context.ts
import { createContext } from 'vc-state';
const [ContextProvider, useContext] = createContext((props: { c: string }) => {
return {
a: '1',
c: props.c
}
}, (initialContext) => {
return {
b: ~~initialContext.a
}
});
// 所以这里创造出来的 context 是 { a: '1', b: 1 }
export { ContextProvider, useContext };
我们需要在上层使用 ContextProvider
// App.tsx
import { ContextProvider } from 'context.ts'
export default defineComponent({
name: 'App',
setup() {
return () => (
<ContextProvider c='test'>
{/* 后代组件 */}
</ContextProvider>
);
},
});
在后代组件里使用 useContext 获取数据
// Child.tsx
import { useContext } from 'context.ts'
export default defineComponent({
name: 'Child',
setup() {
const { a, b, c } = useContext();
return () => (
<div>
a: {a}
b: {b}
c: {c}
</div>
);
},
});
这样我们就可以通过 useContext 来进行 context 的穿透。
因为我们是使用 hooks 来组织状态的,所以我们可以在定义 context 的时候可以使用响应式的 API
嵌套使用
因为我们使用的是生成组件来外层包裹的方式来实现,所以我们支持嵌套使用。
const ContextProvider: FunctionalComponent<Props> = (props, { slots }) => {
return h(
defineComponent({
name: 'Provider',
setup() {
const context = useValue(props);
const hookContextValues = selectors.reduce((merged, selector) => {
return Object.assign({}, merged, selector.call(null, context));
}, Object.create(null));
provide(injectionKey, Object.assign({}, context, hookContextValues));
return () => h(Fragment, slots.default?.());
},
})
);
};
读取最近的值
因为是使用 Vue 提供的 Provide/Inject 来实现上下层级的值传递,所以是遵循 Vue 的规则,只会读取到最近的一个 provide 传递下来的值。
useContext 只能在 Provider 后代中使用
因为 provide 是在 Provider 创建时使用,所以 useContext 必须是要在 Provider 后代才能使用。
例子
我们要用例子来证明一下我们的方案是可行的。
ThemeContextProvider
我们使用一个简单的例子来展示我们的基础功能
CodeSandbox - ThemeContextProvider
import { computed, defineComponent, ref } from 'vue';
import { createContext } from 'vc-state';
type Theme = 'dark' | 'light';
interface ThemeContextProviderProps {
defaultTheme: Theme;
lightColor?: string;
darkColor?: string;
}
// Defined Required Props in useValue function
const [ThemeContextProvider, useThemeContext] = createContext((props: ThemeContextProviderProps) => {
const theme = ref<Theme>(props.defaultTheme);
const toggleTheme = () => (theme.value = theme.value === 'dark' ? 'light' : 'dark');
return { theme, toggleTheme };
});
const Button = defineComponent({
name: 'Button',
setup() {
const { toggleTheme, theme } = useThemeContext();
return () => {
return <button onClick={toggleTheme}>to {theme.value === 'dark' ? 'light' : 'dark'}</button>;
};
},
});
const Panel = defineComponent({
name: 'Panel',
setup() {
const { theme } = useThemeContext();
const currentThemeColor = computed(() => (theme.value === 'dark' ? '#000' : '#fff'));
const oppositeThemeColor = computed(() => (theme.value === 'dark' ? '#fff' : '#000'));
return () => {
return (
<div
style={{
backgroundColor: currentThemeColor.value,
border: `1px ${oppositeThemeColor.value} solid`,
width: '300px',
height: '300px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: oppositeThemeColor.value,
}}
>
<p>I'm in {theme.value} mode</p>
</div>
);
};
},
});
export default defineComponent({
name: 'App',
setup() {
return () => (
// defaultTheme is required
// lightColor and darkColor are optional
<ThemeContextProvider defaultTheme='light'>
<Panel />
<Button />
</ThemeContextProvider>
);
},
});
OverridingProviders
这个例子是展示只能读取最近一个 Provider 的值
CodeSandbox - OverridingProviders
import { computed, defineComponent, ref } from 'vue';
import { createContext } from 'vc-state';
type Theme = 'dark' | 'light';
interface ThemeContextProviderProps {
defaultTheme: Theme;
lightColor?: string;
darkColor?: string;
}
const [ThemeContextProvider, useThemeContext] = createContext((props: ThemeContextProviderProps) => {
const theme = ref<Theme>(props.defaultTheme);
const toggleTheme = () => (theme.value = theme.value === 'dark' ? 'light' : 'dark');
return { theme, toggleTheme };
});
const Button = defineComponent({
name: 'Button',
setup() {
const { toggleTheme, theme } = useThemeContext();
return () => {
return <button onClick={toggleTheme}>to {theme.value === 'dark' ? 'light' : 'dark'}</button>;
};
},
});
const Panel = defineComponent({
name: 'Panel',
setup() {
const { theme } = useThemeContext();
const currentThemeColor = computed(() => (theme.value === 'dark' ? '#000' : '#fff'));
const oppositeThemeColor = computed(() => (theme.value === 'dark' ? '#fff' : '#000'));
return () => {
return (
<div
style={{
backgroundColor: currentThemeColor.value,
border: `1px ${oppositeThemeColor.value} solid`,
width: '300px',
height: '300px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: oppositeThemeColor.value,
}}
>
<p>I'm in {theme.value} mode</p>
</div>
);
};
},
});
export default defineComponent({
name: 'App',
setup() {
return () => (
// useContext receives the provided value of the nearest Provider
<ThemeContextProvider defaultTheme='dark'>
<ThemeContextProvider defaultTheme='light'>
<Panel />
<Button />
</ThemeContextProvider>
<ThemeContextProvider defaultTheme='dark'>
<Panel />
<Button />
</ThemeContextProvider>
<Panel />
<Button />
</ThemeContextProvider>
);
},
});
NestedProviders
这个例子是展现嵌套使用的方法
// theme.context.ts
import { createContext } from 'vc-state';
import { ref } from 'vue';
export type Theme = 'dark' | 'light';
const [ThemeContextProvider, useThemeContext] = createContext(() => {
const theme = ref<Theme>('dark');
const toggleTheme = () => (theme.value = theme.value === 'dark' ? 'light' : 'dark');
return { theme, toggleTheme };
});
export { ThemeContextProvider, useThemeContext };
// Panel.tsx
import { defineComponent } from 'vue';
import { useThemeContext } from '../theme.context';
import { PanelThemeContextProvider, usePanelThemeContext } from './panel.context';
const Panel = defineComponent({
name: 'Panel',
setup() {
const { theme } = useThemeContext();
const { styles } = usePanelThemeContext();
return () => {
return <div style={styles.value}>I'm in {theme.value} mode</div>;
};
},
});
const PanelWrapper = defineComponent({
name: 'PanelWrapper',
setup() {
return () => (
<PanelThemeContextProvider>
<Panel />
</PanelThemeContextProvider>
);
},
});
export { PanelWrapper as Panel };
// panel.context.ts
import { createContext } from 'vc-state';
import { computed } from 'vue';
import { useThemeContext } from '../theme.context';
const [PanelThemeContextProvider, usePanelThemeContext] = createContext(() => {
const { theme } = useThemeContext();
const currentThemeColor = computed(() => (theme.value === 'dark' ? '#000' : '#fff'));
const oppositeThemeColor = computed(() => (theme.value === 'dark' ? '#fff' : '#000'));
const styles = computed(() => {
return {
backgroundColor: currentThemeColor.value,
border: `1px ${oppositeThemeColor.value} solid`,
width: '300px',
height: '300px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: oppositeThemeColor.value,
};
});
return {
styles,
};
});
export { PanelThemeContextProvider, usePanelThemeContext };
// App.tsx
import { defineComponent } from 'vue';
import { ThemeContextProvider, useThemeContext } from './theme.context';
import { Panel } from './Panel';
const Button = defineComponent({
name: 'Button',
setup() {
const { toggleTheme, theme } = useThemeContext();
return () => {
return <button onClick={toggleTheme}>to {theme.value === 'dark' ? 'light' : 'dark'}</button>;
};
},
});
export default defineComponent({
name: 'App',
setup() {
return () => (
<ThemeContextProvider>
<Panel />
<Button />
</ThemeContextProvider>
);
},
});
总结
通过上面的三个例子我们可以看出已经完成了 React.useContext 的几个特性,另外的优点是:
- 可以在定义时使用 hooks 来对状态进行监听
- 可以很简单的创建一个作用域状态
- 完全的类型支持
- 轻量级选手
有兴趣的同学,可以一起探讨和研究一下,也欢迎提供一些思路或者建议。
延伸阅读
- vc-state -- Easily to compose scoped state in Vue.js