Skip to content

Commit 2fccfc0

Browse files
committed
[SDK] Fix caching issue with headless UI when custom resolvers are used (#5657)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR addresses caching issues in headless components and enhances code coverage. It introduces the `getFunctionId` utility in various components to improve resolver handling for names and symbols. ### Detailed summary - Added `getFunctionId` to various components for handling resolvers. - Updated `NFTName`, `NFTDescription`, and `NFTMedia` to use `getFunctionId` instead of `toString()`. - Enhanced `ChainIcon`, `TokenIcon`, `ChainName`, and `TokenName` to include resolver handling. - Introduced `fetchChainName` and `fetchTokenName` for improved name resolution. - Added tests for `fetchChainName`, `fetchTokenName`, `fetchTokenSymbol`, and their respective query keys. - Updated `TokenSymbol` to utilize `fetchTokenSymbol` and improved resolver handling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent d9fb544 commit 2fccfc0

File tree

13 files changed

+552
-108
lines changed

13 files changed

+552
-108
lines changed

.changeset/fluffy-pets-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Fix caching issues for headless component; improve code coverage

packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type { JSX } from "react";
55
import { getChainMetadata } from "../../../../../chains/utils.js";
66
import type { ThirdwebClient } from "../../../../../client/client.js";
7+
import { getFunctionId } from "../../../../../utils/function-id.js";
78
import { resolveScheme } from "../../../../../utils/ipfs.js";
89
import { useChainContext } from "./provider.js";
910

@@ -121,7 +122,18 @@ export function ChainIcon({
121122
}: ChainIconProps) {
122123
const { chain } = useChainContext();
123124
const iconQuery = useQuery({
124-
queryKey: ["_internal_chain_icon_", chain.id] as const,
125+
queryKey: [
126+
"_internal_chain_icon_",
127+
chain.id,
128+
{
129+
resolver:
130+
typeof iconResolver === "string"
131+
? iconResolver
132+
: typeof iconResolver === "function"
133+
? getFunctionId(iconResolver)
134+
: undefined,
135+
},
136+
] as const,
125137
queryFn: async () => {
126138
if (typeof iconResolver === "string") {
127139
return iconResolver;

packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ import { describe, expect, it } from "vitest";
22
import { render, screen, waitFor } from "~test/react-render.js";
33
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
44
import { defineChain } from "../../../../../chains/utils.js";
5-
import { ChainName } from "./name.js";
5+
import { ChainName, fetchChainName } from "./name.js";
66
import { ChainProvider } from "./provider.js";
77

88
describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
9-
it("should return the correct chain name, if the name exists in the chain object", () => {
9+
it("should return the correct chain name, if the name exists in the chain object", async () => {
1010
render(
1111
<ChainProvider chain={ethereum}>
1212
<ChainName />
1313
</ChainProvider>,
1414
);
15-
waitFor(() =>
15+
await waitFor(() =>
1616
expect(
1717
screen.getByText("Ethereum", {
1818
exact: true,
@@ -22,13 +22,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
2222
);
2323
});
2424

25-
it("should return the correct chain name, if the name is loaded from the server", () => {
25+
it("should return the correct chain name, if the name is loaded from the server", async () => {
2626
render(
2727
<ChainProvider chain={defineChain(1)}>
2828
<ChainName />
2929
</ChainProvider>,
3030
);
31-
waitFor(() =>
31+
await waitFor(() =>
3232
expect(
3333
screen.getByText("Ethereum Mainnet", {
3434
exact: true,
@@ -38,13 +38,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
3838
);
3939
});
4040

41-
it("should return the correct FORMATTED chain name", () => {
41+
it("should return the correct FORMATTED chain name", async () => {
4242
render(
4343
<ChainProvider chain={ethereum}>
4444
<ChainName formatFn={(str: string) => `${str}-formatted`} />
4545
</ChainProvider>,
4646
);
47-
waitFor(() =>
47+
await waitFor(() =>
4848
expect(
4949
screen.getByText("Ethereum-formatted", {
5050
exact: true,
@@ -54,14 +54,14 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
5454
);
5555
});
5656

57-
it("should fallback properly when fail to resolve chain name", () => {
57+
it("should fallback properly when fail to resolve chain name", async () => {
5858
render(
5959
<ChainProvider chain={defineChain(-1)}>
6060
<ChainName fallbackComponent={<span>oops</span>} />
6161
</ChainProvider>,
6262
);
6363

64-
waitFor(() =>
64+
await waitFor(() =>
6565
expect(
6666
screen.getByText("oops", {
6767
exact: true,
@@ -70,4 +70,31 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
7070
).toBeInTheDocument(),
7171
);
7272
});
73+
74+
it("fetchChainName should respect nameResolver as a string", async () => {
75+
const res = await fetchChainName({
76+
chain: ethereum,
77+
nameResolver: "eth_mainnet",
78+
});
79+
expect(res).toBe("eth_mainnet");
80+
});
81+
82+
it("fetchChainName should respect nameResolver as a non-async function", async () => {
83+
const res = await fetchChainName({
84+
chain: ethereum,
85+
nameResolver: () => "eth_mainnet",
86+
});
87+
expect(res).toBe("eth_mainnet");
88+
});
89+
90+
it("fetchChainName should respect nameResolver as an async function", async () => {
91+
const res = await fetchChainName({
92+
chain: ethereum,
93+
nameResolver: async () => {
94+
await new Promise((resolve) => setTimeout(resolve, 2000));
95+
return "eth_mainnet";
96+
},
97+
});
98+
expect(res).toBe("eth_mainnet");
99+
});
73100
});

packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type React from "react";
55
import type { JSX } from "react";
6+
import type { Chain } from "../../../../../chains/types.js";
67
import { getChainMetadata } from "../../../../../chains/utils.js";
8+
import { getFunctionId } from "../../../../../utils/function-id.js";
79
import { useChainContext } from "./provider.js";
810

911
/**
@@ -155,19 +157,19 @@ export function ChainName({
155157
}: ChainNameProps) {
156158
const { chain } = useChainContext();
157159
const nameQuery = useQuery({
158-
queryKey: ["_internal_chain_name_", chain.id] as const,
159-
queryFn: async () => {
160-
if (typeof nameResolver === "string") {
161-
return nameResolver;
162-
}
163-
if (typeof nameResolver === "function") {
164-
return nameResolver();
165-
}
166-
if (chain.name) {
167-
return chain.name;
168-
}
169-
return getChainMetadata(chain).then((data) => data.name);
170-
},
160+
queryKey: [
161+
"_internal_chain_name_",
162+
chain.id,
163+
{
164+
resolver:
165+
typeof nameResolver === "string"
166+
? nameResolver
167+
: typeof nameResolver === "function"
168+
? getFunctionId(nameResolver)
169+
: undefined,
170+
},
171+
] as const,
172+
queryFn: async () => fetchChainName({ chain, nameResolver }),
171173
...queryOptions,
172174
});
173175

@@ -183,3 +185,23 @@ export function ChainName({
183185

184186
return <span {...restProps}>{displayValue}</span>;
185187
}
188+
189+
/**
190+
* @internal Exported for tests only
191+
*/
192+
export async function fetchChainName(props: {
193+
chain: Chain;
194+
nameResolver?: string | (() => string) | (() => Promise<string>);
195+
}) {
196+
const { nameResolver, chain } = props;
197+
if (typeof nameResolver === "string") {
198+
return nameResolver;
199+
}
200+
if (typeof nameResolver === "function") {
201+
return nameResolver();
202+
}
203+
if (chain.name) {
204+
return chain.name;
205+
}
206+
return getChainMetadata(chain).then((data) => data.name);
207+
}

packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type { JSX } from "react";
55
import type { ThirdwebContract } from "../../../../../contract/contract.js";
6+
import { getFunctionId } from "../../../../../utils/function-id.js";
67
import { useNFTContext } from "./provider.js";
78
import { getNFTInfo } from "./utils.js";
89

@@ -100,7 +101,7 @@ export function NFTDescription({
100101
typeof descriptionResolver === "string"
101102
? descriptionResolver
102103
: typeof descriptionResolver === "function"
103-
? descriptionResolver.toString()
104+
? getFunctionId(descriptionResolver)
104105
: undefined,
105106
},
106107
],

packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type { JSX } from "react";
55
import type { ThirdwebContract } from "../../../../../contract/contract.js";
6+
import { getFunctionId } from "../../../../../utils/function-id.js";
67
import { MediaRenderer } from "../../MediaRenderer/MediaRenderer.js";
78
import type { MediaRendererProps } from "../../MediaRenderer/types.js";
89
import { useNFTContext } from "./provider.js";
@@ -140,7 +141,7 @@ export function NFTMedia({
140141
typeof mediaResolver === "object"
141142
? mediaResolver
142143
: typeof mediaResolver === "function"
143-
? mediaResolver.toString()
144+
? getFunctionId(mediaResolver)
144145
: undefined,
145146
},
146147
],

packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type { JSX } from "react";
55
import type { ThirdwebContract } from "../../../../../contract/contract.js";
6+
import { getFunctionId } from "../../../../../utils/function-id.js";
67
import { useNFTContext } from "./provider.js";
78
import { getNFTInfo } from "./utils.js";
89

@@ -102,7 +103,7 @@ export function NFTName({
102103
typeof nameResolver === "string"
103104
? nameResolver
104105
: typeof nameResolver === "function"
105-
? nameResolver.toString()
106+
? getFunctionId(nameResolver)
106107
: undefined,
107108
},
108109
],

packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getChainMetadata } from "../../../../../chains/utils.js";
66
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
77
import { getContract } from "../../../../../contract/contract.js";
88
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
9+
import { getFunctionId } from "../../../../../utils/function-id.js";
910
import { resolveScheme } from "../../../../../utils/ipfs.js";
1011
import { useTokenContext } from "./provider.js";
1112

@@ -117,7 +118,19 @@ export function TokenIcon({
117118
}: TokenIconProps) {
118119
const { address, client, chain } = useTokenContext();
119120
const iconQuery = useQuery({
120-
queryKey: ["_internal_token_icon_", chain.id, address] as const,
121+
queryKey: [
122+
"_internal_token_icon_",
123+
chain.id,
124+
address,
125+
{
126+
resolver:
127+
typeof iconResolver === "string"
128+
? iconResolver
129+
: typeof iconResolver === "function"
130+
? getFunctionId(iconResolver)
131+
: undefined,
132+
},
133+
] as const,
121134
queryFn: async () => {
122135
if (typeof iconResolver === "string") {
123136
return iconResolver;

0 commit comments

Comments
 (0)