Skip to content

Commit ca8b14e

Browse files
authored
Merge pull request #99 from aave/fix/read-hooks-error
fix: error handling in read hooks
2 parents 1150470 + 252afdb commit ca8b14e

File tree

15 files changed

+598
-28
lines changed

15 files changed

+598
-28
lines changed

.changeset/funny-waves-argue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@aave/react": patch
3+
---
4+
5+
**fix:** error handling in read hooks with suspense enabled

.github/workflows/release.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
permissions:
2+
contents: read
3+
pull-requests: write
4+
name: Release
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
11+
concurrency: ${{ github.workflow }}-${{ github.ref }}
12+
13+
jobs:
14+
release:
15+
name: Release
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout Repo
19+
uses: actions/checkout@v4
20+
21+
- name: Setup
22+
uses: ./.github/actions/setup
23+
24+
- name: Create Release Pull Request
25+
uses: changesets/action@v1
26+
with:
27+
branchName: 'release/packages'
28+
title: 'chore: release packages'
29+
commit: 'chore: bump package versions'
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

AGENTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# AGENTS.md
2+
3+
## Dev environment tips
4+
5+
- Use `nvm use` to use the correct Node.js version.
6+
- Use `corepack enable` to install the correct version of pnpm.
7+
- Use `pnpm install` to install the dependencies.
8+
- Use `pnpm build` to build the project.
9+
10+
## Testing instructions
11+
12+
- Use `pnpm test:client --run` to run `@aave/client` tests.
13+
- Use `pnpm test:react --run` to run `@aave/react` tests.
14+
- Use `pnpm vitest --run --project <project-name> <path-to-test-file> -t "<test-name>"` to focus on one single test.

packages/react/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ function MarketData() {
5050
}
5151

