@tour-kit/checklistsHooks
useChecklist
useChecklist hook: manage task completion, track overall progress, and control checklist state with toggle and reset actions
useChecklist
The primary hook for accessing and controlling checklist state.
Usage
import { useChecklist } from '@tour-kit/checklists';
function OnboardingChecklist() {
const {
checklist,
visibleTasks,
progress,
isComplete,
completeTask,
executeAction,
} = useChecklist('onboarding');
if (!checklist) return null;
return (
<div>
<h2>{checklist.config.title}</h2>
<progress value={progress.completed} max={progress.total} />
{visibleTasks.map((task) => (
<button
key={task.config.id}
onClick={() => executeAction(task.config.id)}
disabled={task.locked}
>
{task.completed ? '✓' : '○'} {task.config.title}
</button>
))}
</div>
);
}Parameters
Prop
Type
Return Value
Prop
Type
Basic Examples
Display Checklist Progress
function ChecklistHeader() {
const { checklist, progress, isComplete } = useChecklist('onboarding');
if (!checklist) return null;
return (
<div>
<h3>{checklist.config.title}</h3>
<p>{checklist.config.description}</p>
<div className="progress-bar">
<div style={{ width: `${progress.percentage}%` }} />
</div>
<p>
{progress.completed} of {progress.total} completed
{isComplete && ' 🎉'}
</p>
</div>
);
}Task List
function TaskList() {
const { visibleTasks, completeTask } = useChecklist('onboarding');
return (
<ul>
{visibleTasks.map((task) => (
<li key={task.config.id}>
<input
type="checkbox"
checked={task.completed}
disabled={task.locked}
onChange={() => completeTask(task.config.id)}
/>
<span>{task.config.title}</span>
{task.locked && <span>🔒</span>}
</li>
))}
</ul>
);
}With Actions
function ActionableChecklist() {
const { visibleTasks, executeAction } = useChecklist('onboarding');
return (
<div>
{visibleTasks.map((task) => (
<div key={task.config.id}>
<h4>{task.config.title}</h4>
<p>{task.config.description}</p>
<button
onClick={() => executeAction(task.config.id)}
disabled={task.locked || task.completed}
>
{task.completed ? 'Completed ✓' : 'Start'}
</button>
</div>
))}
</div>
);
}Advanced Examples
Dismissible Checklist
function DismissibleChecklist() {
const {
checklist,
isDismissed,
dismiss,
restore,
isComplete,
} = useChecklist('onboarding');
if (!checklist || isDismissed) {
return (
<button onClick={restore}>
Show onboarding checklist
</button>
);
}
return (
<div>
<h3>{checklist.config.title}</h3>
{/* Checklist content */}
{checklist.config.dismissible && !isComplete && (
<button onClick={dismiss}>
Dismiss
</button>
)}
</div>
);
}Collapsible Checklist
function CollapsibleChecklist() {
const {
checklist,
visibleTasks,
progress,
isExpanded,
toggleExpanded,
completeTask,
} = useChecklist('onboarding');
if (!checklist) return null;
return (
<div>
<button onClick={toggleExpanded}>
{checklist.config.title}
<span>{progress.completed}/{progress.total}</span>
<span>{isExpanded ? '▼' : '▶'}</span>
</button>
{isExpanded && (
<div>
{visibleTasks.map((task) => (
<div key={task.config.id}>
<input
type="checkbox"
checked={task.completed}
onChange={() => completeTask(task.config.id)}
disabled={task.locked}
/>
{task.config.title}
</div>
))}
</div>
)}
</div>
);
}Reset Functionality
function ChecklistWithReset() {
const { checklist, progress, isComplete, reset } = useChecklist('onboarding');
if (!checklist) return null;
return (
<div>
<h3>{checklist.config.title}</h3>
<p>{progress.completed} of {progress.total} completed</p>
{/* Checklist content */}
{isComplete && (
<div>
<p>All done! 🎉</p>
<button onClick={reset}>Start over</button>
</div>
)}
</div>
);
}Custom Progress Display
function CustomProgress() {
const { progress, visibleTasks } = useChecklist('onboarding');
const completedTasks = visibleTasks.filter((t) => t.completed);
const nextTask = visibleTasks.find((t) => !t.completed && !t.locked);
return (
<div>
<h4>Your Progress</h4>
{/* Percentage */}
<div className="circular-progress">
{Math.round(progress.percentage)}%
</div>
{/* Counts */}
<p>
{progress.completed} completed,{' '}
{progress.remaining} remaining
</p>
{/* Next task */}
{nextTask && (
<div>
<p>Up next: {nextTask.config.title}</p>
</div>
)}
{/* Completed list */}
{completedTasks.length > 0 && (
<details>
<summary>Completed tasks ({completedTasks.length})</summary>
<ul>
{completedTasks.map((task) => (
<li key={task.config.id}>
✓ {task.config.title}
</li>
))}
</ul>
</details>
)}
</div>
);
}Handling Missing Checklists
Always check if the checklist exists:
function SafeChecklist() {
const { exists, checklist } = useChecklist('my-checklist');
if (!exists) {
return (
<div>
<p>Checklist not found</p>
<p>Make sure "my-checklist" is in your provider config</p>
</div>
);
}
return (
<div>
{/* Safe to use checklist */}
<h3>{checklist.config.title}</h3>
</div>
);
}Task State Properties
Each task in tasks and visibleTasks has:
interface ChecklistTaskState {
config: ChecklistTaskConfig; // Original config
completed: boolean; // Completion status
locked: boolean; // Whether dependencies are met
visible: boolean; // Whether when condition passed
active: boolean; // Currently in progress (future)
completedAt?: number; // Timestamp of completion
}Example: Show Lock Reason
function TaskWithLockReason() {
const { tasks } = useChecklist('onboarding');
return (
<>
{tasks.map((task) => {
const deps = task.config.dependsOn ?? [];
const unmetDeps = deps.filter((depId) => {
const dep = tasks.find((t) => t.config.id === depId);
return dep && !dep.completed;
});
return (
<div key={task.config.id}>
<h4>{task.config.title}</h4>
{task.locked && unmetDeps.length > 0 && (
<p className="text-muted">
🔒 Complete these first:{' '}
{unmetDeps.map((id) => {
const dep = tasks.find((t) => t.config.id === id);
return dep?.config.title;
}).join(', ')}
</p>
)}
</div>
);
})}
</>
);
}Progress Calculation
The progress object provides:
interface ChecklistProgress {
completed: number; // Number of completed tasks
total: number; // Total visible tasks
percentage: number; // 0-100
remaining: number; // total - completed
}Progress is calculated from visible tasks only. Hidden tasks (failing when conditions) don't count.
Example: Progress Stages
function ProgressStages() {
const { progress } = useChecklist('onboarding');
let stage = 'not-started';
if (progress.percentage === 100) stage = 'complete';
else if (progress.percentage >= 50) stage = 'halfway';
else if (progress.percentage > 0) stage = 'started';
const messages = {
'not-started': 'Ready to begin?',
'started': 'Great start! Keep going!',
'halfway': "You're halfway there!",
'complete': 'All done! 🎉',
};
return (
<div>
<p>{messages[stage]}</p>
<progress value={progress.completed} max={progress.total} />
</div>
);
}Best Practices
Memoize Expensive Computations
function ChecklistStats() {
const { tasks } = useChecklist('onboarding');
const stats = useMemo(() => ({
total: tasks.length,
completed: tasks.filter((t) => t.completed).length,
locked: tasks.filter((t) => t.locked).length,
visible: tasks.filter((t) => t.visible).length,
}), [tasks]);
return <div>{/* Use stats */}</div>;
}Avoid Stale Closures
// Bad: Capturing stale completeTask in setTimeout
function BadExample() {
const { completeTask } = useChecklist('onboarding');
const handleClick = (taskId: string) => {
setTimeout(() => {
completeTask(taskId); // May be stale
}, 5000);
};
}
// Good: Use stable ref or latest value
function GoodExample() {
const { completeTask } = useChecklist('onboarding');
const completeTaskRef = useRef(completeTask);
useEffect(() => {
completeTaskRef.current = completeTask;
}, [completeTask]);
const handleClick = (taskId: string) => {
setTimeout(() => {
completeTaskRef.current(taskId);
}, 5000);
};
}Related
- useTask - Control individual tasks
- useChecklistPersistence - Manage persistence
- ChecklistProvider - Provider configuration