做全局状态的正确方法是什么?



我有一个应用程序,我希望应用程序组件保持当前登录的用户。我有以下路由和组件:

基本上,我的应用程序中的每个组件都会使用user对象。当然,我可以将用户作为道具传递给每个组件,但这并不优雅。全局共享用户道具的正确React方式是什么?
const App = () => {
const [user, setUser] = useState(null);
return (
<Router>
<div className="app">
<Topbar />
<Switch>
<Route path="/login" exact component={Login} />
<Route path="/home" exact component={Home} />
<Route path="/" exact component={ShopMenu} />
<Route path="/orders">
<Orders />
</Route>
<Route path="/wishlist" exact component={Wishlist} />
<Route path="/wallet" exact component={Wallet} />
<Route path="/cart" exact component={Cart} />
</Switch>
<BottomBar />
</div>
</Router>
);
};

看看React Context,更具体地说,当你使用hooks时使用econtext。

上下文的概念就是这样的——您可以将可更新的状态共享给后代,而不必从一个组件传递到另一个组件(即所谓的"prop-drilling")。

export const UserContext = React.createContext(null);
const App = () => {
const [user, setUser] = useState(null);
return (
<Router>
<div className="app">
<UserContext.Provider value={{ user: user, setUser: setUser }}>
<Topbar />
<Switch>
{/* <routes>... */}
</Switch>
<BottomBar />
</UserContext.Provider>
</div>
</Router>
);
};

然后,在你的组件中:

import { UserContext } from './app';
const Topbar = () => {
const { user, setUser } = useContext(UserContext);
// use `user` here
};

如果你想访问setter (setUser),你也可以通过上下文传递它。

Simple React Global State with Hooks (Observer Design Pattern)

然而"反应方式"它可能会对所有寻求简单而健壮的替代方案的人有所帮助。

codesandbox例子

这个概念是基于Yezy Ilomo的文章。它只是被置于工作状态,并被重命名为变量&

我在生产中使用它,在多个项目中,直接使用这个特定的实现(而不是Yezy的npm包),它像魅力一样工作。

作为一个最近花了很多时间的新手离开这里…全局状态是什么?或者反应中的模态?我们有redux和mobx,两者都有缺点,我们在useState中有本地状态……预计原生道具演练。然后我们有了useContext,它很好,无需与每个组件握手就可以进行prop钻取。

经过大量的实验,研究了支持和反对之后,我们期望使用econtext作为本地全局/模块化状态,而不是强制使用上下文来呈现每个组件,无论对象属性被更新。

这里正确的答案是useSyncExternalStore。为什么第三方状态引擎现在不使用这个https://react.dev/reference/react/useSyncExternalStore

redux尝试使用econtext并放弃,mobx似乎有它的包装的可观察方法。使用你自己的和useSyncExternalStore

在那个页面上有一个可用的例子,但你也可以滚动你自己的存储引擎,添加删除事件监听绑定在一个代理包裹的状态…然后,您可以订阅添加/删除事件

function subscribe(callback) {
System.addEventListener('setState.theme', null, callback);
return () => System.removeEventListener('setState.theme', null, callback);
}

你可以在你的组件函数

中使用
const theme: string = useSyncExternalStore(subscribe, () => System.state.theme);

现在当我做这个

System.state.theme = theme

代理正在检查添加的事件,然后命中分配给对象属性的回调。

useSyncExternalStore处理重新呈现,它也确保你只获得一个事件添加,通过使用订阅方法来删除和重新添加订阅返回删除事件。

这种模式使我能够使用任何状态引擎,与浏览器API同步,并具有适当的结构化存储(而不是冗长的useContext混乱)。

import Store from '@/base/Store';

/**
* @public @name System
* @description Used for capturing system state, things that track the state of the system as its used
*/
class System extends Store {
/**
* @public @constructor @name constructor
* @description Process called function triggered when component is instantiated (but not ready or in DOM, must call super() first)
*/
constructor() {
super();
}
// map state values with defaults
get defineState() {
return {
test: false,
theme: 'light',
};
}
// cast state values to ensure type
get state(): System['defineState'] {
return super.state as System['defineState'];
}
}
// export singleton instance
export default new System();

的基类看起来像这样