5252
function UserPositions() {
53-
const { data: supplies } = useUserSupplies({
53+
const { data: supplies, loading } = useUserSupplies({
5454
markets: [evmAddress('0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2')],
5555
user: evmAddress('0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234'),
5656
});
57+
58+
if (loading) return <div>Loading...</div>;
5759

5860
return <div>Supplies: {supplies?.length || 0}</div>;
5961
}

packages/react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@types/react": "^19.1.8",
7070
"ethers": "^6.14.4",
7171
"happy-dom": "^18.0.1",
72+
"msw": "^2.10.5",
7273
"react": "^19.1.0",
7374
"react-dom": "^19.1.0",
7475
"thirdweb": "^5.105.25",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { UnexpectedError } from '@aave/client';
2+
import { HealthQuery } from '@aave/graphql';
3+
import { graphql, HttpResponse } from 'msw';
4+
import { setupServer } from 'msw/node';
5+
import {
6+
afterAll,
7+
afterEach,
8+
beforeAll,
9+
describe,
10+
expect,
11+
it,
12+
vi,
13+
} from 'vitest';
14+
import { renderHookWithinContext } from '../test-utils';
15+
import { useSuspendableQuery } from './reads';
16+
17+
const server = setupServer(
18+
graphql.query(HealthQuery, () => {
19+
return HttpResponse.json({
20+
data: {
21+
value: true,
22+
},
23+
});
24+
}),
25+
);
26+
27+
describe(`Given the '${useSuspendableQuery.name}' hook`, () => {
28+
beforeAll(() => {
29+
server.listen();
30+
});
31+
32+
afterEach(() => {
33+
server.resetHandlers();
34+
});
35+
36+
afterAll(() => {
37+
server.close();
38+
});
39+
40+
describe('When rendering with suspense disabled', () => {
41+
it('Then it should return data after a loading state', async () => {
42+
const { result } = renderHookWithinContext(() =>
43+
useSuspendableQuery({
44+
document: HealthQuery,
45+
variables: {},
46+
suspense: false,
47+
}),
48+
);
49+
50+
await vi.waitUntil(() => !result.current.loading);
51+
52+
expect(result.current.loading).toBe(false);
53+
expect(result.current.data).toBe(true);
54+
expect(result.current.error).toBeUndefined();
55+
});
56+
57+
it('Then it should return any error as component state', async () => {
58+
server.use(
59+
graphql.query(HealthQuery, () => {
60+
return HttpResponse.json({
61+
errors: [
62+
{ message: 'Test error', extensions: { code: 'TEST_ERROR' } },
63+
],
64+
});
65+
}),
66+
);
67+
68+
const { result } = renderHookWithinContext(() =>
69+
useSuspendableQuery({
70+
document: HealthQuery,
71+
variables: {},
72+
suspense: false,
73+
}),
74+
);
75+
76+
await vi.waitUntil(() => !result.current.loading);
77+
78+
expect(result.current.loading).toBe(false);
79+
expect(result.current.data).toBeUndefined();
80+
expect(result.current.error).toBeDefined();
81+
});
82+
});
83+
84+
describe('When rendering with suspense enabled', () => {
85+
it('Then it should suspend and render once the query is resolved', async () => {
86+
const { result } = renderHookWithinContext(() =>
87+
useSuspendableQuery({
88+
document: HealthQuery,
89+
variables: {},
90+
suspense: true,
91+
}),
92+
);
93+
94+
await vi.waitUntil(() => result.current);
95+
96+
expect(result.current.data).toBe(true);
97+
});
98+
99+
it('Then it should throw any error so that can be captured via an Error Boundary', async () => {
100+
server.use(
101+
graphql.query(HealthQuery, () => {
102+
return HttpResponse.json({
103+
errors: [
104+
{ message: 'Test error', extensions: { code: 'TEST_ERROR' } },
105+
],
106+
});
107+
}),
108+
);
109+
110+
const onError = vi.fn();
111+
renderHookWithinContext(
112+
() =>
113+
useSuspendableQuery({
114+
document: HealthQuery,
115+
variables: {},
116+
suspense: true,
117+
}),
118+
// biome-ignore lint/suspicious/noExplicitAny: not worth the effort
119+
{ onCaughtError: onError as any },
120+
);
121+
122+
// Wait for the error boundary to catch the error
123+
await vi.waitFor(() => expect(onError).toHaveBeenCalled());
124+
125+
expect(onError).toHaveBeenCalledWith(
126+
expect.any(UnexpectedError),
127+
expect.any(Object),
128+
);
129+
});
130+
});
131+
});

packages/react/src/helpers/reads.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import { UnexpectedError } from '@aave/client';
12
import type { AnyVariables, StandardData } from '@aave/graphql';
23
import { invariant } from '@aave/types';
34
import { useMemo } from 'react';
45
import { type TypedDocumentNode, useQuery } from 'urql';
5-
import { ReadResult, type SuspendableResult } from './results';
6+
import {
7+
ReadResult,
8+
type SuspendableResult,
9+
type SuspenseResult,
10+
} from './results';
611

712
/**
813
* @internal
@@ -12,15 +17,40 @@ export type Suspendable = { suspense: true };
1217
/**
1318
* @internal
1419
*/
15-
export type UseSuspendableQueryArgs<Value, Variables extends AnyVariables> = {
20+
export type UseSuspendableQueryArgs<
21+
Value,
22+
Variables extends AnyVariables,
23+
Suspense extends boolean = boolean,
24+
> = {
1625
document: TypedDocumentNode<StandardData<Value>, Variables>;
1726
variables: Variables;
18-
suspense: boolean;
27+
suspense: Suspense;
1928
};
2029

2130
/**
2231
* @internal
2332
*/
33+
export function useSuspendableQuery<Value, Variables extends AnyVariables>({
34+
document,
35+
variables,
36+
suspense,
37+
}: UseSuspendableQueryArgs<Value, Variables, false>): ReadResult<Value>;
38+
/**
39+
* @internal
40+
*/
41+
export function useSuspendableQuery<Value, Variables extends AnyVariables>({
42+
document,
43+
variables,
44+
suspense,
45+
}: UseSuspendableQueryArgs<Value, Variables, true>): SuspenseResult<Value>;
46+
/**
47+
* @internal
48+
*/
49+
export function useSuspendableQuery<Value, Variables extends AnyVariables>({
50+
document,
51+
variables,
52+
suspense,
53+
}: UseSuspendableQueryArgs<Value, Variables>): SuspendableResult<Value>;
2454
export function useSuspendableQuery<Value, Variables extends AnyVariables>({
2555
document,
2656
variables,
@@ -37,8 +67,12 @@ export function useSuspendableQuery<Value, Variables extends AnyVariables>({
3767
}
3868

3969
if (error) {
40-
// biome-ignore lint/suspicious/noExplicitAny: temporary workaround
41-
return ReadResult.Failure(error) as any;
70+
const unexpected = UnexpectedError.from(error);
71+
if (suspense) {
72+
throw unexpected;
73+
}
74+
75+
return ReadResult.Failure(unexpected);
4276
}
4377

4478
invariant(data, 'No data returned');

packages/react/src/helpers/results.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { UnexpectedError } from '@aave/client';
2+
13
/**
24
* A read hook result.
35
*
@@ -48,4 +50,6 @@ export const ReadResult = {
4850
*/
4951
export type SuspenseResult<T> = { data: T };
5052

51-
export type SuspendableResult<T> = ReadResult<T> | SuspenseResult<T>;
53+
export type SuspendableResult<T, E extends UnexpectedError = UnexpectedError> =
54+
| ReadResult<T, E>
55+
| SuspenseResult<T>;

packages/react/src/markets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function useAaveMarket(
3232
* Fetch a single Aave Market.
3333
*
3434
* ```tsx
35-
* const { data, loading } = useAaveMarket({
35+
* const { data, error, loading } = useAaveMarket({
3636
* address: evmAddress('0x8787…'),
3737
* chainId: chainId(1),
3838
* });
@@ -84,7 +84,7 @@ export function useAaveMarkets(
8484
* Fetch all Aave Markets for the specified chains.
8585
*
8686
* ```tsx
87-
* const { data, loading } = useAaveMarkets({
87+
* const { data, error, loading } = useAaveMarkets({
8888
* chainIds: [chainId(1), chainId(8453)],
8989
* user: evmAddress('0x742d35cc...'),
9090
* });

packages/react/src/misc.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function useAaveChains(
4545
* Fetch all supported Aave chains.
4646
*
4747
* ```tsx
48-
* const { data, loading } = useAaveChains({
48+
* const { data, error, loading } = useAaveChains({
4949
* filter: ChainsFilter.MAINNET_ONLY,
5050
* });
5151
* ```
@@ -84,7 +84,7 @@ export function useAaveHealth(args: Suspendable): SuspenseResult<boolean>;
8484
* Health check query.
8585
*
8686
* ```tsx
87-
* const { data, loading } = useAaveHealth();
87+
* const { data, error, loading } = useAaveHealth();
8888
* ```
8989
*/
9090
export function useAaveHealth(): ReadResult<boolean>;
@@ -125,7 +125,7 @@ export function useUsdExchangeRates(
125125
* Fetch USD exchange rates for different tokens on a given market.
126126
*
127127
* ```tsx
128-
* const { data, loading } = useUsdExchangeRates({
128+
* const { data, error, loading } = useUsdExchangeRates({
129129
* market: evmAddress('0x1234…'),
130130
* underlyingTokens: [evmAddress('0x5678…'), evmAddress('0x90ab…')],
131131
* chainId: chainId(1),

0 commit comments

Comments
 (0)