What is patching?
Patching (sometimes called monkey-patching) lets you wrap an existing function with your own logic: run code before it, after it, or in place of it. It’s how a plugin changes behavior it doesn’t own, from a Discord method deep in a module to a component’s render. The idea is simple. If a function lives on an object, you can replace that property with your own wrapper that calls the original. Doing this by hand is fiddly: you have to keep a reference to the original, restore it later, and cooperate with other plugins patching the same function. Thepatcher does all of that for you.
It’s available as unbound.patcher and from @unbound-app/api.
Full type inference
args, result, and this are typed from the target automatically. See Typing a patch.Scoped cleanup
Patches grouped under a name revert together in one call.
Error isolation
Each callback runs in its own try/catch, so one bad patch won’t break the rest.
Garbage-collection safe
Targets are held weakly, so patching an object never keeps it alive on its own.
Creating a patcher
CallcreatePatcher with a name to get an instance scoped to your plugin. The name labels your patches so they can be reverted together. Every patch you apply through this instance is tracked, and one call reverts all of them.
Give the patcher the same name as your plugin’s
id. It keeps your patches grouped under one label and makes them easy to clean up in stop.The three patch types
You hook a function at one of three points. Each takes the parent object, the method name on it, and a callback that receives a context object.- before
- after
- instead
Runs before the original. The callback gets
args, which you can mutate to change what the original receives. Useful for rewriting inputs or blocking a call.The patch context
Every callback receives one context object. Which fields are meaningful depends on the patch type.The arguments passed to the function. Mutate this in a
before patch to change what the original receives.The original’s return value. Available in
after patches; return a value to replace it.The unpatched function. Call it yourself in an
instead patch to run the original behavior.The
this the function was called with. Bind it when you call original (original.apply(self, args)).To pass data from a
before patch to an after patch on the same call, stash it on this (or on an argument object) in before and read it back in after. The two callbacks share the same context for a given call.The this context
ctx.this is whatever the function was called with, exactly as JavaScript resolved it at the call site. What that is depends on how the function was invoked.
- class method
- object method
- bound function
Patching a method on a class prototype,
this is the instance the method was called on. Read its props, state, or fields through ctx.this.Returning values
Each patch type has two ways to change the call, and they’re interchangeable: pick whichever reads cleaner.| Patch | Mutate in place | Or return |
|---|---|---|
before | edit ctx.args | a new args array |
after | set ctx.result | the replacement result |
instead | (n/a) | the function’s return value |
before patch can change the arguments either by editing ctx.args or by returning a fresh array. An after patch can set ctx.result or return a new value; returning nothing keeps the original result. An instead patch’s return value is the function’s return value, so to keep the original behavior you must call original and return what it gives you.
Typing a patch
The patcher infers types from the function you target. Point it at a typed module andargs, result, and this are all typed for you, with no annotations.
PatchContext<Args, Result, Self>. All three parameters are optional and default to any, so pass only the ones you need.
ctx.result is Res | null. It’s null in before and instead callbacks (the original hasn’t run yet) and populated in after. Reach for it only in after patches.One-time patches
Pass{ once: true } as a fourth argument and the patch reverts itself automatically after it fires once. This is the right tool for reaching a value that only exists for a single render or call, such as patching a child component created fresh on each parent render (see Temporary patches from a parent).
Patching classes
The same three methods work on constructors. Patch the class on its parent module by name, and the context behaves the same:args are the constructor arguments, and in an after patch result is the new instance.
Execution order
When several patches target the same function, they run in a fixed order:instead (or the original)
The
instead patches run. If there are none, the original function runs here.Each callback runs inside its own try/catch. If one patch throws, the error is logged and the remaining patches still run, so a single broken plugin won’t take down everyone else’s patches on the same function.
Cleaning up
CallunpatchAll in your plugin’s stop to revert every patch the instance applied. This restores each function to its original and lets the next plugin (or a re-enable of yours) patch cleanly.
patcher.unpatchAllByCaller(name) removes every patch registered under a caller name, and patcher.unpatchAll() (the module-level function, not the instance method) removes every patch globally.