TinyBase logoTinyBase β

Todo App v2 (indexes)

In this demo, we build a more complex 'Todo' app. In addition to what we built in Todo App v1 (the basics), we let people specify a type for each todo, such as 'Home', 'Work' or 'Archived'.

We also index those types with an Indexes object so that people can see their todos filtered by each type.

We're making changes to the Todo App v1 (the basics) demo.

Additional Initialization

We'll be creating an Indexes object in this demo, so we'll need an additional import, the useCreateIndexes hook. We'll also use a SliceView component to display the index, instead of the simple TableView component that we used before. We'll be using a Value for the view state, so we'll also import the useSetValueCallback hook and useValue hook.

-const {createStore} = TinyBase;
+const {createIndexes, createStore} = TinyBase;
 const {
   CellView,
   Provider,
-  TableView,
+  SliceView,
   useAddRowCallback,
   useCell,
+  useCreateIndexes,
   useCreateStore,
   useSetCellCallback,
+  useSetValueCallback,
+  useValue,
 } = TinyBaseUiReact;

We're defining a list of the types a todo can have, and giving our default todos each a different initial type:

+const TYPES = ['Home', 'Work', 'Archived'];
 const INITIAL_TODOS = {
   todos: {
-    0: {text: 'Clean the floor'},
+    0: {text: 'Clean the floor', type: 'Home'},
-    1: {text: 'Install TinyBase'},
+    1: {text: 'Install TinyBase', type: 'Work'},
-    2: {text: 'Book holiday'},
+    2: {text: 'Book holiday', type: 'Archived'},
   },
 };

Adding Additional Stores And Indexes

In this demo we let people select a todo type and see a filtered list. The current type being displayed will need to be known by components across the app. We could make this a part of the top level component's state and pass it around with props.

But instead, we will create and memoize a second Store object called viewStore to store the current type being viewed, in a Value called type.

We also want to index the todos by type, so we create and memoize an Indexes object, and define an index called types on the todos Table, based on the value of the type Cell:

 const App = () => {
   const store = useCreateStore(() => createStore().setTables(INITIAL_TODOS));
+  const viewStore = useCreateStore(() =>
+    createStore().setValue('type', 'Home'),
+  );
+  const indexes = useCreateIndexes(store, (store) =>
+    createIndexes(store).setIndexDefinition('types', 'todos', 'type'),
+  );

   return (
-    <Provider store={store}>+    <Provider store={store} storesById={{viewStore}} indexes={indexes}>       <Title />
       <NewTodo />
+      <Types />
       <Todos />
       <Inspector />
     </Provider>
   );
 };

Notice that we pass the new viewStore and indexes down into the app using the same Provider that we used for the store in Todo App v1. We need to pass viewStore in the storesById prop so we can refer to it explicitly for the components that need it (to disambiguate it from the default Store object that we provided in the store prop).

We also added a new component to the app called Types. This is a side-bar that lists the types so people can pick one and view the filtered Todos list for it.

The Types Component

This new component goes on the left-hand side of the demo and lists the available types. When people click a type name, the current type will be set in the viewStore and the list on the right will be filtered accordingly. (Additionally, a new todo will be set to have this current type when it's added.)

The component literally just enumerates the TYPES array and creates a Type component for each one:

const Types = () => (
  <ul id="types">
    {TYPES.map((type) => (
      <Type key={type} type={type} />
    ))}
  </ul>
);
#types {
  margin: 0;
}

The Type Component

In the Types component, each type appears as a clickable name. The viewStore provides the currently selected type, and if it matches, this component will have an additional CSS class added to it.

If the component is clicked, the viewStore's value will be updated with a callback provided by the useSetValueCallback hook:

const Type = ({type}) => {
  const currentType = useValue('type', 'viewStore');
  const handleClick = useSetValueCallback(
    'type',
    () => type,
    [type],
    'viewStore',
  );
  const className = 'type' + (type == currentType ? ' current' : '');

  return (
    <li className={className} onClick={handleClick}>
      {type}
    </li>
  );
};

NB: In this example, we are setting up one listener on the viewStore for every instance of the Type component in the side bar. This makes the Type components completely self-sufficient. An alternative approach would be to use the useCell hook once in the parent Types component and pass down the current type as a prop to each item. We would then pass a parameter to the useSetCellCallback hook to set the value based on the item clicked.

Which of these two approaches is optimal in the general case will depend on the number and complexity of the children components being rendered. For example we will do something similar to this in the Countries demo, which has a longer list of items in the side bar).

The Type component has a small amount of styling:

#types .type {
  cursor: pointer;
  margin-bottom: @spacing;
  user-select: none;
  &.current {
    color: @accentColor;
  }
}

Upgrading The Todos Component

Previously we used a TableView component to list all the todos in the todos Table of the store. But now we want to show only the todos of the current type. We created an Indexes object (called indexes) that has an index called types. Within that is one slice per type, which we can render with a SliceView component.

The slices in an index are simply sets of Row Ids from a Table grouped according to the index's definition. Often - as here - these are sets of Row Ids that share a particular Cell value.

We simply need to change the Todos component to fetch the current type from the viewStore, and then pass the corresponding index slice to the SliceView component. It still uses the Todo component to render each Row itself:

 const Todos = () => (
   <ul id="todos">-    <TableView tableId="todos" rowComponent={Todo} />
+    <SliceView
+      indexId="types"
+      sliceId={useValue('type', 'viewStore')}
+      rowComponent={Todo}
+    />
   </ul>
 );

Upgrading The Todo Component

Since the todo has the new type Cell, we can display that alongside the text. In fact, we want to let people change the type for each todo too, so we implement a new component called TodoType that contains as a select dropdown. It has a callback to update the todo Row with the value from the select element if it is changed:

 const Todo = (props) => (
   <li className="todo">
     <TodoText {...props} />+    <TodoType {...props} />
   </li>
 );
const TodoType = ({tableId, rowId}) => {
  const type = useCell(tableId, rowId, 'type');
  const handleChange = useSetCellCallback(
    tableId,
    rowId,
    'type',
    ({target: {value}}) => value,
    [],
  );

  return (
    <select className="type" onChange={handleChange} value={type}>
      {TYPES.map((type) => (
        <option>{type}</option>
      ))}
    </select>
  );
};

We can style the select element so that it appears on the right of the Todo component:

#todos .todo .type {
  border: none;
  color: #777;
  font: inherit;
  font-size: 0.8rem;
  margin-top: 0.1rem;
}

Upgrading the NewTodo Component

Our final step is to make sure that when someone adds a new todo it defaults to the current type from the viewStore - if only so that the newly-created todo appears in the current IndexView component:

 const NewTodo = () => {
   const [text, setText] = useState('');
+  const type = useValue('type', 'viewStore');
   const handleChange = useCallback(({target: {value}}) => setText(value), []);
   const handleKeyDown = useAddRowCallback(
     'todos',
     ({which, target: {value: text}}) =>
-      which == 13 && text != '' ? {text} : null,
+      which == 13 && text != '' ? {text, type} : null,
-    [],
+    [type],
     undefined,
     () => setText(''),
     [setText],
   );

Note how the current type is now listed as a dependency for the handler function so that that function is correctly memoized.

Summary

And again, that's it: a fairly small set of changes to make the app a little more useful. But there's more!

Next, we will build a yet more complex 'Todo' app, complete with persistence, a schema, and metrics. Please continue to the Todo App v3 (persistence) demo.