whisk
Concepts

State machine

The states Whisk transitions through, what triggers each transition, and how to read them.

Whisk runs on a small explicit state machine. Each visible state of the widget corresponds to exactly one node in the machine, and every transition is pure and reproducible. If you can name the state, you know what's on screen.

The states

kindWhat's happening
disconnectedNo wallet connected. Connect CTA is shown.
idleConnected. The user is filling in the form.
resolvingLooking up a recipient (address or ENS).
quotingBuilding the route and fees for the current input.
reviewQuote is ready. Confirmation card is on screen.
sendingTransaction submitted. Step rail tracks bridge state.
successTransfer or bridge completed. Result card visible.
errorTerminal failure. The banner shows a typed WhiskError.

Reading state in React

The discriminated union narrows automatically once you check kind. You don't need optional chaining because TypeScript knows which fields exist in each branch:

const { state } = useWhisk();

if (state.kind === "review") {
  console.log("Sending", state.quote.amountIn, "USDC");
}

if (state.kind === "error") {
  toast.error(state.error.message);
}

This is the part of Whisk worth understanding well. Building any custom UI on top of useWhisk is mostly a matter of branching on state.kind and rendering the right slice.

Triggering transitions

You don't dispatch reducer actions yourself. The actions object from useWhisk is the public surface. It's stable across renders, so it's safe to include in useEffect deps:

const { actions } = useWhisk();

// disconnected → idle happens automatically on wallet connect.
await actions.resolve("vitalik.eth", "Base_Sepolia"); // → idle (with recipient)
await actions.quote(recipient, "10", "Arc_Testnet"); // → review
await actions.send(); // → success | error
actions.back(); // review → idle
actions.reset(); // any → idle

back() is intentionally narrow; only available from review. It won't drop you back to the disconnected state if you're mid-bridge. For "cancel everything," call reset().

Hooking external listeners

<WhiskSend onStateChange={...}> fires for every transition. Common uses:

<WhiskSend
  onStateChange={(state) => {
    if (state.kind === "sending") toast.loading("Sending…");
    if (state.kind === "success") toast.success("Done.");
    if (state.kind === "error") toast.dismiss();

    appStore.setWhiskState(state.kind);
  }}
/>

Resolving and quoting are noisy. The user can change the recipient or amount mid-typing and you'll see lots of resolving → idle → quoting → review cycles. If you only care about meaningful transitions, gate the callback:

const meaningful = new Set(["review", "sending", "success", "error"]);

onStateChange={(state) => {
  if (meaningful.has(state.kind)) analytics.track(state.kind);
}}

Driving the machine without React

The reducer is exported from core: reduce, initialState, initialSteps, and the WhiskAction action type. You can drive the machine from anything — a server-side workflow, an integration test, a Vue component. The React hooks just wrap it.

Next

How Whisk decides between same-chain and bridge routes.

On this page