React hooks for easy integration with the Willow network. Built on top of @willow-network/sdk.
npm install @willow-network/react-hooks @willow-network/sdk
# or
yarn add @willow-network/react-hooks @willow-network/sdkimport React from 'react';
import { WillowProvider, useAuth, useCollection, useData } from '@willow-network/react-hooks';
// 1. Wrap your app with WillowProvider
function App() {
return (
<WillowProvider config={{ apiUrl: 'http://localhost:3031' }}>
<NotesApp />
</WillowProvider>
);
}
// 2. Use hooks in your components
function NotesApp() {
const { isAuthenticated, generateAndRegister } = useAuth();
const { store } = useCollection('notes');
// Auto-generate DID and authenticate
const handleLogin = async () => {
const { did, privateKey } = await generateAndRegister();
console.log('Logged in as:', did);
};
// Store data
const createNote = async () => {
await store('note-1', {
title: 'My Note',
content: 'Hello from React!',
created: Date.now(),
});
};
// Read data with automatic caching + proof verification
const { data: note, isLoading } = useData('notes', 'note-1');
if (!isAuthenticated) {
return <button onClick={handleLogin}>Generate DID & Login</button>;
}
return (
<div>
<button onClick={createNote}>Create Note</button>
{isLoading ? (
<p>Loading...</p>
) : note ? (
<div>
<h3>{note.title}</h3>
<p>{note.content}</p>
</div>
) : (
<p>No note found</p>
)}
</div>
);
}Access the Willow client and authentication state.
const {
client, // WillowClient instance
config, // WillowConfig
isAuthenticated, // Boolean: has an identity been set?
hasIdentity, // Same as isAuthenticated
isLoading, // Loading state
error, // Error state
initialize, // Initialize with existing credentials (optional)
setIdentity, // (did, privateKey, publicKeyId) => void
clearIdentity, // Clear the active identity
registerDid, // Register a DID document
} = useWillow();Authentication operations.
const {
isAuthenticated,
hasIdentity,
setIdentity, // (did, privateKey, publicKeyId) => void
clearIdentity, // Clear identity (effectively logout)
generateAndRegister, // Generate keypair, build DID document, register it, and set identity
isGenerating, // Loading state for generation
} = useAuth();
// Quick start for new users
const handleQuickStart = async () => {
const { did, privateKey, publicKey, didDocument } = await generateAndRegister();
// Save privateKey securely if you want to re-use this DID later
};Fetch a single record with SWR caching.
const { data, error, isLoading, refetch } = useData(
'dataset-id',
'key',
{
// Optional SWR options + Willow-specific flags
refreshInterval: 5000,
skipVerification: false,
}
);Mutations for a single dataset.
const { store, update, remove } = useDataMutation('dataset-id');
await store('key', { name: 'value' });
await update('key', { name: 'new value' });
await remove('key');Convenience wrapper combining mutations + batch operations + per-key reads for a single dataset.
const {
collection, // { datasetId, client } helper
store, // Store data
update, // Update data
remove, // Delete data
batchStore, // Batch insert
getMultiple, // Read multiple keys
useItem, // (key) => useData(datasetId, key)
} = useCollection('notes');
// Per-key reads with caching
const { data: note1 } = useItem('note-1');
const { data: note2 } = useItem('note-2');
// Batch insert
await batchStore([
{ key: 'note-3', value: { title: 'Note 3' } },
{ key: 'note-4', value: { title: 'Note 4' } },
]);Query indexed data with filters, sort, and pagination.
const { data, documents, isLoading, error, refetch } = useQuery('dataset-id', {
filters: { category: 'electronics' },
sort: { field: 'price', order: 'asc' },
limit: 20,
});
// `documents` is a convenience alias for data?.documentsRegister datasets and deregister subgroves.
const { registerDataset, deregisterSubgrove, isRegistering, error } = useRegistration();
await registerDataset({
dataset_id: 'notes',
name: 'User Notes',
dataset_path: ['collections'],
schema: {
version: 1,
fields: {
title: { type: 'string', indexed: true, required: true },
content: { type: 'string' },
},
indexes: [
{ name: 'by_title', fields: ['title'], unique: false, type: 'hash' },
],
required_fields: ['title'],
},
owner_did: myDid,
writers: [myDid],
readers: [myDid],
});Get cryptographic proofs.
const { proof, error, isLoading } = useProof('dataset-id', 'key');
if (proof) {
console.log('Merkle proof:', proof);
}Reads live on useFiles; uploads/downloads/deletes live on useFileMutations.
import { useFiles, useFileMutations } from '@willow-network/react-hooks';
function FileManager() {
const { files, isLoading } = useFiles('my-subgrove');
const { upload, download, deleteFile } = useFileMutations('my-subgrove');
const handleUpload = async (file: File) => {
const data = Buffer.from(await file.arrayBuffer());
await upload(file.name, file.name, data, 'https://storage1.example.com');
};
return (
<div>
{files?.map((f) => (
<div key={f.file_key}>
{f.filename} ({f.total_size} bytes)
</div>
))}
</div>
);
}<WillowProvider
config={{
apiUrl: 'https://api.willow.tech',
did: 'did:willow:eth:0x...',
privateKey: process.env.REACT_APP_PRIVATE_KEY,
}}
autoConnect={true}
>
<App />
</WillowProvider>function MyComponent() {
const { error } = useWillow();
const { store } = useDataMutation('dataset');
const handleStore = async () => {
try {
await store('key', data);
} catch (err) {
// Handle store error
}
};
if (error) {
return <div>Global error: {error.message}</div>;
}
// ...
}import { mutate } from 'swr';
function NoteEditor({ noteId }) {
const { data: note } = useData('notes', noteId);
const { update } = useDataMutation('notes');
const handleUpdate = async (newContent) => {
const optimisticNote = { ...note, content: newContent };
mutate(['data', 'notes', noteId], optimisticNote, false);
try {
await update(noteId, optimisticNote);
} catch (err) {
// Revert on error
mutate(['data', 'notes', noteId]);
}
};
}import { Suspense } from 'react';
function App() {
return (
<WillowProvider config={config}>
<Suspense fallback={<Loading />}>
<Notes />
</Suspense>
</WillowProvider>
);
}
function Notes() {
const { data } = useData('notes', 'note-1', { suspense: true });
return <div>{data.title}</div>;
}All hooks are fully typed. Define your data types as you go:
interface Note {
title: string;
content: string;
tags: string[];
created: number;
}
function useNotes() {
const { data } = useData('notes', 'note-1');
// Cast at the call-site if you want sharper inference.
return data as Note | null | undefined;
}- Provider placement: put
WillowProviderat the root of your app - Error boundaries: use React error boundaries for fatal errors
- Loading states: always handle loading states in your UI
- Key management: store private keys securely (never in source)
- Caching: leverage SWR caching for repeated reads
- Batch operations: prefer batch operations when storing many records
See the examples/ directory:
quickstart.tsx— minimal: provider, generate DID, store, readapp_registration.tsx— register datasets with schemas and indexesdata_operations.tsx— store, batch store, get, query, update, deleteindexing_and_graphql.tsx— GraphQL queries, subgroves, indexers, verification statstoken_and_validators.tsx— token info, balances, fee schedules, validators (read-only)ProofVerificationExample.tsx— verified vs unverified reads, manual proof verification, server-assisted modefull_app_notes.tsx— complete notes app: auth, setup, CRUD, search
function TodoApp() {
const { isAuthenticated } = useAuth();
const { store, remove, useItem } = useCollection('todos');
const [todoIds, setTodoIds] = useState<string[]>([]);
const addTodo = async (text: string) => {
const id = `todo-${Date.now()}`;
await store(id, { text, completed: false });
setTodoIds((prev) => [...prev, id]);
};
const toggleTodo = async (id: string, completed: boolean) => {
await store(id, { completed });
};
const deleteTodo = async (id: string) => {
await remove(id);
setTodoIds((prev) => prev.filter((t) => t !== id));
};
// ... render UI
}MIT