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 called serde to serialize and deserialize todos to and from JSON since we can only store 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. For our case, this means we need to enable serde
support for Ulid
and Todo
. Fortunately the 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 the UI freezing or annoying delays during typing. Less code means less bugs and issues should be resolved only when they arise. However, if it becomes a problem, there are some potential solutions:
Update LocalStorage
todos only in some match
arms.
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
.)
LocalStorage
are the bottleneck. If hashing is slow, we would make it worse. It would need benchmarks.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).
Apply debouncing or throttling to LocalStorage
updates.
Integrate manual saving and show something like "Do you want to leave? Data won't be saved." when the user wants to leave/close the browser tab (see example unsaved_changes to learn how to implement it).
Compress stored data.
Currently id
is saved twice per each todo - BTreeMap
key and id
in the Todo
struct. We can save it as [[id, title, completed], ..]
instead.
Then, we can apply a compressing algorithm and save it as a big string.
It would need benchmarks to prove that additional computations mitigate slow transfer and saving.
I hope you are as happy as me - our app is working and LocalStorage
integration wasn't too hard. Let's learn about routing and finish our app by proper filter implementation.