Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use react query for fetch hooks fixes #127 #206

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
59 changes: 59 additions & 0 deletions .changeset/gold-impalas-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
'@coinbase/onchainkit': minor
---

**feat**: Replace internal `useOnchainActionWithCache` with `tanstack/react-query`. This affects `useName` and `useAvatar` hooks. The return type and the input parameters also changed for these 2 hooks.

BREAKING CHANGES

The input parameters as well as return types of `useName` and `useAvatar` hooks have changed. The return type of `useName` and `useAvatar` hooks changed.

### `useName`

Before

```tsx
import { useName } from '@coinbase/onchainkit/identity';

const { ensName, isLoading } = useName('0x1234');
```

After

```tsx
import { useName } from '@coinbase/onchainkit/identity';

// Return type signature is following @tanstack/react-query useQuery hook signature
const {
data: name,
isLoading,
isError,
error,
status,
} = useName({ address: '0x1234' }, { enabled: true, cacheTime: 1000 * 60 * 60 * 24 });
```

### `useAvatar`

Before

```tsx
import { useAvatar } from '@coinbase/onchainkit/identity';

const { ensAvatar, isLoading } = useAvatar('vitalik.eth');
```

After

```tsx
import { useAvatar } from '@coinbase/onchainkit/identity';

// Return type signature is following @tanstack/react-query useQuery hook signature
const {
data: avatar,
isLoading,
isError,
error,
status,
} = useAvatar({ ensName: 'vitalik.eth' }, { enabled: true, cacheTime: 1000 * 60 * 60 * 24 });
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"release:version": "changeset version && yarn install --immutable"
},
"peerDependencies": {
"@tanstack/react-query": "^5",
"@xmtp/frames-validator": "^0.5.0",
"graphql": "^14",
"graphql-request": "^6",
Expand All @@ -27,6 +28,7 @@
"devDependencies": {
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
"@tanstack/react-query": "^5.24.1",
"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^14.2.0",
"@types/jest": "^29.5.12",
Expand Down
27 changes: 27 additions & 0 deletions site/docs/pages/identity/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ OnchainKit provides TypeScript utilities and React components to help you build
- Components:
- [`<Avatar />`](/identity/avatar): A component to display an ENS avatar.
- [`<Name />`](/identity/name): A component to display an ENS name.
- Hooks:
- [`useName`](/identity/use-name): A hook to get an onchain name for a given address. (ENS only for now)
- [`useAvatar`](/identity/use-avatar): A hook to get avatar image src. (ENS only for now)
- Utilities:
- [`getEASAttestations`](/identity/get-eas-attestations): A function to fetche EAS attestations.

Expand All @@ -30,3 +33,27 @@ pnpm add @coinbase/onchainkit react@18 react-dom@18 graphql@14 graphql-request@6
```

:::


## Components

If you are using any of the provided components, you will need to install and configure `@tanstack/react-query` and wrap your app in `<QueryClientProvider>`.

```tsx
import { Avatar } from '@coinbase/onchainkit/identity';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// Create a client
const queryClient = new QueryClient();

function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Avatar address="0x1234567890abcdef1234567890abcdef12345678" />
</QueryClientProvider>
);
}
```

See [Tanstack Query documentation](https://tanstack.com/query/v5/docs/framework/react/quick-start) for more info.
35 changes: 35 additions & 0 deletions site/docs/pages/identity/use-avatar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `useAvatar`

The `useAvatar` hook is used to get avatar image url from an onchain identity provider for a given name.

Supported providers:

- ENS

## Usage

```tsx
import { useAvatar } from '@coinbase/onchainkit/identity';

// Return type signature is following @tanstack/react-query useQuery hook signature
const {
data: avatar,
isLoading,
isError,
error,
status,
} = useAvatar({ ensName: 'vitalik.eth' }, { enabled: true, cacheTime: 1000 * 60 * 60 * 24 });
```

## Props

```ts
type UseAvatarOptions = {
ensName: string;
};

type UseAvatarQueryOptions = {
enabled?: boolean;
cacheTime?: number;
};
```
35 changes: 35 additions & 0 deletions site/docs/pages/identity/use-name.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# `useName`

The `useName` hook is used to get name from an onchain identity provider for a given address.

Supported providers:

- ENS

## Usage

```tsx
import { useName } from '@coinbase/onchainkit/identity';

// Return type signature is following @tanstack/react-query useQuery hook signature
const {
data: name,
isLoading,
isError,
error,
status,
} = useName({ address: '0x1234' }, { enabled: true, cacheTime: 1000 * 60 * 60 * 24 });
```

## Props

```ts
type UseNameOptions = {
address: `0x${string}`;
};

type UseNameQueryOptions = {
enabled?: boolean;
cacheTime?: number;
};
```
4 changes: 2 additions & 2 deletions site/docs/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { HomePage } from 'vocs/components';
<div className="space-y-8 max-w-[380px] flex flex-col items-start max-md:items-center">
<h1 className="text-center text-6xl font-medium no-underline">OnchainKit</h1>
<div className="font-regular text-[21px] max-sm:text-[18px] text-[#919193] max-md:text-center">
React <span className="text-black dark:text-white">components</span>
and TypeScript <span className="text-black dark:text-white">utilities</span>
React <span className="text-black dark:text-white">components</span>
and TypeScript <span className="text-black dark:text-white">utilities</span>
for top-tier onchain apps.
</div>
<div className="flex justify-center space-x-2">
Expand Down
13 changes: 13 additions & 0 deletions site/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ export const sidebar = [
},
],
},
{
text: 'React Hooks',
items: [
{
text: 'useName',
link: '/identity/use-name',
},
{
text: 'useAvatar',
link: '/identity/use-avatar',
},
],
},
{
text: 'Utilities',
items: [
Expand Down
20 changes: 10 additions & 10 deletions src/identity/components/Avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ describe('Avatar Component', () => {
});

it('should display loading indicator when loading', async () => {
(useAvatar as jest.Mock).mockReturnValue({ ensAvatar: null, isLoading: true });
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: true });
(useAvatar as jest.Mock).mockReturnValue({ data: null, isLoading: true });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: true });