/* eslint-disable  @typescript-eslint/no-explicit-any */
export type Event = {
scope: any;
callback: () => void;
};
/**
* @public @name Store
* @description Used for storing global state data in react
*/
export default class Store {
private __state: object;
private __events: object;
private __customEvents: string[];
/**
* @public @constructor @name constructor
* @description Process called function triggered when component is instantiated (but not ready or in DOM, must call super() first)
*/
constructor() {
this.__state = new Proxy(this.defineState, this.defineHandler);
this.__events = {};
this.__customEvents = [];
}
/**
* @public @get @name defineState
* @description Basic define state in the store as an empty object that can be proxied
* @return the defined state
*/
get defineState(): object {
return {};
}
/**
* @public @get @name defineHandler
* @description Basic define handler to use with proxy, which bootstraps events in to detect changes
* @return the defined handler
*/
get defineHandler(): object {
return {
get: (target: object, property: string): any => {
this.emit(('getState.' + property) as keyof typeof this.__events, target[property as keyof typeof target]);
return target[property as keyof typeof target];
},
set: (target: object, property: string, value: any): boolean => {
(target[property as keyof typeof target] as typeof value) = value;
this.emit(('setState.' + property) as keyof typeof this.__events, target[property as keyof typeof target]);
return true;
},
};
}
/**
* @public @get @name state
* @description get the basic state of the store and emit an event
* @return the stores full state
*/
get state() {
this.emit('getState' as keyof typeof this.__events, this.__state);
return this.__state;
}
/**
* @public @set @name state
* @description OVERRIDDEN > You cannot set the full state of hte object, please define it in a child class
*/
set state(state: object) {
throw Error('Do not set state object directly, set props instead ' + Object.keys(state));
}
/**
* @public @get @name events
* @description combines default and custom events that may be added
* @return an array of the available states
*/
get events(): Array<string> {
return ['getState', 'setState', ...Object.keys(this.state).map((s) => 'getState.' + s), ...Object.keys(this.state).map((s) => 'setState.' + s), ...this.__customEvents];
}
/**
* @public @name addCustomEvent
* @description add a new custom event to use, when you say want to override the handlers and add your own events in
* @param event the custom event to add to defaults
*/
addCustomEvent(event: string) {
const events = typeof event === 'string' ? [event] : event;
events.forEach((ev) => {
if (this.events.includes(ev)) return false;
this.__customEvents.push(ev);
});
}
/**
* @public @name addEventListener
* @description add a new event listener, this callback will be fired when the event is emitted
* @param event the custom event listener to add
* @param scope any custom scope to use in the callback
* @param callback the callback function to call when event is emitted
*/
addEventListener(event: string, scope: any, callback: () => void) {
// is this a recognised event?
if (!this.events.includes(event)) return;
// check structure and if already present
if (!this.__events[event as keyof typeof this.__events]) (this.__events[event as keyof typeof this.__events] as Array<Event>) = [];
if ((this.__events[event as keyof typeof this.__events] as Array<Event>).find((e) => e.scope === scope && e.callback === callback)) return;
(this.__events[event as keyof typeof this.__events] as Array<Event>).push({ scope, callback });
}
/**
* @public @name removeEventListener
* @description remove an event listener from the queue
* @param event the custom event listener you added to
* @param scope the custom scope you used to add the listener
* @param callback the callback function used to add the listener
*/
removeEventListener(event: string, scope: any, callback: () => void) {
// is this a recognised event?
if (!this.events.includes(event)) return;
// check structure and if already present
if (!this.__events[event as keyof typeof this.__events] || (this.__events[event as keyof typeof this.__events] as Array<Event>).length < 1) return;
if (!(this.__events[event as keyof typeof this.__events] as Array<Event>).find((e) => e.scope === scope && e.callback === callback)) return;
(this.__events[event as keyof typeof this.__events] as Array<Event>) = (this.__events[event as keyof typeof this.__events] as Array<Event>).filter(
(e) => e.scope !== scope || e.callback !== callback,
);
}
/**
* @public @name subscribe
* @description subscribe method to add an event and offer a way to remove as a return function, for use with subscribing in react
* @param event the custom event listener you added to
* @param callback the callback function used to add the listener
*/
subscribe(event: string, callback: () => void): Store['removeEventListener'] {
this.addEventListener(event, null, callback);
return () => this.removeEventListener(event, null, callback);
}
/**
* @public @name syncState
* @description sync all known properties using reacts useSyncExternalStore passed in at sync time from component function
* @param useSyncExternalStore the react instance of useSyncExternalStore calling this from within the function block of the component
* @param props the properties to sync with for automatic re-renders of the component after change
*/
syncState(useSyncExternalStore: (callback: (callback: () => void) => any, snapshot: () => any) => any, props: string[] = []) {
if (!props || props.length < 1) props = Object.keys(this.state);
const cp = Object.keys(this.state);
props = props.filter((p) => cp.includes(p));
props.forEach((prop) => {
useSyncExternalStore(
(callback: () => void) => {
return this.subscribe(`setState${prop ? '.' + prop : ''}` as keyof typeof this.__events, callback);
},
() => {
return prop ? this.state[prop as keyof typeof this.state] : this.state;
},
);
});
}
/**
* @public @name emit
* @description emit an event and run any listeners waiting
* @param event the event to emit
* @param data any data associated with the event
*/
emit(event: string, data: any) {
if (!this.__events[event as keyof typeof this.__events] || (this.__events[event as keyof typeof this.__events] as Array<Event>).length < 1) return;
(this.__events[event as keyof typeof this.__events] as Array<Event>).forEach((e) => e.callback.call<string, any, any>(e.scope, event, data, e.scope));
}
}

需要更多的工作,但也许是这样的…

可以与

连用
System.syncState(useSyncExternalStore);

强制渲染所有存储属性的变化,然后你只需要使用从状态的值,如

System.state.someprop

或仅在特定属性更改时呈现

System.syncState(useSyncExternalStore, ['someprop', 'another']);

只在这些值改变时才重新渲染

在你的模板中只有一行,然后你可以选择如何节省你想要的重新渲染

相关内容

最新更新