Skip to content

Commit 9f4085e

Browse files
committed
feat: first iteration on @aave/react package
1 parent 467e502 commit 9f4085e

18 files changed

+1328
-3
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"lint:fix": "biome check --write",
2020
"new:package": "NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts",
2121
"prepublish": "pnpm run build",
22+
"test:react": "vitest --project react",
2223
"test": "vitest --passWithNoTests"
2324
},
2425
"license": "MIT",

packages/react/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# `@aave/react`
2+
3+
The official React bindings for the Aave Protocol.
4+
5+
---
6+
7+

packages/react/package.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"name": "@aave/react",
3+
"version": "0.0.0",
4+
"description": "The official React bindings for the Aave Protocol",
5+
"repository": {
6+
"directory": "packages/react",
7+
"type": "git",
8+
"url": "git://github.com/aave/aave-sdk.git"
9+
},
10+
"type": "module",
11+
"main": "dist/index.cjs",
12+
"module": "dist/index.js",
13+
"types": "dist/index.d.ts",
14+
"exports": {
15+
".": {
16+
"import": "./dist/index.js",
17+
"require": "./dist/index.cjs"
18+
}
19+
},
20+
"typesVersions": {
21+
"*": {
22+
"import": [
23+
"./dist/index.d.ts"
24+
],
25+
"require": [
26+
"./dist/index.d.cts"
27+
]
28+
}
29+
},
30+
"files": [
31+
"dist"
32+
],
33+
"sideEffects": false,
34+
"scripts": {
35+
"build": "tsup"
36+
},
37+
"dependencies": {
38+
"@aave/client": "workspace:*",
39+
"@aave/env": "workspace:*",
40+
"@aave/graphql": "workspace:*",
41+
"@aave/types": "workspace:*",
42+
"urql": "^4.2.2"
43+
},
44+
"devDependencies": {
45+
"@testing-library/dom": "^10.4.0",
46+
"@testing-library/react": "^16.3.0",
47+
"@types/react": "^19.1.8",
48+
"ethers": "^6.14.4",
49+
"happy-dom": "^18.0.1",
50+
"react": "^19.1.0",
51+
"react-dom": "^19.1.0",
52+
"tsup": "^8.5.0",
53+
"typescript": "^5.6.3",
54+
"viem": "^2.31.6",
55+
"zksync-ethers": "^6.18.0"
56+
},
57+
"peerDependencies": {
58+
"@types/react": "^19.1.8",
59+
"ethers": "^6.14.4",
60+
"react": "^19.1.0",
61+
"viem": "^2.31.6",
62+
"zksync-ethers": "^6.18.0"
63+
},
64+
"peerDependenciesMeta": {
65+
"@types/react": {
66+
"optional": true
67+
},
68+
"ethers": {
69+
"optional": true
70+
},
71+
"viem": {
72+
"optional": true
73+
},
74+
"zksync-ethers": {
75+
"optional": true
76+
}
77+
},
78+
"license": "MIT"
79+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { AaveClient } from '@aave/client';
2+
// biome-ignore lint/correctness/noUnusedImports: intentional
3+
import React, { type ReactNode } from 'react';
4+
5+
import { AaveContextProvider } from './context';
6+
7+
/**
8+
* <AaveProvider> props
9+
*/
10+
export type AaveProviderProps = {
11+
/**
12+
* The children to render
13+
*/
14+
children: ReactNode;
15+
/**
16+
* The Aave client instance to use
17+
*/
18+
client: AaveClient;
19+
};
20+
21+
/**
22+
* Manages the internal state of the Aave SDK.
23+
*
24+
* ```tsx
25+
* import { AaveProvider, AaveClient, mainnet } from '@aave/react';
26+
*
27+
* const client = AaveClient.create({
28+
* environment: mainnet,
29+
* });
30+
*
31+
* function App() {
32+
* return (
33+
* <AaveProvider client={client}>
34+
* // ...
35+
* </AaveProvider>
36+
* );
37+
* }
38+
* ```
39+
*/
40+
export function AaveProvider({ children, client }: AaveProviderProps) {
41+
return <AaveContextProvider client={client}>{children}</AaveContextProvider>;
42+
}

