无需外部库即可在 React 中轻松实现共享响应式状态
最终代码
目前使用useState`with`useContext需要编写大量样板代码。对于每个上下文,你都需要自定义提供程序,正如我们所见,这非常麻烦。不知何故,Facebook 拒绝修复这个问题,所以我们只能使用其他库:
这些方法我觉得比较有用,但归根结底,你仍然是在使用外部库来实现 React 本身就能做到的事情。难道就没有办法在 React 中不用编写那么多样板代码就能实现这个功能吗?
用户提供者
所以我在本系列的前一篇文章useProvider中创建了这个钩子函数,作为一项实验。它基本上使用一个映射表来存储对象,以便您可以随时检索它们。
那篇文章大概是我最不受欢迎的文章之一,后来我明白了原因。你不能把useState对象存储在映射里,至少不能在保持响应式的前提下这样做。
正如我一直以来的计划,我回头仔细思考了一下,想办法解决这个问题。
🤔💡💭
如果我们把上下文存储在映射表(map)中,而不是状态本身呢?好吧,说实话,我也不知道当时是怎么想的,但我不知怎么的(也许是无意中)就找到了实现方法。你仍然只有一个通用的提供程序,并且可以通过上下文获取任何状态(甚至是响应式状态)。回顾之前的帖子,了解这一演变过程:
use-provider.tsx
'use client';
import {
FC,
ReactNode,
createContext,
useContext,
type Context,
useState
} from "react";
const _Map = <T,>() => new Map<string, T>();
const Context = createContext(_Map());
export const Provider: FC<{ children: ReactNode }> = ({ children }) =>
<Context.Provider value={_Map()}>{children}</Context.Provider>;
const useContextProvider = <T,>(key: string) => {
const context = useContext(Context);
return {
set value(v: T) { context.set(key, v); },
get value() {
if (!context.has(key)) {
throw Error(`Context key '${key}' Not Found!`);
}
return context.get(key) as T;
}
}
};
这段代码与第一篇文章中的代码相同,只是重命名为useContextProvider。不过,现在我们将使用它作为真正useProvider钩子的辅助函数:
export const useProvider = <T,>(key: string, initialValue?: T) => {
const provider = useContextProvider<Context<T>>(key);
if (initialValue !== undefined) {
const Context = createContext<T>(initialValue);
provider.value = Context;
}
return useContext(provider.value);
};
事情是这样的。它useContextProvider创建了一个通用提供程序,可以将任何内容存储在映射中。再次强调,请参阅第一篇文章。useProvider它会为传入的任何值创建一个新的上下文,并将其设置为传入键的值。我知道这听起来有点令人困惑,所以请想象一下:
容器
<Provider>
---- my app components
</Provider>
简化集值(伪代码)
// create a new map and set that as value of universal provider
const providers = new Map()
providers.set('count', createContext(0))
<Context.Provider value={provider} />
简化的获取值(伪代码)
// get the 'count' key from universal provider
// which returns a context, use that context to get counter
const providers = useContext(Provider)
const countContext = providers.get('count')
const counter = useContext(countContext.value)
我不确定这样解释是否清楚,但这就是它最简单的运作方式。使用方法很简单,只需这样调用:
父母
// create a state context
const state = useState(0);
useProvider('count', state);
孩子
const [count, setCount] = useProvider('count')
就这样!!!
您只需一个通用服务提供商,即可拥有任意数量的服务提供商。您可以随意命名它。就这么简单context hell!
然而,我并没有就此止步。你几乎总是需要共享状态,所以为什么不让它也自动化呢!
export const useSharedState = <T,>(key: string, initialValue?: T) => {
let state = undefined;
if (initialValue !== undefined) {
const _useState = useState;
state = _useState(initialValue);
}
return useProvider(key, state);
};
这个辅助函数可以让你在任何地方像使用状态钩子一样使用提供程序!
父母
const [count, setCount] = useSharedState('count', 0);
子女/兄弟姐妹/孙子女
const [count, setCount] = useSharedState<number>('count');
请记住,count`context` 是上下文名称,` 0initial` 是初始值。就这么简单!在任何地方都能完美运行。您仍然需要在根目录中包含 ONE UNIVERSAL PROVIDER:
page.tsx
import Test from "./test";
import { Provider } from "./use-provider";
export default function Home() {
return (
<Provider>
<Test />
</Provider>
);
}
最终代码
use-provider.tsx
'use client';
import {
FC,
ReactNode,
createContext,
useContext,
type Context,
useState
} from "react";
const _Map = <T,>() => new Map<string, T>();
const Context = createContext(_Map());
export const Provider: FC<{ children: ReactNode }> = ({ children }) =>
<Context.Provider value={_Map()}>{children}</Context.Provider>;
const useContextProvider = <T,>(key: string) => {
const context = useContext(Context);
return {
set value(v: T) { context.set(key, v); },
get value() {
if (!context.has(key)) {
throw Error(`Context key '${key}' Not Found!`);
}
return context.get(key) as T;
}
}
};
export const useProvider = <T,>(key: string, initialValue?: T) => {
const provider = useContextProvider<Context<T>>(key);
if (initialValue !== undefined) {
const Context = createContext<T>(initialValue);
provider.value = Context;
}
return useContext(provider.value);
};
export const useSharedState = <T,>(key: string, initialValue?: T) => {
let state = undefined;
if (initialValue !== undefined) {
const _useState = useState;
state = _useState(initialValue);
}
return useProvider(key, state);
};
这段代码量并不大,却能提供如此强大的功能!它能帮你省去很多样板代码!
注:上面我用了一个技巧来实现条件判断,useState就是先把它设置成一个未调用的函数,如果你觉得这个技巧有趣的话 :)
- GitHub 仓库
- StackBlitz - (如果加载错误,请使用 CodeSandbox)
- CodeSandBox
我肯定漏掉了什么,但这看起来太棒了。如果我以后决定真的使用 React(我喜欢 Svelte 和 Qwik!),我肯定会使用这个自定义 hook useProvider:。
如果我漏掉了什么,请告诉我!
J
当前重新构建代码。
更新于 2024 年 3 月 10 日
这是可重用use-shared.ts文件的编译示例。
'use client';
import {
FC,
ReactNode,
createContext,
useContext,
type Context
} from "react";
const _Map = <T,>() => new Map<string, T>();
const Context = createContext(_Map());
export const Provider: FC<{ children: ReactNode }> = ({ children }) =>
<Context.Provider value={_Map()}>{children}</Context.Provider>;
const useContextProvider = <T,>(key: string) => {
const context = useContext(Context);
return {
set value(v: T) { context.set(key, v); },
get value() {
if (!context.has(key)) {
throw Error(`Context key '${key}' Not Found!`);
}
return context.get(key) as T;
}
}
};
export const useShared = <T, A>(
key: string,
fn: (value?: A) => T,
initialValue?: A
) => {
const provider = useContextProvider<Context<T>>(key);
if (initialValue !== undefined) {
const state = fn(initialValue);
const Context = createContext<T>(state);
provider.value = Context;
}
return useContext(provider.value);
};
