#State management in React
When working with
React, the UI has a state, which determines the elements rendered onscreen.
Changing said state will cause React to rerender the affected UI components.
React itself already comes with some tools to manage state:
the
State API,
which can be used via the
this.setState() method in class components
and via the newer
useState() hook in function components.
For managing global state React also features a lesser known
Context API out of the box.
However, in large applications state management can prove more difficult or inefficient.
That's what resulted in the
Flux architecture for managing data flow and application state in React.
While the original Flux project by Facebook themselves is no longer actively developed, the architecture has proven useful.
Popular state management libraries of today like
Redux are built upon it.
Source: Facebook's Flux docs
The Flux pattern traditionally consists of central Dispatcher(s), which can receive so-called Actions.
An Action represents a modification of the application's state and has an identifying type (kind) as well as a potential payload of additional data.
Actions may also be viewed as events, which happen within the application.
The dispatcher is responsible for forwarding the received data to the relevant Stores.
Each store is used to store state information relevant to a specific part of the application.
This allows to separate for example which application language the user has selected from which chatroom they are currently viewing.
You can read more about the architecture in the
In-Depth Overview within the Flux Docs.
The store data as well as the ability to dispatch actions now has to be made available for UI components.
For the classic class-based components, this used to be typically done with wrapper components,
which take care of updating and give the inner component access to store data via props or custom methods.
This API is sometimes also referred to as "Connect" since the name of the function used to wrap a component usually includes the word connect.
The alternative API for function component would be realized via the newer concept of
hooks.
i
The React community has been moving more and more towards
function components and hooks.
They make it easy to use data within a component and have it update automatically.
Libraries like
React Redux are now recommending usage of their hooks API over older class component APIs.
#Discord's Flux implementation
Discord uses a (as far as we know) custom implementation of the Flux architecture.
It forms a large part of the data layer behind the UI layer.
!
Named exports have been mangled, which also affects the hooks mentioned below.
Their implementation has both a newer hooks API they are transitioning to as well as an old connect API, which is still used in parts of the codebase.
Both APIs require the relevant stores in an array and a callback which computes the data and is invoked whenever a change has been detected.
All hooks also take an optional dependency array.
In the case of useStateFromStores() a comparator function can be passed as 4th argument to customize update behaviour.
The useStateFromStoresArray() and useStateFromStoresObject() hooks come with a predefined comparator for arrays and objects respectively.
1const MyComponent = () => {
2 const currentUser = useStateFromStores([UserStore], () => UserStore.getCurrentUser());
3
4 return (
5 <div>Current user is: {currentUser.username}</div>
6 );
7};
Their stores as well as their central dispatcher also allow for listeners, which are notified of any incoming actions/events.
This can be useful when you want to use store updates somewhere outside of the UI layer.
1const listener = (action) => console.log("Dispatcher received:", action);
2
3Dispatcher.subscribe("ACTION_TYPE", listener);
4
5Dispachter.unsubscribe("ACTION_TYPE", listener);
1const listener = () => console.log("UserStore was updated");
2
3UserStore.addChangeListener(listener);
4
5UserStore.removeChangeListener(listener);
Besides using the intended functions for dispatching specific actions, actions can also be dispatched directly on the dispatcher using Dispatcher.dispatch().
There is a massive amount of different actions.
In May 2022 nearly 1500 entries were listed within Discord's internal ActionType TypeScript enum.
The enum has since been removed (most likely turned into a const enum) in August 2022.
#Finding Stores
As with all other internals, stores need to be found within the webpack exports cache and have to be reverse engineered.
You can find more information in my
Getting started with BetterDiscord Plugin development guide.
In 2022 Discord added a
getName() method to their store class.
The name is also present as
displayName property on the store constructors.
(Note: BetterDiscord's byDisplayName filter does not search constructors as of now.)
Theoretically you can now find stores using their name rather than interface or other characteristics.
Store names can also prove useful when figuring out a store's purpose.
1const GuildStore = getModule(Filters.byProps("getGuild"));
2const ChannelStore = getModule(Filters.byProps("getChannel", "hasChannel"));
3const UserStore = getModule(Filters.byProps("getUser", "getCurrentUser"));
Usually a good way to start is to look at a part of the UI where data from the store you are searching for is supposedly used.
Go up the parent chain and try to find the first "normal" component whose props do not include the data itself or children with the data already "rendered".
The relevant stores are most likely accessed within this component.
Alternatively you can attempt taking educated guesses regarding the store's name or methods - either complete or partial, like name.toLowerCase().includes("guild").
A list of all stores can be retreived by either checking the name or by making sure it is an instance of Flux.Store:
1getModule(
2 (exports) => typeof exports?.getName === "function" && exports.getName().endsWith("Store"),
3 { first: false }
4)
1getModule((exports) => exports instanceof Flux.Store, { first: false })
Note that the first version will be missing a couple of stores which have no name set.