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:
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 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 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 - the app is working and LocalStorage
integration wasn't too hard. Let's learn about routing and finish the app by proper filter implementation.