0.1.0-alpha.7

Saving with effects

Reload the page and the list resets, because it lives only in memory. The fix is to read the list from the browser's storage when the app starts and write it back whenever it changes.

Writing to storage is a side effect: it reaches outside the page's own state. In zerolith, code that should run whenever some signals change is an effect.

Load, then save

Update src/store.ts:

// The packing list's state, kept in the browser's storage between visits.
import { computed, effect, signal } from "zerolith";

export interface Item
{
    id: number;
    name: string;
    packed: boolean;
}

export type Filter = "all" | "left" | "packed";

const storageKey = "packing-list";
const firstItems: Item[] = [
    { id: 1, name: "Passport", packed: true },
    { id: 2, name: "Phone charger", packed: false },
    { id: 3, name: "Rain jacket", packed: false }
];

function stored(): Item[]
{
    try
    {
        const text = localStorage.getItem(storageKey);
        return text === null ? firstItems : (JSON.parse(text) as Item[]);
    }
    catch
    {
        return firstItems;
    }
}

export const items = signal<Item[]>(stored(), { name: "items" });
export const filter = signal<Filter>("all", { name: "filter" });

effect(() =>
{
    const text = JSON.stringify(items.get());
    try
    {
        localStorage.setItem(storageKey, text);
    }
    catch
    {
        // Storage can be full or turned off; the list still works
        // for this visit.
    }
});

export const packedCount = computed(() =>
    items.get().filter((item) => item.packed).length);
export const shown = computed(() =>
{
    const which = filter.get();
    return which === "all"
        ? items.get()
        : items.get().filter((item) => item.packed === (which === "packed"));
});

export function toggle(id: number): void
{
    items.set(items.get().map((item) =>
        (item.id === id ? { ...item, packed: !item.packed } : item)));
}

export function add(name: string): void
{
    const id = Math.max(0, ...items.get().map((item) => item.id)) + 1;
    items.set([...items.get(), { id, name, packed: false }]);
}

export function remove(id: number): void
{
    items.set(items.get().filter((item) => item.id !== id));
}

stored() runs once, when the module loads, and gives items its starting value: the saved list if there is one, the three starting items if not. Reading storage can fail, in a private window with storage turned off or with damaged data, so a failure falls back to the starting items instead of breaking the page.

The effect runs once right away and then again each time a signal it read changes. It reads items, so every set() saves the new list. It never reads filter, so changing the filter saves nothing. You never list the dependencies by hand: an effect depends on exactly what it read the last time it ran.

Where effects belong

An effect at the top of a module, like this one, lasts as long as the page. An effect created inside a component belongs to that component and stops when the component is removed, so a page that closes takes its effects with it.

Open the page, check a few items off, and reload: the list is still there.

What you learned

  • effect runs code now and again whenever a signal it read changes.
  • Dependencies are tracked from what the code read; there is no list to keep up to date.
  • Effects are for side effects; what the page shows comes from bindings.