Troubleshooting
This guide covers common issues you might encounter with useFlow and how to solve them.
Common issues
Section titled “Common issues”TypeScript errors
Section titled “TypeScript errors””Type ‘string’ is not assignable to type…”
Section titled “”Type ‘string’ is not assignable to type…””Problem: TypeScript can’t infer the correct step names when using dynamic navigation.
// ❌ Error: Type 'string' is not assignableconst stepName = "profile";next(stepName);Solution: Use as const or type the variable:
// ✅ Solution 1: as constconst stepName = "profile" as const;next(stepName);
// ✅ Solution 2: Type the variableconst stepName: "profile" | "preferences" = "profile";next(stepName);“Property does not exist on type ‘never’”
Section titled ““Property does not exist on type ‘never’””Problem: Using the generic useFlowState() hook without proper typing.
// ❌ Error: Property 'name' does not existconst { context } = useFlowState();console.log(context.name);Solution: Use the flow-specific hook or provide type parameter:
// ✅ Solution 1: Use flow-specific hook (recommended)const { context } = myFlow.useFlowState({ step: "profile" });
// ✅ Solution 2: Provide type parameterconst { context } = useFlowState<MyContextType>();Persistence issues
Section titled “Persistence issues”State not persisting
Section titled “State not persisting”Problem: Flow state isn’t being saved to storage.
Common causes and solutions:
- No persister configured:
// ❌ Missing persister<Flow flow={myFlow}>
// ✅ Add persisterimport { createLocalStorageStore, createPersister } from '@useflow/react';
const persister = createPersister({ store: createLocalStorageStore() });
<Flow flow={myFlow} persister={persister}>- Manual save mode without calling save():
// If using saveMode="manual"const { save } = useFlowState();
// Remember to call save() when neededconst handleSave = async () => { await save();};- Storage quota exceeded:
<Flow flow={myFlow} persister={persister} onPersistenceError={(error) => { console.error('Storage error:', error); // Handle storage quota or access issues }}>State not restoring
Section titled “State not restoring”Problem: Saved state exists but isn’t being restored.
Solutions:
- Check if restoration is in progress:
function MyStep() { const { isRestoring } = useFlowState();
if (isRestoring) { return <LoadingSpinner />; }
// Your component content}- Verify flow ID and instance ID match:
// State is saved with flowId + instanceId + variantId// Make sure these are consistent across sessions<Flow flow={myFlow} instanceId="user-123" // Must be same to restore>- Check for version mismatches:
// If flow structure changed, old state might be invalidconst flow = defineFlow({ id: "myFlow", version: "2", // Version change might invalidate old state start: "welcome", steps: { // ... }}).with<MyContext>(() => ({ // Add migration function for version changes migration: (oldState, oldVersion) => { if (oldVersion === "1") { // Transform old state to new format return { ...oldState, newField: "default" }; } return oldState; },}));Navigation issues
Section titled “Navigation issues””Cannot go back from first step”
Section titled “”Cannot go back from first step””Problem: Calling back() when on the first step.
Solution: Check if back navigation is available:
function MyStep() { const { back, canGoBack } = useFlowState();
return ( <button onClick={back} disabled={!canGoBack}> Back </button> );}Navigation not working
Section titled “Navigation not working”Problem: Calling next() but nothing happens.
Common causes:
- Step has no next defined (terminal step):
// Check your flow definitionsteps: { complete: { } // Terminal step - no next}
// Check if next is availableconst { nextSteps } = useFlowState();if (!nextSteps) { console.log("This is a terminal step");}- Branching step requires target:
// For branching steps, must specify targetsteps: { choice: { next: ["option1", "option2"]; }}
// ❌ Error: Must specify targetnext();
// ✅ Correct: Specify targetnext("option1");Context issues
Section titled “Context issues”Context not updating
Section titled “Context not updating”Problem: Setting context but values don’t change.
Common causes:
- Mutating context directly:
// ❌ Don't mutate directlyconst { context, setContext } = useFlowState();context.name = "Alice"; // Won't trigger update
// ✅ Use setContextsetContext({ name: "Alice" });- Async updates not handled:
// ❌ State might be staleconst handleSubmit = async () => { await saveData(); setContext({ saved: true }); next(); // Might navigate before context updates};
// ✅ Update context in next() callconst handleSubmit = async () => { await saveData(); next({ saved: true }); // Context and navigation together};Context type errors
Section titled “Context type errors”Problem: TypeScript errors when updating context.
Solution: Define your context type properly:
// Define complete context typetype MyContext = { name: string; email?: string; // Optional fields preferences: { theme: "light" | "dark"; };};
// Use with your flowconst flow = defineFlow({ // ...});Debugging techniques
Section titled “Debugging techniques”Enable debug logging
Section titled “Enable debug logging”// Log all navigation events<Flow flow={myFlow} onTransition={({ from, to, action, context }) => { console.log(`[Flow] ${action}: ${from} → ${to}`, context); }} onContextUpdate={({ oldContext, newContext }) => { console.log('[Flow] Context updated:', { oldContext, newContext }); }}>Inspect flow state
Section titled “Inspect flow state”function DebugPanel() { const { stepId, context, path, history, status } = useFlowState();
return ( <pre style={{ position: "fixed", bottom: 0, right: 0, background: "black", color: "white", padding: "10px", }} > {JSON.stringify( { currentStep: stepId, status, context, path: path.map((p) => p.stepId), history: history.map((h) => ({ step: h.stepId, action: h.action, })), }, null, 2 )} </pre> );}Use browser devtools
Section titled “Use browser devtools”- Check localStorage:
// In browser consolelocalStorage.getItem("useflow:myFlow:default:default");- Clear saved state:
// Clear specific flowlocalStorage.removeItem("useflow:myFlow:default:default");
// Clear all useFlow dataObject.keys(localStorage) .filter((key) => key.startsWith("useflow:")) .forEach((key) => localStorage.removeItem(key));Performance issues
Section titled “Performance issues”Memory leaks
Section titled “Memory leaks”Problem: Memory usage increases over time.
Solution: Clean up effects and listeners:
function MyStep() { const { next } = useFlowState();
useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Enter") next(); };
window.addEventListener("keydown", handler);
// Clean up! return () => window.removeEventListener("keydown", handler); }, [next]);}Getting help
Section titled “Getting help”Before asking for help
Section titled “Before asking for help”- Check the console for error messages
- Verify your flow definition is correct
- Try the debug techniques above
- Check if you’re using the latest version
- Create a minimal reproduction of the issue
Where to get help
Section titled “Where to get help”- GitHub Issues: Report bugs
- GitHub Discussions: Ask questions
- Examples: Check the recipes for working examples
Reporting issues
Section titled “Reporting issues”When reporting an issue, include:
- useFlow version
- React version
- TypeScript version (if applicable)
- Minimal code example that reproduces the issue
- Error messages from the console
- Expected vs actual behavior
Example issue report:
## Issue: context not persisting between page refreshes
**Versions:**
- useFlow: 1.0.0- React: 18.2.0- TypeScript: 5.0.0
**Code:**\`\`\`tsxconst flow = defineFlow({id: "test",steps: { /_ ... _/ }});
<Flow flow={flow} persister={createPersister({ store: createLocalStorageStore() })}>{/_ ... _/}</Flow>\`\`\`
**Expected:** Context should persist**Actual:** Context resets on refresh**Console errors:** NoneCan i have multiple flows on the same page?
Section titled “Can i have multiple flows on the same page?”Yes! Each flow maintains its own state:
<div> <Flow flow={onboardingFlow} instanceId="onboarding"> {/* ... */} </Flow>
<Flow flow={checkoutFlow} instanceId="checkout"> {/* ... */} </Flow></div>How do i reset a flow programmatically?
Section titled “How do i reset a flow programmatically?”Use the reset() function:
function MyStep() { const { reset } = useFlowState();
const handleError = () => { alert("Something went wrong!"); reset(); // Start over };}Can i prevent navigation based on validation?
Section titled “Can i prevent navigation based on validation?”Yes! Validate before calling navigation functions:
function MyStep() { const { next, context } = useFlowState(); const [errors, setErrors] = useState({});
const handleNext = () => { const validationErrors = validate(context);
if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; // Don't navigate }
next(); };}