Persisting React State in LocalStorage
joshwcomeau.com
joshwcomeau.com
Having a setSticky((k,v) => that modifies the url with the history API is pretty easy, and if you get the results using the Context API, you can have your root monitor the URL (again using the history API) and publish the result using a context so you don't have a storm of updates.
In the case of something like a blog, though, you anticipate folks sharing the URL. It'd be kinda strange to inadvertently share persisted settings (or worse, stuff that's maybe a little personal)
Surely those things should live in the application, no?
Other things like blog gadgets to choose different themes (or whatever) probably should live in the URL to facilitate bug reports! I think Internet users are savvy enough to remove that stuff from a URL when sharing on HN (and if not, the mods seem to).
Also, I think it's pretty clear I'm talking about navigation and queries in the above though, just so there's no other confusion: I definitely suggest continuing to use setState or ref for input boxes!
I think some old browsers didn't, so I guess that's why there are all those tacky switches on blogs these days, but I don't usually worry about obsolete things like "made for netscape 4" banners...
I also think for an application, I'd much prefer my settings on the server so I can get them again when I'm using another device.
But if you wanted to use other applications that didn't have such a switch, you might consider looking into a hotkey or an extension. I found this[1] after a quick ddg. Maybe it'll help?
[1]: https://extensions.gnome.org/extension/2314/dark-mode-switch...
I know that for performance reasons, or for privacy reasons, or even for time/effort reasons, that's not always possible, but I'm also always genuinely hopeful that there's some new class of data out there that doesn't fall into that (general) rule.
There are whole classes of UI errors that are only reproducible if you use the same look and feel as the customer. Black text on a black background being reported as data loss or missing functionality, for starters, but there are many others.
And you want screen resolution and DPI too for a proper "same look and feel" debugging experience. Which you could always hide in the URL if you want, but you shouldn't act on it by default. Do the same thing with UI settings, if you must. Include them but don't act on them.
I think you really need setState to have some idea who might be interested in this state transition. One idea I had was to have an optional argument to specify the "Persistor" [sic], and then I consult the "Reader" in the constructor to build the initial (local) state, and in for own code, I ended up with something pretty much like that.
this.setState({ fields, row: 0 }, UpdateURL)
where: setState(x,y) { return y&&y(x)&&super.setState(x); }
I haven't had any need for multiple "Persistors" [sic], nor any idea what multiple "Readers" might do, but you should find it easy to add a pub/sub mechanism in there if you needed it. import ViewModel from 'statium';
import stateToUri from 'urlito';
const defaultState = {
foo: 'bar',
qux: {
time: Date.now(),
},
};
const [getStateFromUri, setStateToUri] = stateToUri(defaultState, [
'foo',
{
key: 'qux.time',
uriKey: 'time',
fromUri: time => parseInt(time, 10),
toUri: time => String(time),
},
});
const Component = () => (
<ViewModel initialState={getStateFromUri}
observeStateChange={setStateToUri}>
{/* ... */}
</ViewModel>
);How does changing the URL trigger a state change? Does the whole page need to reload?
In my system, my "Reader" knows this object so it can just call the regular setState (I also have some mount/umount logic there to avoid leaking memory). This makes back/forward transitions very snappy!
I can also use the same logic for my Server (another "Persistor" [sic]) which indeed uses POST for submitting updates, but responses come down a shared SSE stream (which I need anyway so that users can see eachothers changes in real-time).
Sorry, I should have been more forthcoming with explanations but got paged at $work. Almost nobody is familiar with Statium yet, it's very new. :) It was developed for cloud applications UI here at DataStax.
Statium implements a very simple key/value storage in a React component called ViewModel. It is using `setState()` internally, so all the usual React rendering logic applies unchanged. Each ViewModel has access to keys of its ancestors, all the way up the chain. There is no global store but a chain of stores instead, which helps to keep state local to consumer components that use it.
Urlito is just a simple library for persisting state to and from URI, currently using query params. This is intended for local component state like selected tab, sort order in a table, or a list of expanded rows in a tree grid, the sort of things that do not deserve full blown URI routing pattern matching. We still use `react-router-dom` for that.
> How does changing the URL trigger a state change? Does the whole page need to reload?
No, it's the usual React logic: when ViewModel renders it will call the function provided in `initialState` prop, which in turn will read the current state of the model from URI query string. Whenever ViewModel state changes, `observeStateChange` function is called, and updates URI to reflect the current model state.
Urlito implements the functions for reading keys/values from URI and writing them back to URI, with support for default values and key filtering.
> In my system, my "Reader" knows this object so it can just call the regular setState (I also have some mount/umount logic there to avoid leaking memory). This makes back/forward transitions very snappy!
Almost the same solution in Statium, except that we have a full blown class based React component to hold the state, and hooks are not used internally (there is a hook based consumer API). The main reason for not using hooks for us is that the values held in `useState` are hard to propagate down the component tree, and hard to test. ViewModel takes care of this easily, with each key available anywhere downstream, e.g. a Component somewhere deep can retrieve the value of the topmost ViewModel without having to access it directly. This helps mightily with testing too: just wrap your tested components with a ViewModel and pass whatever you want to it (including state changes).
No memory leak issues at all, since a ViewModel is simply a React component that outsources `this.setState` for consumer components. :)
Transitions are very snappy with Statium, too. In fact, state updates are lighting fast: value updater function will walk up the ViewModel tree, find the closest owner and set the value in it using `setState`. This will cause the owner ViewModel and its children to re-render, but the render will be automatically scoped to the least amount of components. Since the state is usually localized, the problem of updating the whole app state on a keypress does not apply by default.
> I can also use the same logic for my Server (another "Persistor" [sic]) which indeed uses POST for submitting updates, but responses come down a shared SSE stream (which I need anyway so that users can see eachothers changes in real-time).
Asynchronous logic is hard to handle in synchronous React rendering paradigm... That was the reason for me to come up with a ViewController concept (https://github.com/riptano/statium#viewcontroller), which is the other part of Statium. The idea is to write business logic in a more imperative style, statements not functions:
const loadUserPosts = async ({ $get, $set }) => {
let [user, posts, comments] = $get('user', 'posts', 'comments');
if (!posts) {
await $set({ loading: true });
posts = await loadPosts(user);
await $set({ loading: false, posts });
}
if (!comments) {
await $set({ loading: true });
comments = await loadComments(user, posts);
await $set({ loading: false, comments });
}
};
Decyphering this: $get is a function that returns ViewModel state values by keys, and $set allows updating these values. This is a matter of personal preference of course, but I think this approach makes the logic much easier to read and understand than Redux thunks or sagas.There's also the RealWorld example app I came up with for React + Statium, check it out: https://github.com/nohuhu/react-statium-realworld-example-ap.... It's not yet submitted to the official list because I'm stuck trying to come up with a sensible logo for it... :)
Edit: after some basic research among other complaints are: bad performance, synchronous, bad security model, only strings, limited to 5mb, no access to web workers.
Localstorage has its uses but shouldn't be abused to store everything in it.
Component state should only contain volatile data that is not important to save, the opposite of LocalStorage.
I misread the parent comment, apologies for that! After re-reading, I still agree with them, but with some nuance.
This article describes "somewhat" volatile state, and I think there are interesting use-cases for temporary sticky state that stays between visits (but doesn't need to be stored permanently). I see this being cool for search filter states and stuff.
I'll leave my original comment as-is because I still thought that was worth sharing, too.
--
I've put a lot of thought into easy, no DB persistence for a project of mine, and I have to agree with your assessment.
I ended up persisting to the private fragment identifier (hash) portion of the URL. It makes bookmarking and copy-paste state sharing easier. Most major browsers have a bookmark sync feature these days, so state sync is free (a nice benefit!).
While researching persistence options, I came across storage.sync[0]. It's like local storage, but synced across browsers. I've never used it, but the promised functionality would be very cool.
I think storage sync is a really good idea. I just wish there was a "web browser state file system" of sorts for users to browse, copy, back-up, etc. In absence of that, I feel no user can be confident that local storage is working, and as a consequence I won't persist anything of consequence there.
[0]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/Web...
https://stackoverflow.com/questions/23560090/is-the-fileapi-...
I have a few completely serviceless web apps that utilize localstorage for this kind of thing.
However someone above made the point that sometimes LS _is_ a good idea, I just wouldn't do it at the component level.
Where do you store data that you retrieve from a server for rendering? Storing it in the state is perfectly fine. In this example, data is retrieved from local storage and stored in the state.
I disagree, these kind of settings should not be tied to the account as an account may be accessed across multiple devices. Using LS to store device-specific preferences seems like a good solution to me.
However, I still disagree on doing it on a component level (with hooks or whatever). If I log out, that LS should be gone in most cases. Either the logout must know too much about specific components (leaky abstraction), or the components will need to handle too many edge cases.
I.e., you have something that on login creates a local storage key value for the active user, and then when someone logs out that is cleared (or it's keyed by user id and only used when that user is logged in). Components adding something need to add it to that key somehow, or could make use of prefixes. (the lack of structured data in local storage hurts a little here)
This way the thing managing the profile doesn't need to know what people are storing, and the components can still clear on logout.
For example, I use a similar approach in an open source trivia game[0] to store the last categories selected. Then, when a new game is started, these categories are pre-selected but easily changed. It's not crucial to the game play, but it is convenient.
It can also be used in PWA's when offline.
instead of directly interfacing with window.localStorage, we opted to use lscache[3], which allows us to expire keys: `lscache.set(key, value, 5)` (expires in 5 minutes)
[1]: https://blog.cloudflare.com/jamstack-at-the-edge-how-we-buil... [2]: https://github.com/cloudflare/workers.cloudflare.com/blob/ma... [3]: https://github.com/pamelafox/lscache
React.useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
It's pretty neat because it has a simple and familiar API. I think it's more like sticky props than sticky state, though...Edit: here's another hook with a more accurate, less catchy name, plus error handling: https://usehooks.com/useLocalStorage/
> Every single time, I have to swap the default currency from USD to CAD. Why can't it remember that I'm Canadian??
US-based devs and English-speaking devs often forget about the rest of the world. Either it's bad or inexistent i18n, overly broad assumptions (e.g name=`${firstName} ${lastName}`) or just straight-up bad default values that can't be changed (like the example).
When you're developing a SaaS, more often than not you're doing it for a global audience. Don't forget that.
So an internal application I work on has communication between teams where first + last isn't a given. So I'm constantly paranoid if I'm going to get it right (usually just don't communicate name at all, just make statements).
Fast forward to a month ago when I started working on a rewrite and while constructing the users (and actually making them users within a company instead of effectively companies themselves), I added an enum for NameOrder and separated the two fields.
Not particularly important to the application in the grand scheme of things, but geez, it feels mildly freeing that it won't be so ambiguous in the future.
I've generally heard it recommend to just have a "Full Name" text box, but that wouldn't solve your use-case, so now I'm not sure.
function useStickyState(defaultValue, key)
should be function useStickyState(key, defaultValue)useStickyState("count", 0);
Then later in the article:
useStickyState('day', 'calendar-view');
Thanks for the heads-up.
And even if it is a trivial solution, it brings up an interesting idea. Why this article and not the others? Because it was posted at the right time. I'm sure if another one of those articles was posted here (and was written in an equally approachable way) it would do just as well.
LE: I actually tested the site with Cookies disabled and the entire site crashed due to the localStorage calls.
Using feature detection solves the SSR issue and as a bonus that code won't error if localStorage doesn't otherwise exist (old browsers, tests).
The best method of feat. detection of localStorage, IMO, is writing and reading back a test value in a try catch block. That will catch safari incognito which lies about support.
It would make handling your data much more sane (it all lives in one place, your Redux store), making it easier to sync (just sync your store's state), solve the key collision issues (with the author's implementation, separate components can overwrite each other's data).
[0] https://egghead.io/lessons/javascript-redux-persisting-the-s...
I'm new to react but hooks look interesting.
You should be persisting state in the Data layer on the client
The Data layer should be syncing with the servers or peer to peer once in a while
Quote from the React landing page:
> Design simple views for each state in your application
Quite literally, one of the selling points of React is that it's focused on the view based on data, not on where that data comes from.