Skip to content

Managing State

How to persist view state across renders and re-mounts.

What kind of state do you need?
├── Need to sync with LLM? → data-llm attribute
│ (The model needs to know what user is viewing)
├── Simple key/value state? → useViewState
│ (Form inputs, selected items, flags — persisted by the host)
└── Complex state with actions? → createStore
(Multiple related values, computed state, async actions)

Persistent state that survives re-renders and display mode changes. Unlike React’s useState, this state is stored by the host and restored when your view remounts.

import { useViewState } from "@usefractal/frac/web";
function CounterView() {
const [state, setState] = useViewState({ count: 0 });
if (!state) return null;
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => setState(prev => ({ count: prev.count + 1 }))}>
Increment
</button>
</div>
);
}
const [state, setState] = useViewState<T>(defaultState);
  • state: Current state (or null if not yet initialized)
  • setState: Update function (accepts value or updater function)
  • defaultState: Initial state (used if no persisted state exists)

Form state:

const [formData, setFormData] = useViewState({
name: "",
email: "",
message: "",
});
const updateField = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};

Selection state:

const [selected, setSelected] = useViewState<Set<string>>(new Set());
const toggleItem = (id: string) => {
setSelected(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};

A Zustand store that automatically syncs with the host’s persistent state. Unlike useViewState, this provides actions, computed values, and middleware support for complex state management.

import { createStore } from "@usefractal/frac/web";
type CartItem = { id: string; name: string; price: number; quantity: number };
type CartState = {
items: CartItem[];
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
total: () => number;
clear: () => void;
};
const useCartStore = createStore<CartState>((set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find(existingItem => existingItem.id === item.id);
if (existing) {
return {
items: state.items.map(existingItem =>
existingItem.id === item.id ? { ...existingItem, quantity: existingItem.quantity + 1 } : existingItem
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id),
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity } : item
),
})),
total: () => get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
clear: () => set({ items: [] }),
}), { items: [] }); // Default state for first load

Usage:

function CartView() {
const items = useCartStore(state => state.items);
const addItem = useCartStore(state => state.addItem);
const total = useCartStore(state => state.total());
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>{item.name} x{item.quantity}</li>
))}
</ul>
<p>Total: ${total}</p>
</div>
);
}

createStore automatically:

  • Syncs state to the host’s persistent storage
  • Restores state from the host on load
  • Filters out functions (actions) during serialization
FeatureuseViewStatecreateStore
Setup complexitySimpleModerate
State shapeAny objectObject with actions
Multiple consumersRe-renders allSelective subscriptions
Computed valuesManualBuilt-in with get()
Async actionsManual with callbacksBuilt-in
Best forSimple forms, flagsShopping carts, complex flows

State management is separate from LLM context. Use both when needed:

function ProductListView() {
const [selected, setSelected] = useViewState<string | null>(null);
const products = useToolInfo<Product[]>().output?.structuredContent.products;
const selectedProduct = products?.find(product => product.id === selected);
return (
<div data-llm={selected
? `User selected: ${selectedProduct?.name}`
: "User browsing product list"
}>
{products?.map(product => (
<div
key={product.id}
onClick={() => setSelected(product.id)}
data-llm={`Product: ${product.name} - $${product.price}`}
>
{product.name}
</div>
))}
</div>
);
}
  • useViewState persists the selection
  • data-llm tells the model what the user sees

View state persists across re-renders, re-mounts, and display mode changes. frac handles the storage mechanism automatically.

View state resets when:

  • A new conversation starts
  • The tool is called again with new input
  • The user explicitly clears it

If you’re using React useState and losing state on re-renders:

// Before: State lost on re-mount
const [selected, setSelected] = useState<string | null>(null);
// After: State persists
const [selected, setSelected] = useViewState<string | null>(null);

The API is intentionally similar to useState for easy migration.