Discord’s UI is built with React. BetterDiscord provides utilities for working with React components and the React tree.
Accessing React
React is available through BdApi:
const React = BdApi.React;
// Create elements
const element = React.createElement("div", { className: "my-element" }, "Hello World");
// Use JSX if your build process supports it
const element = <div className="my-element">Hello World</div>;
ReactUtils
The BdApi.ReactUtils namespace provides utilities for working with React internals.
Getting internal instances
Get the React fiber node from a DOM element:
const element = document.querySelector(".message-content");
const fiber = BdApi.ReactUtils.getInternalInstance(element);
if (fiber) {
console.log("Component:", fiber.type);
console.log("Props:", fiber.memoizedProps);
console.log("State:", fiber.memoizedState);
}
Getting owner instances
Find the nearest React component instance (usually a class component):
const element = document.querySelector(".message-content");
const owner = BdApi.ReactUtils.getOwnerInstance(element);
if (owner) {
console.log("Component instance:", owner);
console.log("Props:", owner.props);
console.log("State:", owner.state);
}
Filter by component name:
// Only find components with these names
const owner = BdApi.ReactUtils.getOwnerInstance(element, {
include: ["Message", "MessageContent"]
});
// Find components excluding these names
const owner = BdApi.ReactUtils.getOwnerInstance(element, {
exclude: ["Popout", "Tooltip"]
});
Default exclusions: ["Popout", "Tooltip", "Scroller", "BackgroundFlash"]
Use a custom filter:
const owner = BdApi.ReactUtils.getOwnerInstance(element, {
filter: (instance) => {
return instance.props.message?.author?.id === "123456789";
}
});
Wrapping HTML elements
Wrap HTML elements in a React component:
const htmlElement = document.createElement("div");
htmlElement.textContent = "Custom content";
const WrappedComponent = BdApi.ReactUtils.wrapElement(htmlElement);
// Use in React
BdApi.showConfirmationModal("Title", [
BdApi.React.createElement(WrappedComponent)
]);
Wrap multiple elements:
const elements = [
document.createElement("div"),
document.createElement("span")
];
const WrappedComponent = BdApi.ReactUtils.wrapElement(elements);
Working with exotic components
Unwrap components from React.memo, React.forwardRef, and React.lazy:
const wrappedComponent = React.memo(MyComponent);
const actualComponent = BdApi.ReactUtils.getType(wrappedComponent);
console.log(actualComponent === MyComponent); // true
This is useful when patching:
const module = BdApi.Webpack.getByKeys("MessageContent");
const Component = BdApi.ReactUtils.getType(module.MessageContent);
// Now you can patch the actual component
BdApi.Patcher.after("MyPlugin", module, "MessageContent", callback);
Rendering functional components
Render functional components outside of React’s normal lifecycle:
const FunctionalComponent = (props) => {
const [count, setCount] = React.useState(0);
return React.createElement("div", null, `Count: ${count}`);
};
// Wrap to render with hooks support
const wrapped = BdApi.ReactUtils.wrapInHooks(FunctionalComponent);
const result = wrapped({ someProp: "value" });
console.log(result); // Rendered React element
Provide custom hook implementations:
const wrapped = BdApi.ReactUtils.wrapInHooks(FunctionalComponent, {
useState(initial) {
return ["custom value", () => {}];
},
useEffect() {
console.log("Effect called");
}
});
wrapInHooks uses mock hooks. State changes won’t cause re-renders. Use this only for inspecting components or testing renders.
Creating React components
Functional components
Create simple functional components:
function MyComponent(props) {
return BdApi.React.createElement("div", {
className: "my-component",
style: { color: props.color }
}, props.children);
}
With JSX:
function MyComponent({ color, children }) {
return (
<div className="my-component" style={{ color }}>
{children}
</div>
);
}
Using hooks
Use React hooks in your components:
function Counter() {
const [count, setCount] = BdApi.React.useState(0);
BdApi.React.useEffect(() => {
console.log("Count changed:", count);
}, [count]);
return BdApi.React.createElement("button", {
onClick: () => setCount(count + 1)
}, `Count: ${count}`);
}
Common hooks:
useState - Component state
useEffect - Side effects
useCallback - Memoized callbacks
useMemo - Memoized values
useRef - Persistent references
useContext - Context values
Class components
Create class-based components:
class MyComponent extends BdApi.React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
console.log("Component mounted");
}
componentWillUnmount() {
console.log("Component will unmount");
}
render() {
return BdApi.React.createElement("div", null,
`Count: ${this.state.count}`
);
}
}
Modifying React trees
Finding elements
Search through React elements:
function findInTree(tree, filter) {
if (filter(tree)) return tree;
if (tree?.props?.children) {
if (Array.isArray(tree.props.children)) {
for (const child of tree.props.children) {
const result = findInTree(child, filter);
if (result) return result;
}
} else {
return findInTree(tree.props.children, filter);
}
}
return null;
}
// Usage in a patch
BdApi.Patcher.after("MyPlugin", module, "render", (thisObject, args, returnValue) => {
const button = findInTree(returnValue,
e => e?.props?.onClick && e.type === "button"
);
if (button) {
button.props.className += " custom-button";
}
return returnValue;
});
Modifying children
Safely modify React children:
BdApi.Patcher.after("MyPlugin", module, "render", (thisObject, args, returnValue) => {
if (!returnValue?.props?.children) return returnValue;
const children = returnValue.props.children;
// Handle arrays
if (Array.isArray(children)) {
children.push(BdApi.React.createElement("span", null, "Added!"));
}
// Handle single child
else {
returnValue.props.children = [
children,
BdApi.React.createElement("span", null, "Added!")
];
}
return returnValue;
});
Using React.Children
Utilities for working with children:
function MyComponent({ children }) {
const childArray = BdApi.React.Children.toArray(children);
// Map over children
const modifiedChildren = BdApi.React.Children.map(children, child => {
if (BdApi.React.isValidElement(child)) {
return BdApi.React.cloneElement(child, {
className: "modified"
});
}
return child;
});
return BdApi.React.createElement("div", null, modifiedChildren);
}
Context
Reading context
Access React context values:
const ThemeContext = BdApi.Webpack.getByKeys("ThemeContext")?.ThemeContext;
function ThemedComponent() {
const theme = BdApi.React.useContext(ThemeContext);
return BdApi.React.createElement("div", {
style: { backgroundColor: theme === "dark" ? "#000" : "#fff" }
});
}
Creating context
Create your own context:
const MyContext = BdApi.React.createContext("default value");
function Provider({ children }) {
return BdApi.React.createElement(MyContext.Provider, {
value: "provided value"
}, children);
}
function Consumer() {
const value = BdApi.React.useContext(MyContext);
return BdApi.React.createElement("div", null, value);
}
Node patching
Create a node patcher for advanced DOM manipulation:
const nodePatcher = BdApi.ReactUtils.createNodePatcher();
nodePatcher.patch({
callback: (node) => {
if (node.classList.contains("message-content")) {
node.style.backgroundColor = "yellow";
}
}
});
// Clean up when done
nodePatcher.unpatch();
Best practices
- Always clean up in
componentWillUnmount or useEffect cleanup
- Use
React.memo for expensive components
- Avoid creating components inside render methods
- Handle missing props and children gracefully
- Use proper dependency arrays in
useEffect and useMemo
- Test your components with React DevTools
Modifying React’s internal fiber structure can cause crashes or unexpected behavior. Always test thoroughly.