---
title: "Save to a file"
audience: "anyone whose terminal tool keeps data between runs"
description: "You'll read the task list from a JSON file under your home directory and write it back on every change. A missing file starts the list with a default."
snippet_caption: "app/routes/page.loader.ts"
snippet: "function read(where: string): Task[] {\n  try {\n    return JSON.parse(file.read(where));\n  } catch (error) {\n    if (error instanceof Error && error.code === \"ENOENT\") return STARTER;\n    throw error;\n  }\n}"
---

import { Install } from "@src/ui/Install";

You'll read the task list from a JSON file under your home directory and write it back on every change. A missing file starts the list with a default. It starts from the app you built in [620](https://snapfirers.com/fsr/learn/620-read-the-command-line).

<Install captured="0.x" />

## The machine comes from `std`

A terminal app's page and loader read the machine through `file` and `path` from `@snapfire/fsr/host` and `system` from `@snapfire/fsr/terminal`:

| Member | What it has |
| --- | --- |
| `file` | `read`, `write`, `exists`, `list`, `makeDir` and `stat` |
| `path` | `join`, `resolve`, `normalize`, `dirname`, `basename`, `extname`, `relative` and `isAbsolute` |
| `system` | `env(name)`, `home()`, `platform()`, `tempDir()`, `lineEnd()`, `cwd()` and `memory()` |

They take the place of Node's `fs`, `path` and `os`, which a terminal app cannot import. Each one answers at once, so you call it without `await`. A web page cannot use them, since a browser has no files.

## Read the file in the loader

The loader from 620 runs once, before the first frame, so it is where the list is read. It works out the file's path from the name, reads the list and hands both to the page:

```ts title="app/routes/page.loader.ts"
import type { Ctx } from "@snapfire/fsr/host";
import { file, path } from "@snapfire/fsr/host";
import { system } from "@snapfire/fsr/terminal";

type Task = { title: string; done: boolean };

const STARTER: Task[] = [{ title: "Write the first task", done: false }];

function read(where: string): Task[] {
  try {
    return JSON.parse(file.read(where));
  } catch (error) {
    if (error instanceof Error && error.code === "ENOENT") return STARTER;
    throw error;
  }
}

export async function load({ params }: Ctx<"/">) {
  const name = params.file ?? "tasks.json";
  const where = path.join(system.home(), ".tasks", name);
  return { name, where, tasks: read(where), limit: Number(params.limit ?? "10") };
}
```

`file.read` of a path with nothing there throws an error whose `code` is `ENOENT`, as Node's does. The `catch` turns that one error into the starter list and throws any other, so a file you cannot read is not silently replaced.

## Save from the page

The `START` list from 610 goes away. The page starts from the tasks the loader read and writes the list back on every change. Add `save` above the component:

```tsx title="app/routes/page.tsx"
import { useState } from "react";
import type { RootProps } from "@generated/client";
import { file, path } from "@snapfire/fsr/host";
import { Box, Newline, Spacer, Text, useInput } from "ink";

type Task = { title: string; done: boolean };

function save(where: string, tasks: Task[]) {
  file.makeDir(path.dirname(where), { recursive: true });
  file.write(where, JSON.stringify(tasks, null, 2));
}
```

`file.makeDir` without `recursive` fails when the directory is already there, as Node's `mkdirSync` does. With `{ recursive: true }` it makes the parents and leaves an existing directory alone.

```tsx title="app/routes/page.tsx" {1-2,9-11}
export default function Tasks({ name, where, tasks: initial, limit }: RootProps) {
  const [tasks, setTasks] = useState(initial);
  const [selected, setSelected] = useState(0);

  useInput((input, key) => {
    if (key.upArrow && selected > 0) setSelected(selected - 1);
    if (key.downArrow && selected < shown.length - 1) setSelected(selected + 1);
    if (input === " ") {
      const next = tasks.map((task, index) => (index === selected ? { ...task, done: !task.done } : task));
      setTasks(next);
      save(where, next);
    }
  });

  const open = tasks.filter((task) => !task.done).length;
  const shown = tasks.slice(0, limit);
```

The rest of the component is the same as in 620.

The handler saves `next`, the list it just made. Saving `tasks` would write the list as it was before the key.

## Run it

```shell-session
$ fsr dev app --terminal
```

The first time there is no `~/.tasks/tasks.json`, so the list starts with the starter task:

```text
╭──────────────────────────────────────────╮
│ Tasks tasks.json                  1 open │
│                                          │
│ > Write the first task                   │
│                                          │
│ up and down move, space marks one done   │
│ Ctrl-C quits                             │
╰──────────────────────────────────────────╯
```

Press space and quit. The directory and the file are there now:

```shell-session
$ cat ~/.tasks/tasks.json
[
  {
    "title": "Write the first task",
    "done": true
  }
]
```

Run it again and the task opens as done, with `0 open` in the header. `--file work.json` keeps a second list in `~/.tasks/work.json`.

Next up: [640. Type into the app](https://snapfirers.com/fsr/learn/640-type-into-the-app).
