Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/fix-provider-strict-mode-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@ledgerhq/wallet-api-client-react": patch
---

fix(client-react): create WalletAPIClient once in WalletAPIProvider

`WalletAPIProvider` created its client inside a `useState` initializer. Because
the `WalletAPIClient` constructor has a side effect — it registers itself on
`transport.onMessage` — React strict mode's double-invocation constructed a
second, discarded client whose constructor hijacked the transport. Responses
were then routed to a client with no pending requests, throwing
`no ongoingRequest`.

The client is now instantiated exactly once via the ref "create once" pattern,
so a single instance owns the transport even under strict mode.
2 changes: 1 addition & 1 deletion examples/client-nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { AccountsList } from "../components/AccountsList";
const isSimulator =
typeof window === "undefined"
? false
: new URLSearchParams(window.location.search).get("simulator");
: new URLSearchParams(window.location.search).has("simulator");

function getWalletAPITransport() {
if (typeof window === "undefined") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WalletAPIClient } from "@ledgerhq/wallet-api-client";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { WalletAPIProviderContext, initialContextValue } from "./context";
import type {
WalletAPIProviderContextState,
Expand All @@ -21,11 +21,22 @@ export function WalletAPIProvider({
initialContextValue.state,
);

const [client] = useState<WalletAPIClient | undefined>(
() =>
providedClient ??
new WalletAPIClient(transport, logger, getCustomModule, eventHandlers),
);
// A WalletAPIClient registers itself on `transport.onMessage` in its
// constructor, so it must be instantiated exactly once. A `useState`
// initializer is unsafe here: React strict mode invokes it twice, which
// constructs a second (discarded) client whose constructor hijacks the
// transport, leaving responses routed to a client with no pending requests
// ("no ongoingRequest"). The ref "create once" pattern guarantees a single
// instance that owns the transport.
const clientRef = useRef<WalletAPIClient | undefined>(undefined);
clientRef.current ??=
providedClient ??
new WalletAPIClient(transport, logger, getCustomModule, eventHandlers);
// Reading the ref during render is intentional here: the client is created
// once (above) and its identity is stable for the lifetime of the provider,
// so it can safely be passed down without triggering re-renders.
// eslint-disable-next-line react-hooks/refs -- stable create-once instance
const client = clientRef.current;

useEffect(() => {
if (eventHandlers) {
Expand Down
Loading