Hacker News new | past | comments | ask | show | jobs | submit login
Persisting React State in LocalStorage (joshwcomeau.com)
137 points by joshwcomeau on Feb 28, 2020 | hide | past | favorite | 64 comments



I'd recommend just putting this crap in the URL. That way (a) the server could see it too, and (b) users could share their state by sharing the URL (which makes supporting them easier!). If you've got so much state this doesn't make sense, then ¯\_(ツ)_/¯ I suppose caching it in localStorage is fine, but I haven't yet run into this problem.

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.


User specific state like "dark mode enabled" would probably be better inside localStorage than inside a shareable URL.


Disagree.

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.


That's useful in the specific case of debugging but as a user it would be pretty awful if all my customization disappeared every time I clicked a link from another user.

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.


That can also be achieved by storing that information as user preferences on the backend, and it won’t affect your friends but let’s support people figure out what they need to know.


Don't browsers have media queries for things like that?

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.


Compatible with everything except IE 11, but in draft: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pref...


We have an application that's used outdoors day and night in some cases. The good news is it makes a ton of difference in usability; the bad news is I honestly still have to search for instructions on toggling dark mode on Ubuntu. The switch is a lot simpler.


Hey go nuts. Whatever works for you.

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...


Sure, it was just an example. :)


I figured as much, but I feel pretty strongly that every piece of state needs to be accessible to a user or to the server, otherwise you lose control of it and that can war on.

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.


That's a neat idea!

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)


What kind of persisted settings?

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!


There should be a single useState hook which takes an optional `{persistor, reader}` param. Then the choice of whether to stuff this in local storage, URL, local state, global (transient) state, post it to an endpoint, etc is all configurable depending on the use case.


I've thought about something like that, but then what namespace would the keys belong to? Are they just some kind of hidden "superglobal" and if so, how do you reconcile the intention that state be limited to its component? On the other hand, you could (somehow) build unique symbols for each instance, then have the problem of who's responsibility is it to nominate friendly names for the query string (or yuck, __viewState!)? Or you could make it the persistor's [sic] responsibility; give it this, the key and the value, and let it worry about this exact same problem? Even if you choose one way, there's going to be people who want it a different way, so you've got to be right!

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.


This is what Statium and Urlito are for:

    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>
    );


I'm not familiar with either of them, but that seems like a lot more code!

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).


> I'm not familiar with either of them, but that seems like a lot more code!

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... :)


I remember people were abandoning localstorage a while ago because of some fundamental problem I can't quite remember right now. Not sure if it was difficulty in keeping in sync which the author mentions or the fact that it can't be scoped in a way to share among subdomains. I'm sure someone here will have a better memory than me.

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.


The "bad security model" applies to storing things like tokens; it's an argument against using JWTs and localStorage as an alternative to secure cookies. It doesn't apply here.


Re web workers: is anyone (apart from Google) using this in practice? I assume Photopea and similar apps could benefit from it in some specific cases, but haven't had the need for them myself. Am I missing something?


I've experimented with using it together with TensorFlow.js to avoid blocking the UI thread while performing computationally expensive operations. Operations do take a bit longer to complete in a web worker compared to the main thread, but in return you get a smooth UI and a better user experience


Last I checked, they can only pass messages to the main thread as strings. All that (de)serialization can easily become a bottleneck, depending on the workload.


Yes we use it for relatively heavy processing in our web app. To not block the UI and allow offline use


LocalStorage is tied to a device. The kind of settings described in the article should be tied to an account.

Component state should only contain volatile data that is not important to save, the opposite of LocalStorage.


Edit:

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...


Chrome has the FileSystem API, but they couldn't get the other vendors on board and it's basically dead now.

https://stackoverflow.com/questions/23560090/is-the-fileapi-...


> The kind of settings described in the article should be tied to an account.

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.


Ok I see your point, LS might be the right place to save such a state.

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.


Perhaps you could build the LS storage on top of a per-profile local storage?

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.