packages/react/src/context.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { AaveClient } from '@aave/client';
2+
import { invariant } from '@aave/types';
3+
import React, { type ReactNode, useContext } from 'react';
4+
import { Provider as UrqlProvider } from 'urql';
5+
6+
const AaveContext = React.createContext<AaveClient | null>(null);
7+
8+
/**
9+
* @internal
10+
*/
11+
export type AaveContextProviderProps = {
12+
children: ReactNode;
13+
client: AaveClient;
14+
};
15+
16+
/**
17+
* @internal
18+
*/
19+
export function AaveContextProvider({
20+
children,
21+
client,
22+
}: AaveContextProviderProps) {
23+
return (
24+
<AaveContext.Provider value={client}>
25+
<UrqlProvider value={client.urql}>{children}</UrqlProvider>
26+
</AaveContext.Provider>
27+
);
28+
}
29+
30+
/**
31+
* Retrieve the injected {@link AaveClient} from the context.
32+
*/
33+
export function useAaveClient(): AaveClient {
34+
const client = useContext(AaveContext);
35+
36+
invariant(
37+
client,
38+
'Could not find Aave SDK context, ensure your code is wrapped in a <AaveProvider>',
39+
);
40+
41+
return client;
42+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './reads';
2+
export * from './results';
3+
export * from './tasks';
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { AnyVariables, StandardData } from '@aave/graphql';
2+
import { invariant } from '@aave/types';
3+
import { useMemo } from 'react';
4+
import { type TypedDocumentNode, useQuery } from 'urql';
5+
import { ReadResult, type SuspendableResult } from './results';
6+
7+
/**
8+
* @internal
9+
*/
10+
export type Suspendable = { suspense: true };
11+
12+
/**
13+
* @internal
14+
*/
15+
export type UseSuspendableQueryArgs<Value, Variables extends AnyVariables> = {
16+
document: TypedDocumentNode<StandardData<Value>, Variables>;
17+
variables: Variables;
18+
suspense: boolean;
19+
};
20+
21+
/**
22+
* @internal
23+
*/
24+
export function useSuspendableQuery<Value, Variables extends AnyVariables>({
25+
document,
26+
variables,
27+
suspense,
28+
}: UseSuspendableQueryArgs<Value, Variables>): SuspendableResult<Value> {
29+
const [{ data, fetching, error }] = useQuery({
30+
query: document,
31+
variables,
32+
context: useMemo(() => ({ suspense }), [suspense]),
33+
});
34+
35+
if (fetching) {
36+
return ReadResult.Initial();
37+
}
38+
39+
if (error) {
40+
// biome-ignore lint/suspicious/noExplicitAny: temporary workaround
41+
return ReadResult.Failure(error) as any;
42+
}
43+
44+
invariant(data, 'No data returned');
45+
46+
return ReadResult.Success(data.value);
47+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* A read hook result.
3+
*
4+
* It's a discriminated union of the possible results of a read operation:
5+
* - Rely on the `loading` value to determine if the `data` or `error` can be evaluated.
6+
* - If `error` is `undefined`, then `data` value will be available.
7+
*/
8+
export type ReadResult<T, E = never> =
9+
| {
10+
data: undefined;
11+
error: undefined;
12+
loading: true;
13+
}
14+
| {
15+
data: T;
16+
error: undefined;
17+
loading: false;
18+
}
19+
| {
20+
data: undefined;
21+
error: E;
22+
loading: false;
23+
};
24+
25+
/**
26+
* @internal
27+
*/
28+
export const ReadResult = {
29+
Initial: <T, E = never>(): ReadResult<T, E> => ({
30+
data: undefined,
31+
error: undefined,
32+
loading: true,
33+
}),
34+
Success: <T, E = never>(data: T): ReadResult<T, E> => ({
35+
data,
36+
error: undefined,
37+
loading: false,
38+
}),
39+
Failure: <T, E = never>(error: E): ReadResult<T, E> => ({
40+
data: undefined,
41+
error,
42+
loading: false,
43+
}),
44+
};
45+
46+
/**
47+
* A read hook result that supports React Suspense
48+
*/
49+
export type SuspenseResult<T> = { data: T };
50+
51+
export type SuspendableResult<T> = ReadResult<T> | SuspenseResult<T>;

0 commit comments

Comments
 (0)