Getting started with BetterDiscord plugin development
14 January 2022 (Updated: 23 March 2023)|15min read
This guide is supposed to act as an introduction to plugin development for the Discord client modification BetterDiscord. It mostly focuses on covering topics that are not covered by other resources, like monkey patching or webpack internals.

#Environment

The Discord desktop application uses Electron. Note that plugins run in Electron's renderer process. This means our environment is a browser not Node.js!. As of September 2022, BetterDiscord polyfills a small subset of absolutely necessary functions in modules shipped with Node. However, this is far from exhaustive.
The UI is created using React and all of the data is handled by a custom event system and a custom Flux implementation. Discord's source code is written in TypeScript, however that is mostly irrelevant for plugin development. Everything is transpiled with SWC and then bundled together using webpack.
Thanks to being based on Chromium, Electron's renderer window has the Chrome DevTools. Since the Discord update on 25 January 2022 they are disabled by default. You can reenable them in BetterDiscord's developer settings.
BetterDiscord dev tools settings
Afterwards you can access the dev tools by pressing Ctrl + Shift + I (or Cmd + Opt + I on a Mac). The dev tools will be extremely important for any kind of reverse engineering later on and can be used to experiment in the console.

#React Dev Tools

React Developer Tools extends the regular dev tools with React specific tabs. If you have debugged a React application before, you should be familiar with them. They will be very helpful when working with Discord's own React components.
In order to use them in Discord, you will need to have a Chrome installation on your computer. Install the React Developer Tools extension from the Chrome Web Store. (As an alternative manually mimicking the folder structure and placing the extension files in there will also suffice.) Then enable the setting in BetterDiscord's developer settings and the dev tools will automatically be loaded in Discord as well.
React dev tools

#BetterDiscord API

BetterDiscords full API will not be covered here in details. You can find additional information in the BetterDiscord documentation (some parts work in progress). If you encounter any outdated or unclear parts, you should ask in the BetterDiscord server.
A basic BetterDiscord plugin could look something like this:
Class
Callback
1/**
2 * @name MyPlugin
3 * @author YourName
4 * @version 0.1.0
5 * @description What the plugin does.
6 */
7
8module.exports = class MyPlugin {
9 constructor(meta) {
10 // do something when the plugin is loaded
11 }
12 start() {
13 // do something on plugin start
14 }
15 stop() {
16 // do something on plugin stop
17 }
18};
BetterDiscord also provides the BdApi global, which has a few very helpful things for plugins. It handles storing & loading plugin data as well as injecting & removing styles (CSS).
The react and react-dom packages are available as BdApi.React and BdApi.ReactDOM. They are the same instances as the ones used by Discord. Always use these and do not bundle your own versions.
JSX
JS
1const { React } = BdApi;
2
3const MyComponent = () => (
4 <div className="foo">
5 Hello world!
6 </div>
7);
ReactDOM will not see much use in actual plugins, since most of the time you are rendering into already existing React element trees.
In BdApi we also find the BdApi.Patcher, which is used to modify existing functions at runtime. More details on patching will follow later on.
Lastly, BdApi.Webpack features a few utilities used to search through the webpack export cache. We will take a closer look at this next.

#Webpack modules

Webpack is a bundler, meaning at its core it is a tool to take a collection of JS modules and merge them together into one or multiple big files for production. A module may import other modules and then expose some exports of its own. In order to avoid executing the modules multiple times, webpack keeps exports of already executed modules in a cache.
BdApi.Webpack allows us to search through this export cache in order to find variables, functions, objects or React components internally used by Discord. Webpack.getModule(filter, options) takes a filter function and searches through the exports of all currently cached modules.
Webpack.Filters contains helpers to create commonly used search filter patterns. Filters.byProps(...props) possibly should have been named something like byKeys instead, since people tend to confuse it with React's Component props. It has nothing to do with React's props, it searches for specific properties (keys) in module.exports. Filters.byDisplayName(name) searches for a specific displayName set on the object. Filters.byStrings(...strings) is best explained as something similar to target.toString().includes(...).
When trying to hook into Discord's internals, searching for the relevant webpack modules is the first step. This may involve reading Discord's source code, inspecting components through React dev tools, experimenting in the console etc.
The Discord update on 27 September 2022 brought some major changes to characteristics of their production code. Named top-level exports have had their names "mangled" (minified). The module merging also became more aggressive, merging e.g. all of the channel list components into a single module. These changes pose additional challenges when searching modules.

#Finding a component

