Skip to content

Commit

Permalink
feat: use react query for fetch hooks fixes #127 (#206)
Browse files Browse the repository at this point in the history
Co-authored-by: Leonardo Zizzamia <[email protected]>
  • Loading branch information
Yuripetusko and Zizzamia authored Mar 6, 2024
1 parent 6b7885c commit 4090f4f
Show file tree
Hide file tree
Showing 20 changed files with 313 additions and 236 deletions.
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

0 comments on commit 4090f4f

Please sign in to comment.