createInterceptor(...) entries.
| Step role | Example target | What you inject |
|---|---|---|
| Feature surface (body) | TradingView.Desktop | Main widget next to the original desktop layout |
| Control surface (menu) | TradingView.DisplayControl.DesktopMenuList | A menu row that toggles visibility and stays in sync |
https://github.com/OrderlyNetwork/fast-place-order-plugin. Read src/index.tsx (registerFastPlaceOrderPlugin) alongside the steps below; the repo is the canonical runnable example for this tutorial.
Prerequisites: Tutorial 1 — plugin scaffold and registerPlugin basics.
Scope: Implementation inside the plugin package only (interceptors, shared state, dedupe, Hooks). Loading the plugin in a host app, provider wiring, and production QA are not covered on this page.
Step 1 — Understand why you need multiple targets
A single interceptor is enough for simple overlays. For product-grade UX you often need several insertion points — for example:- Body target — where the feature lives (panel, popup, extra controls).
- Menu target — where users discover and switch the feature (display control list, settings, and so on).
- Further targets — headers, mobile sheets, or secondary menus; same registration, more
createInterceptorrows.
useFastPlaceOrderVisibility shared between the menu interceptor and the widget.
Step 2 — Define the plugin factory and options
- Import
createInterceptorandOrderlySDKfrom@orderly.network/plugin-core. - Export a register function such as
registerFastPlaceOrderPlugin(options?)that returns(SDK: OrderlySDK) => { ... }and, inside that callback, callsSDK.registerPlugin({ ... })(the usual plugin factory shape from Tutorial 1). - Normalize options once (for example
const autoShowOnFullscreen = options?.autoShowOnFullscreen ?? true) so every interceptor sees the same values.
Step 3 — Call SDK.registerPlugin once with a stable id
Inside the returned function, call SDK.registerPlugin({ ... }) a single time with:
id,name,version,orderlyVersion— as required by your release process.interceptors: [ ... ]— onecreateInterceptor(...)per target (this guide covers two; add more entries for additional surfaces).- Optional
setupfor non-UI side effects (subscriptions, logging).
registerPlugin call, two interceptor slots. Placeholders omit widget/menu logic; Steps 4–6 fill those in.
items={nextItems} on the same <Original /> (Steps 5–6).
Many targets still mean one plugin registration and one shared lifecycle.
Step 4 — Interceptor A: mount the widget on the desktop body
Target:"TradingView.Desktop" (string must match runtime injector targets; paths are case-sensitive).
Pattern:
- Wrap with anything the widget needs globally (the reference uses
LocaleProviderfor i18n). - Render
<Original {...props} />first so the default trading view stays intact. - Render your widget after the original, passing through whatever props the injector supplies for that target (for example
symbol={props.symbol}) plus your plugin options (autoShowOnFullscreen).
interceptors array, then menus and chrome, so the file reads top-down like the user journey.
Step 5 — Interceptor B: inject a menu item on the display control list
Target:'TradingView.DisplayControl.DesktopMenuList'.
Pattern:
- Read/write shared visibility with the same hook the widget uses (in the reference,
useFastPlaceOrderVisibility(false)inside the interceptor callback). - Build the next
itemsarray fromprops.items. - Return
<Original {...props} items={nextItems} />so you only extend the list, not replace the whole menu implementation.
Step 6 — Dedupe menu items before append
List targets re-render often. If you always push a new object without filtering, you can get duplicate rows. Do this every time you buildnextItems:
- Choose a stable string
idfor your row (for example"fastPlaceOrderPopupToggle"). - Remove any existing item with that
idfromprops.items ?? []. - Append your
customItemonce.
useMemo so the array identity stays stable when dependencies (isWidgetVisible, props.items, setter) do not change:
Step 7 — Keep Hook usage valid in interceptors
In this Orderly pattern, the interceptorcomponent: (Original, props) => ... runs as a React function component, so Hooks are allowed there.
Rules of thumb:
- Call Hooks unconditionally and in a fixed order on every render.
- If the interceptor grows large, extract a small inner component and call Hooks there instead — same rules, easier to read.
react-hooks rules.
Step 8 — Verify from the plugin project
Stay inside the plugin repo / package:pnpm build(or your package script) completes with no TypeScript errors.pnpm lintpasses if your pipeline requires it.- Code review pass:
interceptorsarray lists every intended target once; menu branch uses stableid+ filter (Step 6) so duplicates cannot accumulate on re-render. - Hook audit: no conditional Hook calls in interceptor callbacks (Step 7).
Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Widget shows, no menu row | Wrong menu target string | Compare with runtime injector targets letter by letter |
| Menu row does nothing | Visibility not shared with the widget | One hook/store used by menu and widget (same module as the reference) |
| Duplicate menu rows | No filter-by-id before append | Step 6 dedupe |
| Invalid Hook call | Conditional Hooks or wrong nesting | Step 7; simplify or extract a child component |
Skills and tooling
| Skill / doc | Use when |
|---|---|
orderly-plugin-write | Designing interceptors and Hook-safe composition |
| Runtime injector targets | Confirming exact target path strings while coding |
Next step
Extract target strings into shared constants and add module augmentation for typedprops on each target so createInterceptor(...) stays accurate as the SDK evolves.