TinyBase logoTinyBase

Todo App v1 (the basics)

In this demo, we build a minimum viable 'Todo' app. It uses React and a simple Store to let people add new todos and then mark them as done.

Initialization

First, we pull in React, ReactDOM, and TinyBase:

<script src="/umd/react.production.min.js"></script>
<script src="/umd/react-dom.production.min.js"></script>
<script src="/umd/tinybase/index.js"></script>
<script src="/umd/tinybase/ui-react/index.js"></script>
<script src="/umd/tinybase/ui-react-inspector/index.js"></script>

We're using the Inspector component for the purposes of seeing how the data is structured.

We import the functions and components we need:

const {createStore} = TinyBase;
const {
  CellView,
  Provider,
  TableView,
  useAddRowCallback,
  useCell,
  useCreateStore,
  useSetCellCallback,
} = TinyBaseUiReact;
const {useCallback, useState} = React;
const {Inspector} = TinyBaseUiReactInspector;

In this demo, we'll start with some sample data. We'll have a single Table, called todos, that has three Row entries, each representing one todo:

const INITIAL_TODOS = {
  todos: {
    0: {text: 'Clean the floor'},
    1: {text: 'Install TinyBase'},
    2: {text: 'Book holiday'},
  },
};

The Top-Level App Component

We have a top-level App component, in which we create our Store object with the sample data, and render the parts of the app. We use the useCreateStore hook, since it provides memoization in case the component is rendered more than once.

We could drill the Store object as a React prop down to all the components, but for clarity we wrap our app with the Provider component, which then makes it available throughout our app as the default Store object:

const App = () => {
  const store = useCreateStore(() => createStore().setTables(INITIAL_TODOS));

  return (
    <Provider store={store}>
      <Title />
      <NewTodo />
      <Todos />
      <Inspector />
    </Provider>
  );
};

We also added the Inspector component at the end there so you can inspect what is going on with the data during this demo. Simply click the TinyBase logo in the corner.

The app only has three components: Title, NewTodo (a form to enter a new todo), and Todos (a list of all of the todos).

We use LESS to create a grid layout and some defaults for the app's styling:

@accentColor: #d81b60;
@spacing: 0.5rem;
@border: 1px solid #ccc;
@font-face {
  font-family: Inter;
  src: url(https://tinybase.org/fonts/inter.woff2) format('woff2');
}

body {
  display: grid;
  grid-template-columns: 35% minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  box-sizing: border-box;
  font-family: Inter, sans-serif;
  letter-spacing: -0.04rem;
  grid-gap: @spacing * 2 @spacing;
  margin: 0;
  min-height: 100vh;
  padding: @spacing * 2;
  * {
    box-sizing: border-box;
    outline-color: @accentColor;
  }
}

When the window loads, we render the App component to start the app:

window.addEventListener('load', () =>
  ReactDOM.createRoot(document.body).render(<App />),
);

Let's look at each of three components that make up the app.

The Title Component

There's not much to say about this component for now. It's just the title for the app. We'll do more with this later!

const Title = () => 'Todos';

The NewTodo Component

This component lets the user add a new todo. It's a standard managed input element that keeps the text of the input box in the component's state, and also listens to key presses:

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

  return (
    <input
      id="newTodo"
      onChange={handleChange}
      onKeyDown={handleKeyDown}
      placeholder="New Todo"
      value={text}
    />
  );
};

The useAddRowCallback hook creates us a callback that is run whenever the user presses a key. This lets us check if the key pressed was the Return key, and if there's some text in the input box. If so, it adds a new todo Row to the default Store object and then resets the form.

As an aside, it may seem like the useAddRowCallback hook has a lot of parameters, since we are providing two functions, each with their own lists of dependencies (which are used to memoize them). The first parameter is the Id of the table to which we are adding a row. The second is the handler that takes the event and produces the new row to be added and the third parameter is the list of dependencies for that function (of which there are none). The fourth parameter would allow us to explicitly specify which Store object to use, but undefined gives us the app-wide default. The fifth parameter is a 'then' callback which will run after the row has been added (which is where we can reset the input field) and the sixth and final parameter is a list of dependencies for that (of which there is only one, the function used to do that setting of the input field text).

The input box also needs styling:

#newTodo {
  border: @border;
  display: block;
  font: inherit;
  letter-spacing: inherit;
  padding: @spacing;
  width: 100%;
}

The Todos Component

To get the list of todos, we build a very simple component, comprising the TableView component right out of the box. We pass props to that component to indicate that we will be rendering each Row of the todos Table. Since we are not providing a store prop to the TableView, it will also use the default one from the Provider component that we wrapped the whole app in:

const Todos = () => (
  <ul id="todos">
    <TableView tableId="todos" rowComponent={Todo} />
  </ul>
);

The final rowComponent prop for the TableView component allows us to specify how each Row is rendered. We will have our own Todo component (described below) to do this.

Finally, we style this part of the app and position it in the grid:

#todos {
  grid-column: 2;
  margin: 0;
  padding: 0;
}

The Todo Component

This simple Todo component renders a simple div containing the todo's text that toggles the done flag when clicked. Each Row has two fields: the todo's text and an optional boolean indicating whether it is done that we get using the useCell hook.

We also create a simple handleClick callback that negates the done flag when the text is clicked:

const Todo = (props) => (
  <li className="todo">
    <TodoText {...props} />
  </li>
);

const TodoText = ({tableId, rowId}) => {
  const done = useCell(tableId, rowId, 'done');
  const className = 'text' + (done ? ' done' : '');
  const handleClick = useSetCellCallback(tableId, rowId, 'done', () => !done, [
    done,
  ]);

  return (
    <span className={className} onClick={handleClick}>
      <CellView tableId={tableId} rowId={rowId} cellId="text" />
    </span>
  );
};

NB: The parent TableView component passes its Store object as a prop to each of its rowComponent children (such as this TodoText). We could have used that prop here, but instead we're using the useSetCellCallback hook that will automatically get it from the Provider component.

Finally, we style each of these todos, and use the done CSS class to strike through those that are completed:

#todos .todo {
  background: #fff;
  border: @border;
  display: flex;
  margin-bottom: @spacing;
  padding: @spacing;
  .text {
    cursor: pointer;
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    user-select: none;
    white-space: nowrap;
    &::before {
      content: '\1F7E0';
      padding: 0 0.5rem 0 0.25rem;
    }
    &.done {
      color: #ccc;
      &::before {
        content: '\2705';
      }
    }
  }
}

Summary

That's it: a simple Todo app made from a handful of tiny components, and less than 100 lines of generously-formatted code.

Next, we will build a more complex viable 'Todo' app. Please continue to the Todo App v2 (indexes) demo.