使用不到 40 行代码构建您自己的 React 状态管理库 - 支持 TypeScript
你有没有想过 React 的状态管理库是如何构建的?从像 Redux 这样有大量样板代码和庞大 bundle 大小的解决方案,到像 zustand 或 jotai 这样轻量级、简洁的库,应有尽有。今天,我们将构建自己的状态管理库,并探索其背后的神奇之处。
理解 useSyncExternalStore
React 18 引入了一个名为useSyncExternalStore 的新钩子,它允许 React 同步到任何外部存储。
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
以下是其参数的细分:
- subscribe接收一个回调作为参数,并将该回调订阅到外部 store,以便在 store 状态改变时调用,它需要返回一个 unsubscribe 函数。
- getSnapshot获取存储的当前快照,此快照必须是一个缓存值,因为 React 会在每次渲染时比较此值
Object.is(getSnapshot(), oldSnapshot)
,每次提供新值都会导致无限循环。 - getServerSnapshot(可选)允许我们在服务器上渲染时返回快照,这在外部存储或订阅源无法在服务器上运行或需要特定处理才能在服务器上运行的某些情况下非常有用。
利用 useSyncExternalStore,我们可以构建一个符合我们要求的简约商店。
为什么不直接使用 React Context?
React Context是 React 中的一个特性,它允许组件将 props 向下传递给它下面的整个组件树,这意味着它可以用作 store,是一个可行的选择。
React 上下文需要一些样板:
const context = createContext();
const CountProvider = ({ children }) => {
const [count, setCount] = useState(0);
return <context.Provider value={{ count, setCount }}>{children}</context.Provider>;
};
export function App() {
return (
<CountProvider>
<Outer />
<Other />
</CountProvider>
);
}
大量使用 Context 可能会导致“Context Hell”,其中许多上下文提供程序嵌套在 App 组件中:
export function App() {
return (
<CountProvider>
<AuthProvider>
<ThemeProvider>
<CacheProvider>
<IntlProvider>
<TooltipProvider>
<UserSettingsProvider>
<NotificationProvider>
<AnalyticsProvider>
<Content />
</UserSettingsProvider>
</NotificationProvider>
</AnalyticsProvider>
</TooltipProvider>
</IntlProvider>
</CacheProvider>
</ThemeProvider>
</AuthProvider>
</CountProvider>
);
}
此外,使用上下文可能会无意中触发整个组件树的重新渲染,如下所示:
export function App() {
const [count, setCount] = useState(0);
return (
<context.Provider value={{ count, setCount }}>
<Outer />
<Other />
</context.Provider>
);
}
从上下文的消费者使用 setCount 将导致整个应用程序的重新渲染(Outer 和 Other 都将被重新渲染),因为状态是在 App 组件上设置的,并且当它重新渲染时,它的所有子组件也会重新渲染。
此外,使用外部存储可以让我们更轻松地同步与外部系统(如 http 请求)的反应,在上下文中您将使用 useEffect,而使用外部存储您可以直接更新存储,更改将在订阅组件中生效。
建立我们的商店
让我们深入研究商店的实现。我们将从基本结构开始,并根据需求逐步增强它。
import { useSyncExternalStore } from 'react';
export type Listener = () => void;
function createStore<T>({ initialState }: { initialState: T }) {
let subscribers: Listener[] = [];
let state = initialState;
const notifyStateChanged = () => {
subscribers.forEach((fn) => fn());
};
return {
subscribe(fn: Listener) {
subscribers.push(fn);
return () => {
subscribers = subscribers.filter((listener) => listener !== fn);
};
},
getSnapshot() {
return state;
},
setState(newState: T) {
state = newState;
notifyStateChanged();
},
};
}
Subscribers是一个监听器数组,我们的 store 会在状态每次改变时通知它们。State是 store 的
状态,我们会在调用 setState 时更新它,并将更新通知给 store 的所有订阅者。
为了在 React 中使用存储,我们将创建 createUseStore,它是一个以方便的方式包装 createStore 和 useSyncExternalStore 的助手:
export function createUseStore<T>(initialState: T) {
const store = createStore({ initialState });
return () => [useSyncExternalStore(store.subscribe, store.getSnapshot), store.setState] as const;
}
使用商店
有了我们的商店,让我们开始构建一个 Counter 组件:
import React, { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
并在我们的应用程序中渲染三次:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Counter } from './Counter.tsx';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Counter />
<Counter />
<Counter />
</React.StrictMode>,
);
我们现在在页面上看到三个计数器,单击“增加”只会增加其中一个计数器: 让我们使用我们的存储使这 3 个计数器使用相同的状态,首先我们将使用之前创建的 createUseStore 助手创建 useCountStore:
export const useCountStore = createUseStore(0);
现在让我们在计数器中使用 useCountStore 钩子:
import { useCountStore } from "./countStore";
function Counter() {
const [count, setCount] = useCountStore();
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
现在我们的 3 个计数器已同步,并且它们全部一起递增: 由于使用了泛型,typescript 知道 count 是一个数字,而 setCount 是一个接受数字的回调:
后续步骤Next steps
关于如何改进和构建我们的简易商店的一些想法:
减少状态
在我们的商店中设置状态非常直接,这很方便,但有时我们可能需要在确定状态时处理复杂的逻辑,这就是减速器可以帮助我们的地方,我们可以在商店中添加一个新的调度功能:
dispatch(action) {
state = reducer(action);
notifyStateChanged();
},
处理深度嵌套状态
设置新状态需要解构现有状态,如果我们有一个深度嵌套的状态,这可能会很烦人,为了解决这个问题,我们可以使用 immer 或类似的库:
// without immer
setState({
...state,
nested: {
...state.nested,
sub: {
...state.nested.sub,
new: true,
}
}
});
// with immer
import { produce } from "immer";
const nextState = produce(state, s => {
s.nested.sub.new = true;
});
setState(nextState);
我们甚至可以在内部将 immer 添加到我们的商店中,并在 setState 中接受回调,如下所示:
setState((state) => {
state.nested.sub.new = true;
return state;
});
结论
在本教程中,我们完成了构建一个支持 TypeScript 的简单 React 状态管理库的步骤。
利用 React 的useSyncExternalStore
hooks,我们构建了一个简单但功能强大的 store,可以与 React 组件无缝集成。
现在你已经掌握了它,可以开始构建你自己的定制状态管理库了。
在 React 文档中阅读有关useSyncExternalStore 的更多信息。
要查看本文讨论的概念的实际实现,请在此处查看tinystate-react 。该库是使用本教程中描述的方法构建的,可让您更深入地了解代码和示例。
鏂囩珷鏉ユ簮锛�https://dev.to/paripsky/build-your-own-react-state-management-library-in-under-40-lines-of-code-with-typescript-support-hji