I consider this a best practice. I always key my localstorage values to the user account and build version so you don't get issues with old localstorage schemas causing bugs when a new build is released.


I would suggest just calling clearAll on localStorage upon logout. To keep it simple.


Yes. When possible. If you're not about to set up a server side of things, this works great.

I have a few completely serviceless web apps that utilize localstorage for this kind of thing.


> Component state should only contain volatile data that is not important to save, the opposite of LocalStorage.

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.


Fair point, but server state is tied ti my account, LS is not. You can tie LS to the account, but IMO that would be out of scope for an "everything component" approach, like the article.


An appropriate use is as a progressive enhancement.

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.

[0] https://justtrivia.fun


Totally agree. I usually have a /userSettings api to store all the user customized UI settings (e.g. sidemenu collapsed or not, theme, etc.)


what's the best way to do that - tie the data to an account?


Having some kind of "settings" object together with the account sounds reasonable.

However someone above made the point that sometimes LS _is_ a good idea, I just wouldn't do it at the component level.


It's a lot more setup but I've had a good experience with redux, redux-persist and localForage. localForage picks the best available browser storage API. One can select which subset of data to persist with redux-persist. It does assume a redux buy in...


this is great - i implemented something similar in a recent project, where we have some state being persisted "at the edge" (in cloudflare workers' key-value store product, workers kv) and cached in-browser using local storage. we wrote a bit about it here[1], though our approach[2] has changed a bit even since this post went out.

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


Damn, as someone currently trying to move from React classes to React Hooks, this does a pretty great job of showing the use case for Hooks. I have 10+ components across 5+ projects for which I'd use this hook.


I don't think key should be in the last part of here, because key should never change inside the closure:

    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/


The ESLint rules for hooks enforce adding any values used inside the function to the dependency array.


The author briefly touches on a pet peeve of mine:

> 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.


> overly broad assumptions (e.g name=`${firstName} ${lastName}`)

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.


Names are pretty complicated, not sure that fully covers it. Many people have multiple last names, and how do you handle middle names?

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.


Heads up to the author, the code example is wrong.

  function useStickyState(defaultValue, key)
should be

  function useStickyState(key, defaultValue)


I was also very confused by this. Does anyone know why this article has so many votes? This is a trivial solution and there are many other articles explaining this exact thing.


Because it brings attention to an interesting concept in a simple way. I'm starting to use React again after a year away using Vue and I've only just started to play with hooks. So it's cool to see other use cases.

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.


I think my opinion is biased because I know React pretty well and have a lot of JS experience so the article feels just like explaining syntax and does not cover important edge cases (eg. what if you have localStorage disabled? your hook will crash!).

LE: I actually tested the site with Cookies disabled and the entire site crashed due to the localStorage calls.


Looks like it's called both ways:

useStickyState("count", 0);

Then later in the article:

useStickyState('day', 'calendar-view');


Ah, good catch! Yeah, I changed this partway through, missed that!

Thanks for the heads-up.


For SSR supports, use cookie instead of localStorage. But I would put a damn user preference flag in backend rather than solving it in the front-end.


I'm astonished at the lack of feature detection. I though that was common knowledge.

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.


Wouldn't it make more sense to stick this view persisted state in your Redux store? (and write it to LocalStorage with a throttle). Dan Abramov does it that way in [0]

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...


Not all React applications use Redux.


I just did this with Vuex and vuex-persistedstate. It was a drop in plugin in nuxt. Anytime a mutation happens state is written.

I'm new to react but hooks look interesting.


React is a view layer tech

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


I don't think react claims to be "view" only anymore.


AFAIK, React does claim to solve "the view problem" and not more than that. You still need an architecture around your application if you're building something larger than a demo.

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.


localStorage is the data layer in this example. Components are the view. The hook is keeping the view and the data layer in sync.


The Apollo React client does this for us


Care to elaborate?




Consider applying for YC's Summer 2025 batch! Applications are open till May 13

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: