Skip to content
⚠️ Beta: API may change before v1.0. Pin to ~0.x.0 to avoid breaking changes.

Troubleshooting

This guide covers common issues you might encounter with useFlow and how to solve them.

”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 assignable
const stepName = "profile";
next(stepName);

Solution: Use as const or type the variable:

// ✅ Solution 1: as const
const stepName = "profile" as const;
next(stepName);
// ✅ Solution 2: Type the variable
const 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 exist
const { 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 parameter
const { context } = useFlowState<MyContextType>();

Problem: Flow state isn’t being saved to storage.

Common causes and solutions:

  1. No persister configured:
// ❌ Missing persister
<Flow flow={myFlow}>
// ✅ Add persister
import { createLocalStorageStore, createPersister } from '@useflow/react';
const persister = createPersister({ store: createLocalStorageStore() });
<Flow
flow={myFlow}
persister={persister}
>
  1. Manual save mode without calling save():
// If using saveMode="manual"
const { save } = useFlowState();
// Remember to call save() when needed
const handleSave = async () => {
await save();
};
  1. Storage quota exceeded:
<Flow
flow={myFlow}
persister={persister}
onPersistenceError={(error) => {
console.error('Storage error:', error);
// Handle storage quota or access issues
}}
>

Problem: Saved state exists but isn’t being restored.

Solutions:

  1. Check if restoration is in progress:
function MyStep() {
const { isRestoring } = useFlowState();
if (isRestoring) {
return <LoadingSpinner />;
}
// Your component content
}
  1. 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
>
  1. Check for version mismatches:
// If flow structure changed, old state might be invalid
const 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;
},
}));

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>
);
}

Problem: Calling next() but nothing happens.

Common causes:

  1. Step has no next defined (terminal step):
// Check your flow definition
steps: {
complete: {
} // Terminal step - no next
}
// Check if next is available
const { nextSteps } = useFlowState();
if (!nextSteps) {
console.log("This is a terminal step");
}
  1. Branching step requires target:
// For branching steps, must specify target
steps: {
choice: {
next: ["option1", "option2"];
}
}
// ❌ Error: Must specify target
next();
// ✅ Correct: Specify target
next("option1");

Problem: Setting context but values don’t change.

Common causes:

  1. Mutating context directly:
// ❌ Don't mutate directly
const { context, setContext } = useFlowState();
context.name = "Alice"; // Won't trigger update
// ✅ Use setContext
setContext({ name: "Alice" });
  1. Async updates not handled:
// ❌ State might be stale
const handleSubmit = async () => {
await saveData();
setContext({ saved: true });
next(); // Might navigate before context updates
};
// ✅ Update context in next() call
const handleSubmit = async () => {
await saveData();
next({ saved: true }); // Context and navigation together
};

Problem: TypeScript errors when updating context.

Solution: Define your context type properly:

// Define complete context type
type MyContext = {
name: string;
email?: string; // Optional fields
preferences: {
theme: "light" | "dark";
};
};
// Use with your flow
const flow = defineFlow({
// ...
});
// 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 });
}}
>
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>
);
}
  1. Check localStorage:
// In browser console
localStorage.getItem("useflow:myFlow:default:default");
  1. Clear saved state:
// Clear specific flow
localStorage.removeItem("useflow:myFlow:default:default");
// Clear all useFlow data
Object.keys(localStorage)
.filter((key) => key.startsWith("useflow:"))
.forEach((key) => localStorage.removeItem(key));

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]);
}
  1. Check the console for error messages
  2. Verify your flow definition is correct
  3. Try the debug techniques above
  4. Check if you’re using the latest version
  5. Create a minimal reproduction of the issue

When reporting an issue, include:

  1. useFlow version
  2. React version
  3. TypeScript version (if applicable)
  4. Minimal code example that reproduces the issue
  5. Error messages from the console
  6. 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:**
\`\`\`tsx
const flow = defineFlow({
id: "test",
steps: { /_ ... _/ }
});
<Flow flow={flow} persister={createPersister({ store: createLocalStorageStore() })}>
{/_ ... _/}
</Flow>
\`\`\`
**Expected:** Context should persist
**Actual:** Context resets on refresh
**Console errors:** None

Can 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>

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();
};
}