Skip to content

Use Arkiv with React

In a React app that connects wallets through wagmi, read through an Arkiv public client and write through an Arkiv wallet client built from wagmi’s connection. Users connect once, with the connect button you already have. This guide uses a Next.js app with RainbowKit, which includes wagmi and TanStack Query. The same code works with ConnectKit or plain wagmi.

Before you start, you need:

  • A React app with wagmi and TanStack Query set up, for example from RainbowKit’s Next.js template
  • The SDK installed
Terminal window
bun add @arkiv-network/sdk

rabdi makes the chain from your access key. Arkiv’s public RPC is heavily rate limited to prevent abuse, so get a free key from the Arkiv Hub. Make the chain once and share it. Make a second one without the key for the user’s wallet:

lib/arkiv.ts
import { rabdi } from "@arkiv-network/sdk/chains"
export const chain = rabdi({
accessKey: process.env.NEXT_PUBLIC_ARKIV_ACCESS_KEY ?? "",
})
// For the user's wallet, which keeps the chain's RPC URL
export const walletChain = rabdi({ accessKey: "" })

The key ships to the browser, so add your site to the key’s Origins on the Hub. See lock the key down.

Add walletChain to the chains your config lists, next to any chains your app already uses:

lib/wagmi.ts
import { getDefaultConfig } from "@rainbow-me/rainbowkit"
import { walletChain } from "@/lib/arkiv"
export const config = getDefaultConfig({
appName: "My App",
projectId: "your_walletconnect_project_id",
chains: [walletChain],
ssr: true,
})

RainbowKit then asks the user to switch networks when their wallet is on a chain that isn’t in the list. If the wallet doesn’t have Rabdi yet, wagmi adds it with the chain’s RPC URL. The wallet keeps that URL and uses it for every app on Rabdi, so walletChain has no key. Otherwise other apps would spend your quota. Your public client still reads with chain and your key.

Reads don’t need the user’s wallet, so one public client serves the whole app. Put it in lib/arkiv.ts, next to the chain and your schemas:

lib/arkiv.ts
import { createPublicClient } from "@arkiv-network/sdk"
import { addr, bytes, dec, defineSchema, str } from "@arkiv-network/sdk/schema"
import { http } from "viem"
export const Posts = defineSchema("my-app.posts", {
author: addr().indexed(),
category: str().indexed(),
score: dec().indexed(),
title: str(),
body: bytes(),
summary: str().optional(),
})
export const publicClient = createPublicClient({
chain,
transport: http(),
})

Use of the public RPC is subject to the Arkiv Terms of Use and Privacy Policy.

Wrap each query in useQuery. This hook lists the posts an account owns:

hooks/use-posts.ts
import { useQuery } from "@tanstack/react-query"
import type { Address } from "viem"
import { Posts, publicClient } from "@/lib/arkiv"
export function usePosts(owner: Address | undefined) {
return useQuery({
queryKey: ["posts", owner],
enabled: owner !== undefined,
queryFn: async () => {
const { entities } = await publicClient
.select({ $key: true, title: true, score: true })
.from(Posts)
.ownedBy(owner as Address)
.fetch()
return entities
},
})
}

Anyone can write entities with your schema’s tag, so ownedBy keeps the list to the account’s own posts. Keep bigint values out of query keys, since TanStack Query can’t hash them.

Use the hook with the connected account:

components/my-posts.tsx
"use client"
import { useAccount } from "wagmi"
import { usePosts } from "@/hooks/use-posts"
export function MyPosts() {
const { address } = useAccount()
const { data: posts, isPending } = usePosts(address)
if (!address) return <p>Connect a wallet to see your posts.</p>
if (isPending) return <p>Loading…</p>
return (
<ul>
{posts?.map((post) => (
<li key={post.$key}>
{post.title.value} ({post.score.value})
</li>
))}
</ul>
)
}

Writes need an Arkiv wallet client. Build it from the connection wagmi already manages:

hooks/use-arkiv-wallet.ts
import { createWalletClient } from "@arkiv-network/sdk"
import { useMemo } from "react"
import { custom } from "viem"
import { useWalletClient } from "wagmi"
import { chain } from "@/lib/arkiv"
export function useArkivWallet() {
// Only resolves while the wallet is connected and on Rabdi
const { data: walletClient } = useWalletClient({ chainId: chain.id })
return useMemo(() => {
if (!walletClient) return undefined
return createWalletClient({
chain,
transport: custom(walletClient.transport),
account: walletClient.account,
})
}, [walletClient])
}

custom(walletClient.transport) sends the Arkiv client’s requests through the connected wallet, so the user approves each write there. With chainId: chain.id, the hook returns undefined until the wallet is connected and on Rabdi.

Don’t use window.ethereum in a wagmi app. It may not be the wallet the user connected, and with WalletConnect there may be none.

Wrap each write in useMutation, and invalidate the queries it changes:

hooks/use-create-post.ts
import { ExpirationTime } from "@arkiv-network/sdk"
import { addr, dec } from "@arkiv-network/sdk/schema"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { stringToBytes } from "viem"
import { useArkivWallet } from "@/hooks/use-arkiv-wallet"
import { Posts } from "@/lib/arkiv"
export function useCreatePost() {
const arkiv = useArkivWallet()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (post: { title: string; body: string }) => {
if (!arkiv) throw new Error("Connect a wallet on Rabdi first")
const values = {
author: addr(arkiv.account.address),
category: "news",
score: dec("4.5"),
title: post.title,
body: stringToBytes(post.body),
}
const expiration = ExpirationTime.fromDays(30)
return arkiv.createEntity(Posts, values, { expiration })
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["posts"] }),
})
}

createEntity resolves once the post is in a block, so the refetched list includes it. Call the mutation from a component:

components/new-post-button.tsx
"use client"
import { useCreatePost } from "@/hooks/use-create-post"
import { useArkivWallet } from "@/hooks/use-arkiv-wallet"
export function NewPostButton() {
const ready = useArkivWallet() !== undefined
const createPost = useCreatePost()
return (
<button
disabled={!ready || createPost.isPending}
onClick={() => createPost.mutate({ title: "Hello", body: "From React" })}
>
{createPost.isPending ? "Confirm in your wallet…" : "New post"}
</button>
)
}
  • A component that calls these hooks is a client component, so it needs "use client" at the top. The hooks don’t
  • lib/arkiv.ts has no browser code. Server components and route handlers can import publicClient and query with it