As a first example, we will attempt to find Discord's button component in their webpack modules. When inspecting in React devtools, the first component we stumble upon is called U. Looking at its received props, we realize this cannot be the component we are looking for: it is receiving the element as children, meaning those were rendered by one of its parents!
Button component U
Going up to the direct parent we find component y, which is the component we are looking for. We inspect its source and click the pretty print button in oder to see what we are working with.
View source
This brings us to something similar to:
1var y = function (e) {
2 var t = e.look,
3 n = void 0 === t ? m.FILLED : t,
4 i = e.color,
5 u = void 0 === i ? T.BRAND : i,
6 _ = e.borderColor,
7 O = e.hover,
8 v = e.size,
9 y = void 0 === v ? g.MEDIUM : v,
10 /* other props... */;
11 // local vars, hooks...
12 var ie = (0, r.jsx)(s.tE, /* react elements... */);
13 if (te) {
14 // alternative return...
15 }
16 return ie;
17}
18y.Looks = m;
19y.Colors = T;
20y.BorderColors = O;
21y.Hovers = v;
22y.Sizes = g;
23y.Link = function (e) {
24 // component source...
25};
26const A = y;
Going up to the top of the module, we find that our component is exported directly (line 9):
1191940: (e, t, n) => {
2 "use strict";
3 n.d(t, {
4 iL: () => m,
5 Tt: () => T,
6 lN: () => v,
7 Ph: () => g,
8 nY: () => S,
9 Co: () => A
10 });
To explain the codeblock above: it starts with a key in an object literal, which is the id of the module. The following value is the module function holding the module's code. When evaluating the module webpack will pass its own versions of module, module.exports, require as arguments in this order. require.d is a helper to define getters.
Jumping back to the button component itself, we will take a look at two approaches for retrieving it. We do so by formulating a search predicate in order to match it within webpack's export cache.
When formulating predicates you have to consider how specific it should be. When being very specific you are unlikely to accidentally find a different module that also matches the predicate. However, a very specific predicate may also break very fast when Discord changes something in their internals. You can check how many potentially wrong modules your predicate currently matches by passing first: false to getModule().
Our first approach is the currently most common way to build search predicates for components. We do not any have export names or component displayNames to work with. However, the component possibly still accesses the React component props it recieves. We can use these to create a predicate using Filters.byStrings(...strings). Looking at the component source, we can figure out which accesssed props could be a suitable for our query.
In this case submittingStartedLabel and submittingFinishedLabel are two props with fairly specific names, which are accessed within the component. And indeed, we only get a single match using either of them:
1getModule(Filters.byStrings("submittingStartedLabel"), { searchExports: true, first: false })
If we are afraid these props are too special and might be removed from the component in the future, we can combine multiple of the more "basic" props. The query below returns 12 matches:
1getModule(Filters.byStrings("look", "submitting"), { searchExports: true, first: false })
One thing we can change is to add the . from accessing the property in front. Discord's toolchain (usually) transpiles destructuring props into regular property access operations. This cuts down our number of matches to 2:
1getModule(Filters.byStrings(".look", ".submitting"), { searchExports: true, first: false })
We can also take a look at the "wrong" matches and attempt to find code fragments that are present in our component but not the others. In this case the other component for example does not access an onClick prop, leaving us with this possible query for the button component:
1const Button = getModule(Filters.byStrings(".look", ".submitting", ".onClick"), { searchExports: true });
Our second approach for finding the button component can only be applied in special cases. Looking at our component source code again, we happen to have a handful of assignments right below our function component. These add enum-like objects and another associated function component. Knowing this, we can attempt to use Filters.byProps(...props) and match these properties instead of matching the component source:
1const Button = getModule(Filters.byProps("Link", "Colors"), { searchExports: true });
If a component or function is not exported from its module, things become a lot more complicated. More advanced techniques like chain patching or dummy rendering plus traversing React fiber will be required.

#Finding a store

Discord uses the Flux architecture to organize their data. You can read more about this in Flux and Discord. In the Flux architecture Stores hold all of the relevant state information.
As an example, we will attempt to find the UserStore. As our entry point for reverse engineering we pick the component responsible for rendering the account panel below the channel list. We start by searching through the component tree for a component which does not receive the data as props from a parent. This brings us to a component called Tee:
1function Tee() {
2 var e = (0, s.e7)([Y.default], (function() {
3 return Y.default.getCurrentUser()
4 })),
5 // some other variables...
6 a = null == e ? void 0 : e.id,
7 l = He.Ok.useSetting(),
8 c = o.useMemo((function() {
9 return null != l ? (0,
10 Z8.Z)(l) : null
11 }), [l]),
12 u = (0, s.cj)([Hc.Z], (function() {
13 return {
14 streaming: null != Hc.Z.findActivity((function(e) {
15 return e.type === Z.IIU.STREAMING
16 }
17 )),
18 status: Hc.Z.getStatus()
19 }
20 })),
21 f = u.streaming
22 d = u.status
23 p = (0, s.e7)([oy.Z], (function() {
24 return null != a && oy.Z.isSpeaking(a)
25 }), [a]),
26 // rest...
27}
There is a fairly obvious call to getCurrentUser() in there. But let's figure out what the rest around it is.
We can take a closer look at the component by selecting it in React devtools and typing $r in console. The type will contain the function, which we can help us figure out what values captured variables contain. If we expand the function, we see a [[Scopes]] entry at the bottom. We can look through these manually or in case one scope of captured variables is very large: right click, store as global variable and then access an entry like temp1.object.s.
Tee scopes
Another helpful technique to help with reverse engineering is to set a breakpoint near the end of the function/component. Then attempt to trigger a call/rerender. When the breakpoint is hit, we can take a look at which values captured as well as local variables hold.
It turns out our captured variable s is Discord's Flux module. And the functions s.e7 and s.cj are the mangled useStateFromStores and useStateFromStoresObject hooks. Y.default is the store we are searching. (Yes, big surprise...) Besides getCurrentUser we for example also find the functions getUsers, getUser or findByTag there.
Production code
Reverse engineered
1var e = (0, s.e7)([Y.default], (function() {
2 return Y.default.getCurrentUser()
3 })),
4 a = null == e ? void 0 : e.id;
Looking at our store, we can easily grab it using Filters.byProps(...props). Since it is exported as default export from its module, we do not need searchExports: true. BetterDiscord checks our filter on both the whole exports object as well the default export by default. Since using "getUser" or "getCurrentUser" on their own gives us more than 1 match, we combine them:
1const UserStore = getModule(Filters.byProps("getUser", "getCurrentUser"));
Alternatively, we can grab the store by its name:
1const UserStore = getModule((exports) => exports?.constructor?.displayName === "UserStore");
1const UserStore = getModule((exports) => typeof exports?.getName === "function" && exports.getName() === "UserStore");

#Patching

Now that we know how to find modules, we can take a look at how we can hook into a function exported by one. This is done using the already mentioned BdApi.Patcher. It allows us to change the behaviour of existing functions at runtime, executing our own code before, after or instead of the original. This is commonly referred to as monkey patching or simply patching.
There is three different kinds of patches the BdApi.Patcher offers. Typically, you will use before patches when you want to intercept and manipulate the incoming function arguments, after patches when you want to intercept and manipulate the resulting return value and instead patches when the behaviour should be changed completely or for other complex scenarios.
In React components you typically want to modify the return value of the original in order to change the elements rendered. This means we want to create an after patch. For class components the target function is Component.prototype.render() usually. For function components it is the component itself.
Note that the patcher expects an object containing the target function and the key of the function entry. You may have to adjust your filter and/or use defaulExport: false when searching with the intent to patch.
1const myComponentModule = getModule(/* filter */);
2
3Patcher.after("MyPluginName", myComponentModule, "myComponentKey", (thisObject, originalArgs, returnValue) => {
4 console.log("Original function received arguments:", originalArgs);
5 console.log("Original function returned value:", returnValue);
6});
After the component has been patched, we still need to trigger a rerender. We should see output being logged into the console for each instance of the patched component that renders onscreen.
Console output
The example above is output from a function component. The first argument received is the component props just as you would expect with a function component. The second argument is an empty object since the component does not forward any refs (or makes use of React's legacy context API).
As to be expected, the return value is what a call to React.createElement() returns - a tree of React element nodes. The most interesting properties on these nodes are type, which indicates what kind of element or component this tree node is, and props, which is the props passed to the HTML element or React component. Child nodes are found under props.children.
If we want to render our own elements somewhere, we have to find the node we want to append them to. This can be done by using tree/graph traversal algorithms like DFS or BFS. You can find examples of utility functions for tree traversal in BdApi.Utils (DFS), Zere's Plugin Library (DFS) or my own plugin repo (BFS).
With a utility function to search the tree for a specific node, appending new elements could look like this:
JSX
JS
1const findInReactTree = (tree, filter) => Utils.findInTree(tree, filter, { walkable: ["props", "children"] });
2
3Patcher.after("MyPluginName", myComponentModule, "myComponentKey", (thisObject, [props], returnValue) => {
4 const foundNode = findInReactTree(returnValue, (node) => node?.type === "li");
5 if (foundNode) {
6 foundNode.props.children = [
7 foundNode.props.children,
8 <div>My elements</div>
9 ];
10 } else {
11 console.error("node was not found");
12 }
13});
Remember to undo the patches you did in start() when your plugin is stopped:
1stop() {
2 Patcher.unpatchAll("MyPluginName");
3}

#Further information

Hopefully this helped you a bit while getting started with developing BetterDiscord plugins and using Discord's internals. Make sure to check the BetterDiscord documentation for information. Reading the source code of other plugins can also help a lot when learning. (Maybe not plugins using BDFDB by DevilBro as they can be difficult to read.)
React's internals and the React fiber have not been touched here at all, but may be interesting for plugin development in some cases. Same thing goes for Discord's internal state management system using event dispatching and Flux.
If you have any questions, the #programming channel in the BetterDiscord server is the place to ask.