Async Operations Examples
Examples for handling async operations with Craft.
Using createDraft / finishDraft
For async operations, use createDraft and finishDraft for manual control:
typescript
import { createDraft, finishDraft } from "@sylphx/craft";
async function updateUser(state: State, userId: string) {
const draft = createDraft(state);
// Make changes over time
const userData = await fetchUser(userId);
draft.user = userData;
// More async operations
const settings = await fetchSettings(userId);
draft.settings = settings;
// Finalize when ready
return finishDraft(draft);
}
const nextState = await updateUser(currentState, "123");WARNING
Don't use the regular craft function with async producers. The draft is finalized immediately when the producer returns, even if it returns a Promise.
Loading States
Simple Loading Pattern
typescript
interface State {
data: any | null;
loading: boolean;
error: string | null;
}
async function loadData(state: State) {
// Set loading
let draft = createDraft(state);
draft.loading = true;
draft.error = null;
let current = finishDraft(draft);
try {
const data = await fetchData();
// Set success
draft = createDraft(current);
draft.data = data;
draft.loading = false;
return finishDraft(draft);
} catch (error) {
// Set error
draft = createDraft(current);
draft.error = error.message;
draft.loading = false;
return finishDraft(draft);
}
}With Status Union
typescript
type Status =
| { type: 'idle' }
| { type: 'loading' }
| { type: 'success'; data: any }
| { type: 'error'; error: string };
interface State {
status: Status;
}
async function loadData(state: State) {
// Set loading
let draft = createDraft(state);
draft.status = { type: 'loading' };
let current = finishDraft(draft);
try {
const data = await fetchData();
// Set success
draft = createDraft(current);
draft.status = { type: 'success', data };
return finishDraft(draft);
} catch (error) {
// Set error
draft = createDraft(current);
draft.status = { type: 'error', error: error.message };
return finishDraft(draft);
}
}Multiple Async Operations
Sequential
typescript
async function loadAllData(state: State) {
const draft = createDraft(state);
// Load sequentially
draft.user = await fetchUser();
draft.settings = await fetchSettings();
draft.preferences = await fetchPreferences();
return finishDraft(draft);
}Parallel
typescript
async function loadAllDataParallel(state: State) {
const draft = createDraft(state);
// Load in parallel
const [user, settings, preferences] = await Promise.all([
fetchUser(),
fetchSettings(),
fetchPreferences()
]);
draft.user = user;
draft.settings = settings;
draft.preferences = preferences;
return finishDraft(draft);
}React Integration
useState with Async
typescript
import { useState } from 'react';
import { createDraft, finishDraft } from '@sylphx/craft';
function useAsyncState<T>(initialState: T) {
const [state, setState] = useState(initialState);
const updateAsync = async (
updater: (draft: Draft<T>) => Promise<void>
) => {
const draft = createDraft(state);
await updater(draft);
setState(finishDraft(draft));
};
return [state, updateAsync] as const;
}
// Usage
function UserProfile() {
const [state, updateAsync] = useAsyncState({
user: null,
loading: false
});
const loadUser = async (id: string) => {
await updateAsync(async (draft) => {
draft.loading = true;
try {
draft.user = await fetchUser(id);
} finally {
draft.loading = false;
}
});
};
return (
// ... JSX
);
}useReducer with Async
typescript
import { useReducer } from 'react';
import { craft, createDraft, finishDraft } from '@sylphx/craft';
type Action =
| { type: 'LOAD_START' }
| { type: 'LOAD_SUCCESS'; payload: any }
| { type: 'LOAD_ERROR'; error: string };
function reducer(state: State, action: Action): State {
return craft(state, draft => {
switch (action.type) {
case 'LOAD_START':
draft.loading = true;
draft.error = null;
break;
case 'LOAD_SUCCESS':
draft.data = action.payload;
draft.loading = false;
break;
case 'LOAD_ERROR':
draft.error = action.error;
draft.loading = false;
break;
}
});
}
function DataLoader() {
const [state, dispatch] = useReducer(reducer, initialState);
const loadData = async () => {
dispatch({ type: 'LOAD_START' });
try {
const data = await fetchData();
dispatch({ type: 'LOAD_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'LOAD_ERROR', error: error.message });
}
};
return (
// ... JSX
);
}Debounced Updates
typescript
import { createDraft, finishDraft } from '@sylphx/craft';
class DebouncedState<T> {
private draft: Draft<T> | null = null;
private timer: NodeJS.Timeout | null = null;
constructor(
private state: T,
private delay: number,
private onUpdate: (state: T) => void
) {}
update(producer: (draft: Draft<T>) => void): void {
// Create draft if needed
if (!this.draft) {
this.draft = createDraft(this.state);
}
// Apply updates
producer(this.draft);
// Clear existing timer
if (this.timer) {
clearTimeout(this.timer);
}
// Set new timer
this.timer = setTimeout(() => {
if (this.draft) {
this.state = finishDraft(this.draft);
this.draft = null;
this.onUpdate(this.state);
}
}, this.delay);
}
flush(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.draft) {
this.state = finishDraft(this.draft);
this.draft = null;
this.onUpdate(this.state);
}
}
}
// Usage
const debounced = new DebouncedState(
initialState,
500,
(state) => {
console.log("Updated:", state);
}
);
// Multiple rapid updates
debounced.update(draft => { draft.count++; });
debounced.update(draft => { draft.count++; });
debounced.update(draft => { draft.count++; });
// Only one finalization after 500ms
// Force immediate finalization
debounced.flush();Batched Updates
typescript
class BatchedUpdates<T> {
private draft: Draft<T> | null = null;
private batchId = 0;
constructor(
private state: T,
private onUpdate: (state: T) => void
) {}
startBatch(): number {
if (!this.draft) {
this.draft = createDraft(this.state);
this.batchId++;
}
return this.batchId;
}
update(producer: (draft: Draft<T>) => void): void {
if (!this.draft) {
this.startBatch();
}
producer(this.draft!);
}
commitBatch(batchId?: number): void {
if (batchId && batchId !== this.batchId) {
throw new Error("Batch ID mismatch");
}
if (this.draft) {
this.state = finishDraft(this.draft);
this.draft = null;
this.onUpdate(this.state);
}
}
cancelBatch(): void {
this.draft = null;
}
}
// Usage
const batched = new BatchedUpdates(
initialState,
(state) => setState(state)
);
const batchId = batched.startBatch();
batched.update(draft => { draft.count++; });
batched.update(draft => { draft.name = "Alice"; });
batched.update(draft => { draft.active = true; });
batched.commitBatch(batchId);Async Validation
typescript
interface FormState {
values: Record<string, any>;
errors: Record<string, string>;
validating: Record<string, boolean>;
}
async function validateField(
state: FormState,
field: string,
value: any,
validator: (value: any) => Promise<string | null>
): Promise<FormState> {
// Set validating
let draft = createDraft(state);
draft.validating[field] = true;
delete draft.errors[field];
let current = finishDraft(draft);
try {
const error = await validator(value);
// Set result
draft = createDraft(current);
draft.validating[field] = false;
if (error) {
draft.errors[field] = error;
}
return finishDraft(draft);
} catch (err) {
// Set error
draft = createDraft(current);
draft.validating[field] = false;
draft.errors[field] = "Validation failed";
return finishDraft(draft);
}
}
// Usage
const nextState = await validateField(
state,
"email",
"test@example.com",
async (email) => {
const exists = await checkEmailExists(email);
return exists ? "Email already taken" : null;
}
);Streaming Updates
typescript
async function streamData(
state: State,
stream: ReadableStream<Uint8Array>,
onProgress: (state: State) => void
): Promise<State> {
const draft = createDraft(state);
const reader = stream.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
draft.data += chunk;
draft.progress = calculateProgress();
// Report progress
onProgress(finishDraft(createDraft(draft)));
}
draft.complete = true;
return finishDraft(draft);
} catch (error) {
draft.error = error.message;
return finishDraft(draft);
}
}Polling
typescript
class PollingState<T> {
private intervalId: NodeJS.Timeout | null = null;
constructor(
private state: T,
private onUpdate: (state: T) => void
) {}
startPolling(
fetcher: () => Promise<Partial<T>>,
interval: number
): void {
this.intervalId = setInterval(async () => {
try {
const data = await fetcher();
const draft = createDraft(this.state);
Object.assign(draft, data);
this.state = finishDraft(draft);
this.onUpdate(this.state);
} catch (error) {
console.error("Polling error:", error);
}
}, interval);
}
stopPolling(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
// Usage
const polling = new PollingState(initialState, setState);
polling.startPolling(() => fetchLatestData(), 5000);
// Stop when done
polling.stopPolling();Next Steps
- Composition - Reusable updaters
- Patches - JSON Patches
- API Reference - Core API