render(<Avatar address="0x123" />);

Expand All @@ -35,8 +35,8 @@ describe('Avatar Component', () => {
});

it('should display default avatar when no ENS name or avatar is available', async () => {
(useAvatar as jest.Mock).mockReturnValue({ ensAvatar: null, isLoading: false });
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: false });
(useAvatar as jest.Mock).mockReturnValue({ data: null, isLoading: false });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: false });

render(<Avatar address="0x123" />);

Expand All @@ -47,8 +47,8 @@ describe('Avatar Component', () => {
});

it('should display ENS avatar when available', async () => {
(useAvatar as jest.Mock).mockReturnValue({ ensAvatar: 'avatar_url', isLoading: false });
(useName as jest.Mock).mockReturnValue({ ensName: 'ens_name', isLoading: false });
(useAvatar as jest.Mock).mockReturnValue({ data: 'avatar_url', isLoading: false });
(useName as jest.Mock).mockReturnValue({ data: 'ens_name', isLoading: false });

render(<Avatar address="0x123" className="custom-class" />);

Expand All @@ -61,8 +61,8 @@ describe('Avatar Component', () => {
});

it('renders custom loading component when provided', () => {
(useAvatar as jest.Mock).mockReturnValue({ ensAvatar: null, isLoading: true });
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: true });
(useAvatar as jest.Mock).mockReturnValue({ data: null, isLoading: true });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: true });

const CustomLoadingComponent = <div data-testid="custom-loading">Loading...</div>;

Expand All @@ -74,8 +74,8 @@ describe('Avatar Component', () => {
});

it('renders custom default component when no ENS name or avatar is available', () => {
(useAvatar as jest.Mock).mockReturnValue({ ensAvatar: null, isLoading: false });
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: false });
(useAvatar as jest.Mock).mockReturnValue({ data: null, isLoading: false });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: false });

const CustomDefaultComponent = <div data-testid="custom-default">Default Avatar</div>;

Expand Down
13 changes: 8 additions & 5 deletions src/identity/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ export function Avatar({
defaultComponent,
props,
}: AvatarProps) {
const { ensName, isLoading: isLoadingName } = useName(address);
const { ensAvatar, isLoading: isLoadingAvatar } = useAvatar(ensName as string);
const { data: name, isLoading: isLoadingName } = useName({ address });
const { data: avatar, isLoading: isLoadingAvatar } = useAvatar(
{ ensName: name ?? '' },
{ enabled: !!name },
);

if (isLoadingName || isLoadingAvatar) {
return (
Expand Down Expand Up @@ -66,7 +69,7 @@ export function Avatar({
);
}

if (!ensName || !ensAvatar) {
if (!name || !avatar) {
return (
defaultComponent || (
<svg
Expand All @@ -88,8 +91,8 @@ export function Avatar({
width="32"
height="32"
decoding="async"
src={ensAvatar}
alt={ensName}
src={avatar}
alt={name}
{...props}
/>
);
Expand Down
8 changes: 4 additions & 4 deletions src/identity/components/Name.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('OnchainAddress', () => {
});

it('displays ENS name when available', () => {
(useName as jest.Mock).mockReturnValue({ ensName: testName, isLoading: false });
(useName as jest.Mock).mockReturnValue({ data: testName, isLoading: false });

render(<Name address={testAddress} />);

Expand All @@ -38,15 +38,15 @@ describe('OnchainAddress', () => {
});

it('displays sliced address when ENS name is not available and sliced is true as default', () => {
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: false });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: false });

render(<Name address={testAddress} />);

expect(screen.getByText(mockSliceAddress(testAddress))).toBeInTheDocument();
});

it('displays empty when ens still fetching', () => {
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: true });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: true });

render(<Name address={testAddress} />);

Expand All @@ -55,7 +55,7 @@ describe('OnchainAddress', () => {
});

it('displays full address when ENS name is not available and sliced is false', () => {
(useName as jest.Mock).mockReturnValue({ ensName: null, isLoading: false });
(useName as jest.Mock).mockReturnValue({ data: null, isLoading: false });

render(<Name address={testAddress} sliced={false} />);

Expand Down
6 changes: 3 additions & 3 deletions src/identity/components/Name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ type NameProps = {
* @param {React.HTMLAttributes<HTMLSpanElement>} [props] - Additional HTML attributes for the span element.
*/
export function Name({ address, className, sliced = true, props }: NameProps) {
const { ensName, isLoading } = useName(address);
const { data: name, isLoading } = useName({ address });

// wrapped in useMemo to prevent unnecessary recalculations.
const normalizedAddress = useMemo(() => {
if (!ensName && !isLoading && sliced) {
if (!name && !isLoading && sliced) {
return getSlicedAddress(address);
}
return address;
Expand All @@ -37,7 +37,7 @@ export function Name({ address, className, sliced = true, props }: NameProps) {

return (
<span className={className} {...props}>
{ensName ?? normalizedAddress}
{name ?? normalizedAddress}
</span>
);
}
Loading
Loading