@liberfi.io/react
v0.1.32
Published
React integration layer for Liberfi SDK
Downloads
3,175
Readme
@liberfi.io/react
React integration layer for the Liberfi SDK. This package provides a DexClientProvider for dependency injection, query hooks (powered by TanStack Query) for data fetching, and subscription hooks for real-time WebSocket updates. It is the primary package consumed by Liberfi UI applications and widgets.
Design Philosophy
- Provider-based DI —
DexClientProviderinjectsIClientandISubscribeClientinstances via React context, keeping hooks decoupled from concrete implementations. - Factory-generated hooks —
createQueryHookandcreateInfiniteQueryHookgenerate hooks from a declarative config, ensuring consistent API surface (each hook exportsuse*Query,*QueryKey, andfetch*). - No built-in side effects — Hooks do not trigger toasts, modals, or analytics. Errors are surfaced via TanStack Query state or
onErrorcallbacks, giving the caller full control. - Query key namespacing — All keys are prefixed with a configurable namespace (default
"liberfi") to avoid collisions.
Installation
pnpm add @liberfi.io/reactPeer Dependencies
The consumer must provide:
| Package | Version |
| ----------------------- | ------- |
| react | >= 18 |
| react-dom | >= 18 |
| @tanstack/react-query | ^5.90.2 |
API Reference
Components
DexClientProvider
Provides the Dex client instances and configuration to all descendant hooks.
| Prop | Type | Default | Description |
| ----------------- | ---------------------- | ----------- | --------------------------------------- |
| client | API.IClient | (required) | REST/HTTP client instance. |
| subscribeClient | API.ISubscribeClient | (required) | WebSocket subscription client instance. |
| queryKeyPrefix | string | "liberfi" | Prefix for all TanStack Query keys. |
| children | ReactNode | (required) | Child components. |
Hooks — Core
useDexClient
Returns the DexClientContextValue from the nearest DexClientProvider.
const { client, subscribeClient, queryKeyPrefix } = useDexClient();Throws if called outside a DexClientProvider.
Hooks — Query (27 hooks)
Each query hook is generated by createQueryHook and exports three items:
use*Query(params, options?)— React hook returningUseQueryResult<TData, Error>.*QueryKey(params, prefix?)— Builds the full query key array for cache operations.fetch*(client, params)— Standalone fetch function for use outside React.
| Hook | Params | Return Data |
| ------------------------------------- | ----------------------------------------------------------- | ------------------------- |
| useTokenQuery | { chain, address } | Token |
| useTokensQuery | { chain, addresses } | Token[] |
| useSearchTokensQuery | SearchTokensOptions | SearchTokenCursorList |
| useNewTokensQuery | { chain, ...GetTokenListOptions } | Token[] |
| useTrendingTokensQuery | { chain, resolution, ...GetTokenListOptions } | Token[] |
| useMigratedTokensQuery | { chain, ...GetTokenListOptions } | Token[] |
| useFinalStretchTokensQuery | { chain, ...GetTokenListOptions } | Token[] |
| useStockTokensQuery | { chain, ...GetTokenListOptions } | Token[] |
| useSwapRouteQuery | SwapParams | SwapRoute |
| useTokenCandlesQuery | { chain, address, resolution, ...GetTokenCandlesOptions } | TokenCandle[] |
| useTokenMarketDataQuery | { chain, address } | TokenMarketData |
| useTokenSecurityQuery | { chain, address } | TokenSecurity |
| useTokenStatsQuery | { chain, address } | TokenStats |
| useTokenHoldersQuery | { chain, address, ...CursorListOptions } | CursorList<TokenHolder> |
| useTokenTradesQuery | { chain, address, ...GetTradesOptions } | CursorList<Trade> |
| useTokenActivitiesQuery | { chain, address, ...GetActivitiesOptions } | CursorList<Activity> |
| useWalletPnlQuery | { chain, address, resolution? } | WalletPnl |
| useWalletPortfoliosQuery | { chain, address, ...paginationOptions } | WalletPortfolios |
| useWalletPortfoliosByTokensQuery | { chain, address, tokenAddresses } | Portfolio[] |
| useWalletPortfolioPnlsQuery | { chain, address, ...paginationOptions } | WalletPortfolioPnls |
| useWalletPortfolioPnlsByTokensQuery | { chain, address, tokenAddresses } | PortfolioPnl[] |
| useWalletTradesQuery | { chain, address, ...GetTradesOptions } | CursorList<Trade> |
| useWalletActivitiesQuery | { chain, address, ...GetActivitiesOptions } | CursorList<Activity> |
| useTxSuccessQuery | { chain, txHash, timeout? } | boolean |
| usePresignedUploadUrlQuery | (none) | string |
Special hooks
| Hook | Type | Description |
| ------------------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
| useSendTxMutation | Mutation | Sends a signed transaction. Returns UseMutationResult<SendTxResult, Error, SendTxParams>. |
| useWalletPortfoliosInfiniteQuery | Infinite Query | Cursor-paginated wallet holdings with fetchNextPage(). |
| useWalletPortfolioPnlsInfiniteQuery | Infinite Query | Cursor-paginated wallet PnLs with fetchNextPage(). |
Hooks — Subscription (14 hooks)
Each subscription hook manages WebSocket lifecycle automatically (subscribe on mount, unsubscribe on unmount or deps change).
Signature: use*Subscription(params, callback, options?)
| Hook | Params | Callback Data |
| ------------------------------------ | -------------------------------- | -------------------------- |
| useTokenSubscription | { chain, address } | TokenSubscribed[] |
| useTokenCandlesSubscription | { chain, address, resolution } | TokenCandle[] |
| useTokenTradesSubscription | { chain, address } | Trade[] |
| useTokenActivitiesSubscription | { chain, address } | Activity[] |
| useWalletPnlSubscription | { chain, address } | WalletPnlSubscribed[] |
| useWalletPortfoliosSubscription | { chain, address } | PortfolioSubscribed[] |
| useWalletPortfolioPnlsSubscription | { chain, address } | PortfolioPnlSubscribed[] |
| useWalletTradesSubscription | { chain, address } | Trade[] |
| useWalletActivitiesSubscription | { chain, address } | Activity[] |
| useNewTokensSubscription | { chain } | TokenSubscribed[] |
| useTrendingTokensSubscription | { chain } | TokenSubscribed[] |
| useMigratedTokensSubscription | { chain } | TokenSubscribed[] |
| useFinalStretchTokensSubscription | { chain } | TokenSubscribed[] |
| useStockTokensSubscription | { chain } | TokenSubscribed[] |
Types
| Name | Definition | Description |
| ------------------------- | ----------------------------------------------------------------- | ----------------------------------------- |
| DexClientProviderProps | PropsWithChildren<{ client, subscribeClient, queryKeyPrefix? }> | Props for DexClientProvider. |
| DexClientContextValue | { client, subscribeClient, queryKeyPrefix } | Shape of the context value. |
| SubscriptionOptions | { enabled?: boolean; onError?: (error: Error) => void } | Options for subscription hooks. |
| QueryHookConfig | { name, queryKey, fetch, defaultOptions? } | Config for createQueryHook. |
| QueryHookReturn | { queryKey, fetch, useQuery } | Return type of createQueryHook. |
| InfiniteQueryHookConfig | { name, queryKey, fetch } | Config for createInfiniteQueryHook. |
| InfiniteQueryHookReturn | { queryKey, useInfiniteQuery } | Return type of createInfiniteQueryHook. |
Constants
| Name | Value | Description |
| -------------------------- | ----------- | ---------------------------------- |
| DEFAULT_QUERY_KEY_PREFIX | "liberfi" | Default prefix for all query keys. |
Factory Functions
createQueryHook<TParams, TData>(config): QueryHookReturn<TParams, TData>
Creates a useQuery-based hook, its queryKey builder, and a standalone fetch function from a single declarative config.
| Config Field | Type | Description |
| ---------------- | ---------------------------------------------------------- | ---------------------------------------------- |
| name | string | Key name segment (e.g. "token"). |
| queryKey | (params: TParams) => string[] | Builds the variable segments of the query key. |
| fetch | (client: API.IClient, params: TParams) => Promise<TData> | Fetches data using the API client. |
| defaultOptions | Partial<Omit<UseQueryOptions, "queryKey" \| "queryFn">> | Default options merged below caller options. |
Returns { queryKey, fetch, useQuery }.
createInfiniteQueryHook<TParams, TData>(config): InfiniteQueryHookReturn<TParams, TData>
Creates a useInfiniteQuery-based hook and its queryKey builder for cursor-paginated endpoints. TData must extend { hasNext?: boolean; endCursor?: string }.
| Config Field | Type | Description |
| ------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------- |
| name | string | Key name segment (e.g. "walletPortfolios"). |
| queryKey | (params: TParams) => string[] | Builds the variable segments of the query key. |
| fetch | (client: API.IClient, params: TParams, cursor: string \| undefined) => Promise<TData> | Fetches a single page of data. |
Returns { queryKey, useInfiniteQuery }.
Utility Functions
| Function | Signature | Description |
| -------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------- |
| toKeySegment | (value: string \| number \| boolean \| Date \| undefined \| null) => string | Serializes a primitive into a stable query key segment. |
| toSortedKeySegment | (value: unknown[] \| undefined \| null) => string | Serializes an array into a sorted JSON string for query keys. |
Usage Examples
Basic setup
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Client } from "@liberfi.io/client";
import { DexClientProvider } from "@liberfi.io/react";
const queryClient = new QueryClient();
const client = new Client("your-api-token");
function App() {
return (
<QueryClientProvider client={queryClient}>
<DexClientProvider client={client} subscribeClient={client}>
<MyComponent />
</DexClientProvider>
</QueryClientProvider>
);
}Fetching token data
import { useTokenQuery } from "@liberfi.io/react";
import { Chain } from "@liberfi.io/types";
function TokenPrice({ address }: { address: string }) {
const {
data: token,
isLoading,
error,
} = useTokenQuery({
chain: Chain.SOLANA,
address,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{token.name}: ${token.marketData?.priceInUsd}
</div>
);
}Real-time subscription
import { useCallback } from "react";
import { useTokenSubscription } from "@liberfi.io/react";
import { Chain, API } from "@liberfi.io/types";
function LiveTokenPrice({ address }: { address: string }) {
const handleUpdate = useCallback((updates: API.TokenSubscribed[]) => {
for (const update of updates) {
console.log("New price:", update.marketData?.priceInUsd);
}
}, []);
useTokenSubscription({ chain: Chain.SOLANA, address }, handleUpdate, {
enabled: true,
});
return <div>Listening for updates...</div>;
}Manual cache invalidation
import { useQueryClient } from "@tanstack/react-query";
import { tokenQueryKey } from "@liberfi.io/react";
import { Chain } from "@liberfi.io/types";
function RefreshButton({ address }: { address: string }) {
const queryClient = useQueryClient();
return (
<button
onClick={() =>
queryClient.invalidateQueries({
queryKey: tokenQueryKey({ chain: Chain.SOLANA, address }),
})
}
>
Refresh
</button>
);
}Creating a custom query hook
import { createQueryHook } from "@liberfi.io/react";
interface MyDataParams {
chain: string;
id: string;
}
interface MyData {
value: number;
}
const {
queryKey: myDataQueryKey,
fetch: fetchMyData,
useQuery: useMyDataQuery,
} = createQueryHook<MyDataParams, MyData>({
name: "myData",
queryKey: (p) => [p.chain, p.id],
fetch: (client, p) => client.getMyData(p.chain, p.id),
});
export { myDataQueryKey, fetchMyData, useMyDataQuery };Infinite scrolling
import { useWalletPortfoliosInfiniteQuery } from "@liberfi.io/react";
import { Chain } from "@liberfi.io/types";
function PortfolioList({ walletAddress }: { walletAddress: string }) {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useWalletPortfoliosInfiniteQuery({
chain: Chain.SOLANA,
address: walletAddress,
limit: 20,
});
const portfolios = data?.pages.flatMap((page) => page.portfolios) ?? [];
return (
<div>
{portfolios.map((p) => (
<div key={p.address}>
{p.symbol}: ${p.amountInUsd}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}Testing
Unit Tests
Unit tests mock the API client and verify hook behavior (query keys, fetch delegation, subscription lifecycle).
pnpm testIntegration Tests
Integration tests use the real @liberfi.io/client to verify end-to-end API communication.
Setup:
cp .env.test.example .env.test # review and adjust URLs if neededRequired environment variables (in .env.test):
| Variable | Description |
| ------------------------ | ------------------------------------------------- |
| CHAINSTREAM_BASE_URL | REST API server URL |
| API_BASE_URL | Auth API base URL (for fetching the access token) |
| CHAINSTREAM_STREAM_URL | (optional) WebSocket URL, defaults to SDK value |
Run:
pnpm test:integrationWhen the environment variables are not set, integration tests are automatically skipped.
Architecture:
src/__integration__/fetch.integration.test.ts— Tests standalonefetch*functions against the real API in a Node environment. Validates API response shapes and end-to-end data flow.src/__integration__/hooks.integration.test.tsx— Tests hooks viarenderHookwith a real client, verifying provider injection and full end-to-end data fetching through React Query. Uses a custom jsdom environment (jsdom-with-fetch.js) that injects Node's nativefetchand removesXMLHttpRequestto bypass CORS.
Future Improvements
- Add a standalone
fetchfunction to infinite query hooks for API consistency. - Add a mutation key to
useSendTxMutationfor cache introspection. - Consolidate
version.tsglobal side effect into a shared utility. - Document subscription deps-stability best practices for consumers.
- Type
useSubscriptionEffectdeps more strictly (currentlyunknown[]).
