Todo App v5 (checkpoints)
In this version of the Todo app, we add a Checkpoints
object that provides us with an undo and redo stack as the main store changes.
We're making changes to the Todo App v4 (metrics) demo.
Additional Initialization
We will create a Checkpoints
object with the useCreateCheckpoints
hook, and will need the useCheckpoints
hook to use it throughout the application. The useSetCheckpointCallback
hook provides a callback to set a new checkpoint whenever something changes in the application that we would like to undo.
The useUndo hook and useRedo hook provide convenient ways to check whether an undo action or a redo action is available:
-const {createIndexes, createMetrics, createStore} = TinyBase;
+const {createCheckpoints, createIndexes, createMetrics, createStore} = TinyBase;
const {createLocalPersister, createSessionPersister} = TinyBasePersisterBrowser;
const {
CellView,
+ CheckpointView,
Provider,
SliceView,
useAddRowCallback,
useCell,
+ useCheckpoints,
+ useCreateCheckpoints,
useCreateIndexes,
useCreateMetrics,
useCreatePersister,
useCreateStore,
useMetric,
+ useRedoInformation,
+ useRow,
useSetCellCallback,
+ useSetCheckpointCallback,
useSetValueCallback,
+ useUndoInformation,
useValue,
} = TinyBaseUiReact;
As before, we make this Checkpoints
object available though the Provider
component as the default for the app. We choose to also clear the Checkpoints
object once the persister has completed its initial load so that there isn't the option to 'undo' that first load:
+ const checkpoints = useCreateCheckpoints(store, createCheckpoints);
useCreatePersister(
store,
(store) => createLocalPersister(store, 'todos/store'),
[],
async (persister) => {
await persister.startAutoLoad([INITIAL_TODOS]);
+ checkpoints?.clear();
await persister.startAutoSave();
},
+ [checkpoints],
);
And as before, we make this Checkpoints
object available though the Provider
component as the default for the app:
return (
<Provider
store={store}
storesById={{viewStore}}
indexes={indexes}
metrics={metrics}
+ checkpoints={checkpoints}
>
<Title />
<NewTodo />
<Types />
+ <UndoRedo />
<Todos />
<Inspector />
</Provider>
);
};
Upgrading useNewTodoCallback
We need to set checkpoints every time something happens in the app that the user might want to undo. One of the actions we want to checkpoint is when a user creates a new todo:
const NewTodo = () => {
const [text, setText] = useState('');
const type = useValue('type', 'viewStore');
const handleChange = useCallback(({target: {value}}) => setText(value), []);
+ const addCheckpoint = useSetCheckpointCallback(
+ () => `adding '${text}'`,
+ [text],
+ );
const handleKeyDown = useAddRowCallback(
'todos',
({which, target: {value: text}}) =>
which == 13 && text != '' ? {text, type} : null,
[type],
undefined,
- () => setText(''),
+ () => {
+ setText('');
+ addCheckpoint();
+ },
- [setText],
+ [setText, addCheckpoint],
);
Upgrading TodoType
When a type is changed for a todo, we also create a checkpoint:
const TodoType = ({tableId, rowId}) => {
const type = useCell(tableId, rowId, 'type');
+ const checkpoints = useCheckpoints();
const handleChange = useSetCellCallback(
tableId,
rowId,
'type',
({target: {value}}) => value,
[],
+ undefined,
+ (_store, type) => checkpoints.addCheckpoint(`changing to '${type}'`),
+ [checkpoints],
);
The sixth parameter is left undefined
so that the default store is used. Following that, we are using the 'then' callback that fires after the Cell
has been set. It depends on checkpoints
, so the final array parameter ensures it is memoized correctly.
Upgrading TodoText
And finally, we create a checkpoint whenever a todo is marked as completed or is being resumed. Our handleClick
function now calls both the previous setCell
function and a new addCheckpoint
function, both from hooks that gave us the default Store
and Checkpoint objects:
const TodoText = ({tableId, rowId}) => {
- const done = useCell(tableId, rowId, 'done');
+ const {done, text} = useRow(tableId, rowId);
const className = 'text' + (done ? ' done' : '');
- const handleClick = useSetCellCallback(tableId, rowId, 'done', () => !done, [
- done,
- ]);
+ const setCell = useSetCellCallback(tableId, rowId, 'done', () => !done, [
+ done,
+ ]);
+ const addCheckpoint = useSetCheckpointCallback(
+ () => `${done ? 'resuming' : 'completing'} '${text}'`,
+ [done],
+ );
+ const handleClick = useCallback(() => {
+ setCell();
+ addCheckpoint();
+ }, [setCell, addCheckpoint]);
return (
<span className={className} onClick={handleClick}>
<CellView tableId={tableId} rowId={rowId} cellId="text" />
</span>
);
};
The UndoRedo
Components
To provide a way to undo the checkpoints, we create two small affordances on the left of the application. Each is only enabled when there is at least one item to undo or redo:
const UndoRedo = () => {
const [canUndo, handleUndo, , undoLabel] = useUndoInformation();
const undo = canUndo ? (
<div id="undo" onClick={handleUndo}>
undo {undoLabel}
</div>
) : (
<div id="undo" className="disabled" />
);
const [canRedo, handleRedo, , redoLabel] = useRedoInformation();
const redo = canRedo ? (
<div id="redo" onClick={handleRedo}>
redo {redoLabel}
</div>
) : (
<div id="redo" className="disabled" />
);
return (
<div id="undoRedo">
{undo}
{redo}
</div>
);
};
We extend the grid slightly so these components are placed below the type lists, and add some styling:
body {
display: grid;
grid-template-columns: 35% minmax(0, 1fr);
- grid-template-rows: auto 1fr;
+ grid-template-rows: auto auto 1fr;
#undoRedo {
grid-column: 1;
grid-row: 3;
#undo,
#redo {
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
user-select: none;
&::before {
padding-right: 0.5rem;
vertical-align: middle;
}
&.disabled {
cursor: default;
opacity: 0.3;
}
}
#undo::before {
content: '\21A9';
}
#redo::before {
content: '\21AA';
}
}
Since we added an extra row to the grid we also make the right hand list of todos span two:
#todos {
grid-column: 2;
+ grid-row: 2 / span 2;
margin: 0;
padding: 0;
}
Summary
It's been very straightforward to add a comprehensive undo and redo stack to our app. We could now declare it complete! But wouldn't is also be cool to make it collaborative? If you agree, let's move on to the Todo App v6 (collaboration) demo...