Why I stored favorites in a Cookie and then moved them to localStorage
Favorites that vanished after a few days led me to compare Cookie and localStorage. Also covers syncing between tabs and how the code actually stores them.
#Frontend #localStorage #Browser
I opened the crypto site after a few days away and the coins I had favorited were all gone. I was sure I had clicked the star on them, but the list was completely empty. I could track down the cause because I built the site, but a user would have thought "why is the stuff I saved gone?" and just left. The goal for this site was that it could be used without logging in, so favorites for users who aren't logged in had to be stored in the browser, not on the server. What I picked first was a Cookie, and that was the problem. In this post I go over why Cookie didn't fit, how localStorage is different, and the new problem that showed up after the move. Cookies disappear eventually A Cookie has an expiry time. If you set expires or max-age it disappears at that point, and if you set nothing it disappears when the browser is closed. The code I first wrote didn't set an expiry, so it was effectively a session cookie, and it was only natural that the favorites were empty after fully closing and reopening the browser. Setting a long expiry does work. But it still bothered me that it was data that "disappears someday". Favorites are information that should stay until the user deletes them, and a store that comes with its own shelf life didn't suit that. Expiry wasn't the only thing that didn't fit The most wasteful part is that it follows every request to the server. A Cookie is attached automatically to the headers of every HTTP request. The server has no use for the favorites at all, yet they're sent along every time prices are fetched and every time an image is loaded. This site makes price requests often, which makes it worse. The capacity is small too. A Cookie is usually limited to 4KB, so with market codes stored as a JSON array, it gets tight at around a hundred favorites. And it's a hassle to work with. Code for parsing the string, encoding it, and calculating and attaching the expiry all piles onto a single save. This is what the cookie storage code actually looks like right now. // utils/cookies.ts const FAVORITES_COOKIE_NAME = 'coin_favorites'; export const saveFavoritesToCookie = (favorites: string[]): void = { const encoded = encodeURIComponent(JSON.stringify(favorites)); const expires = new Date(); expires.setFullYear(expires.getFullYear() + 1); // Expires in 1 year document.cookie = `${FAVORITES_COOKIE_NAME}=${encoded}; expires=${expires.toUTCString()}; path=/`; }; export const getFavoritesFromCookie = (): string[] = { const cookies = document.cookie.split(';'); const favoriteCookie = cookies.find(cookie = cookie.trim().startsWith(`${FAVORITES_COOKIE_NAME}=`)); if (!favoriteCookie) return []; return JSON.parse(decodeURIComponent(favoriteCookie.split('=')[1])) as string[]; }; Even just to read it, you have to split the whole of document.cookie on semicolons and pick out your own. That's too much work to get one value out. This is what it looks like in localStorage localStorage has none of the problems above. There's no expiry, so it stays until you delete it, the capacity is far roomier at around 5MB to 10MB depending on the browser, and it doesn't ride along on server requests. And the API is simple. // Save localStorage.setItem('favorites', JSON.stringify(favorites)); // Read const favorites = JSON.parse(localStorage.getItem('favorites') || '[]'); // Delete localStorage.removeItem('favorites'); All the code that worried about Cookie expiry goes away and these three lines are all that's left. It stays there whether you close and reopen the browser or several days go by. So when do you use a Cookie? Cookies aren't bad. For information the server has to read, meaning login sessions or auth tokens, a Cookie is the right choice. That trait of following every request to the server automatically is actually an advantage there, and with httpOnly you can even block scripts from reading it. Conversely, information only the client uses has no reason to be sent to the server, so localStorage is the right choice. Things like favorites, display settings and the theme. In the end it comes down to one question: "does the server need to know this data?" The problem that came up after the move After moving to localStorage, a new problem appeared. With two tabs open, they don't know about each other. Even when a value changed in one, the other tab kept holding the old value. I read that listening for the storage event would fix it, so I added it, and this time it was the other way around. The other tab updated, but my own tab, the one that changed the value, did not. By design, the storage event isn't delivered to the tab that made the change. The logic is that the side that changed it already knows the value so there's no need to notify it, but when there are several components, that "side that changed it" can be a different part of the screen. So I settled it by making one more custom event and listening to both. useEffect(() = { refresh(); const handler = () = refresh(); window.addEventListener('storage', handler); // When another tab changed it window.addEventListener('mockTradingUpdate', handler); // When my own tab changed it return () = { window.removeEventListener('storage', handler); window.removeEventListener('mockTradingUpdate', handler); }; }, []); I had the side that changes the value fire mockTradingUpdate with dispatchEvent. With the two events hooked to the same handler, the screen updates in one way no matter which side made the change. Switching the storage itself was simple, but who finds out about a change, and how, took a few more days after that. But then I opened the code again While revising this post, I opened the actual code. The name mockTradingUpdate on the event code above was the hint: what I moved to localStorage wasn't the favorites, it was the paper trading balance and holdings. The favorites are still in a Cookie named coin_favorites. The expiry has been extended to 1 year instead. So the problem of them disappearing after a few days was patched by adding an expiry, and the rule from this post, "what the server doesn't need to know goes in localStorage", was applied to the paper trading feature I built afterward. I wrote in the post that I moved them, but the favorites themselves never moved. I think I couldn't find a reason to touch it because it was running fine, but the cookie still follows every request to this day. Right now, clicking the star on the screen moves a coin to the top of the list like this. That information is still in a cookie. If you ever need to store something in the browser, I'd recommend first asking "does the server need to know this?" And once you've set that rule, it's good to open up the old code now and then and check whether you actually applied it there as well. It's easy to do what I did, apply it only to new code and think the rule is in place.