Dark mode

TodoMVC - LocalStorage

Let's store & load todos from LocalStorage!

Persistence

Your app should dynamically persist the todos to localStorage. If the framework has capabilities for persisting data (e.g. Backbone.sync), use that. Otherwise, use vanilla localStorage. If possible, use the keys id, title, completed for each item. Make sure to use this format for the localStorage name: todos-[framework]. Editing mode should not be persisted.

  • We need a new dependency serde to serialize and deserialize todos to/from JSON because we can store only JSON strings in LocalStorage.

  • serde has built-in support for BTreeMap and many other common Rust items. However containers like BTreeMap are de/serializable only when all their items are also de/serializable. In our case it means we need to enable serde support for Ulid and Todo. Fortunately ulid crate has built-in serde support - we just need to enable the required feature "serde". You'll find available features in the crate docs or you can look at Cargo.toml or search through issues. Enabling serde support for the most custom items (like our Todo struct) is easy - just derive Deserialize and Serialize.

Cargo.toml:

[dependencies]
serde = "1.0.112"
strum = "0.18.0"
strum_macros = "0.18.0"
ulid = { version = "0.3.3", features = ["serde"]
...

lib.rs:

use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
...
const ESCAPE_KEY: &str = "Escape";

const STORAGE_KEY: &str = "todos-seed";
...

fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
    Model {
        todos: LocalStorage::get(STORAGE_KEY).unwrap_or_default(),
...

#[derive(Deserialize, Serialize)]
    struct Todo {
...

fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        ...
    }
    LocalStorage::insert(STORAGE_KEY, &model.todos).expect("save todos to LocalStorage");
...

Note: Yes, we insert todos into LocalStorage on each message. I don't see any performance problems like freezing UI or annoying delays during typing. So I don't want to resolve non-existent issues. And less code means less bugs. However when it becomes a problem, there are some potential solutions:

  1. Update LocalStorage todos only in some match arms.

    • It would help, but it would be error-prone - you'll forget to add the updating code in a new/updated arm sooner or later. Also it would introduce boilerplate and therefore reduce readability.
  2. You can save todos hash into Model (BTreeMap implements Hash) and generate a new one on each message. You'll update LocalStorage todos only if those hashes are different. (See how to calculate hash in the example unsaved_changes.)

    • I assume that serialization, data transfer from Rust to JS world and saving into LocalStorage are the bottleneck. If hashing is slow, we would make it worse. It would need benchmarks.
  3. Write / pick a smarter container instead of BTreeMap (or write a wrapper). It would allow you to implement synchronization with LocalStorage and mitigate problems from the solution 1).

  4. Apply debouncing or throttling to LocalStorage updates.

  5. Integrate manual saving and show something like "Do you want to leave? Data won't be saved." when the user wants to leave/close browser tab (see example unsaved_changes to learn how to implement it).

    • It would reduce UX in our TodoMVC, however there are use-cases where it would improve UX.
    • It would make the app less robust - there is a higher probability of losing changes.
  6. Compress stored data.

    1. Currently id is saved twice per each todo - BTreeMap key and id in Todo struct. We can save it as [[id, title, completed], ..] instead.

    2. Then, we can apply a compressing algorithm and save it as a big string.

    3. It would need benchmarks to prove that additional computations mitigate slow transfer and saving.


I hope you are as happy as me - the app is working and LocalStorage integration wasn't too hard. Let's learn about routing and finish the app by proper filter implementation.