Flux and Discord
14 May 2022 (Updated: 23 March 2022)|5min read

#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.
Flux architecture 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.

#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.
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.
Hooks
Connect
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.
Props
Name
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.