DEV Community

Cover image for Persistence Pays Off: React Components with Local Storage Sync 🔄🦸🏻‍♂️
Matt Lewandowski
Matt Lewandowski

Posted on

Persistence Pays Off: React Components with Local Storage Sync 🔄🦸🏻‍♂️

Have you ever spent minutes creating the perfect response only to lose it all with a misclick or accidental refresh? We often focus on optimizing performance and creating good-looking user interfaces, but what about the user experience? Experiences like this often make us want to rage quit. Before auto-save became popular in many applications, this used to happen far too often.

Does anyone remember manually saving your Word documents every few minutes so you didn't lose hours of writing? We've come a long way since then. But what about your web apps? Preserving user input and maintaining state across sessions can be extremely important for some experiences.

Local Storage in React

What is Local Storage?

Local Storage is a powerful web API that allows your web app to store key-value pairs in a web browser with no expiration date. This client-side storage mechanism offers several advantages for web applications:

  1. Persistence: Data remains available even after the browser window is closed, enabling seamless user experiences across sessions.
  2. Capacity: Local Storage typically provides 5-10 MB of data storage per domain, surpassing the limitations of cookies.
  3. Simplicity: With a straightforward API, Local Storage is easy to implement and use, requiring minimal setup.
  4. Performance: Accessing Local Storage is faster than making server requests, reducing latency for frequently accessed data.
  5. Offline Functionality: Applications can leverage Local Storage to provide limited functionality even when users are offline.

Why Sync Component State with Local Storage?

Syncing React component state with Local Storage offers a powerful solution to common web application challenges. This approach bridges the gap between in-memory state and persistent client-side storage, creating several key benefits:

  1. State Persistence Across Sessions: By storing component state in Local Storage, your web app will maintain user progress even after browser refreshes or closes. This persistence enhances the user experience, particularly for forms or multi-step processes where it can almost be expected.
  2. Backup for Unsaved Changes: Implementing a Local Storage sync acts as a backup for accidental data loss, automatically preserving user input as they interact with your web app. This means your state will persist between component unmount -> mounts and the app refreshing.

Implementing Local Storage Sync: A Simple Hook

Let's take a look at a really simple practical example using a custom hook called useLocalStorageState():

import { useState, useEffect } from 'react';

const useLocalStorageState = <T,>(
  key: string, 
  defaultValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] => {
  const [state, setState] = useState<T>(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue !== null ? JSON.parse(storedValue) : defaultValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);

  return [state, setState];
};

// Example usage
const TextEditor: React.FC = () => {
  const [text, setText] = useLocalStorageState<string>('editorText', '');

  return (
    <textarea 
      value={text} 
      onChange={(e) => setText(e.target.value)}
      placeholder="Start typing..."
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

This hook allows us to easily sync any component state with local storage. Keep in mind this is a really simple example, it will always use the localStorage value first if it exists.

Real-World Examples

Kollabe: Preserving User Input in Agile Retrospectives

I was running a sprint retrospective with my team recently on my web app Kollabe. We were running an icebreaker where we all had to draw our favorite planets. Some team members accidentally lost their beautiful masterpieces after clicking away from the input, instead of clicking submit. This was a horrible user experience, and adding a simple local storage sync easily fixed it.

GIF of persisting input on Kollabe

Other Platforms: Claude and ChatGPT

But it's not just me! Other platforms that you are most likely familiar with are also doing this. Imagine creating a really long prompt on ChatGPT or Claude and it disappearing. You might also rage quit and choose to use their competitor from now on.

Claude saving prompt into local storage

When to Use Local Storage Sync (and When Not To)

  • Ideal for preserving user input that requires significant effort
  • Not necessary for every piece of state in your application
  • Consider the trade-offs between convenience and performance
  • Do not store sensitive information. Local storage is not encrypted and has no expiration. It can easily be accessed by anyone with physical access to the same device.

Performance Considerations

Performance Considerations: Balancing Persistence and Speed

While syncing component state with Local Storage offers numerous benefits, it's crucial to consider the potential performance implications. Understanding these impacts and implementing mitigation strategies ensures an optimal balance between persistence and speed.

Potential Performance Impacts

  1. Increased Read/Write Operations: Frequent synchronization with Local Storage can lead to a lot of read and write operations, potentially affecting your web apps responsiveness.
  2. Parsing Overhead: Local Storage only stores strings, making it necessary to parse and stringify JSON for complex data structures. This process can become computationally expensive for larger objects.
  3. Storage Limitations: Browsers typically limit Local Storage to 5-10 MB per domain. Exceeding this limit can result in errors and data loss.

To help improve performance, you might consider debouncing your saves to local storage. This is particularly important for inputs.

import { useState, useEffect, useCallback } from 'react';
import debounce from 'lodash/debounce';

const useLocalStorageState = <T,>(key: string, defaultValue: T, delay = 300) => {
  const [state, setState] = useState<T>(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue !== null ? JSON.parse(storedValue) : defaultValue;
  });

  const debouncedSync = useCallback(
    debounce((value: T) => {
      localStorage.setItem(key, JSON.stringify(value));
    }, delay),
    [key, delay]
  );

  useEffect(() => {
    debouncedSync(state);
  }, [state, debouncedSync]);

  return [state, setState] as const;
};
Enter fullscreen mode Exit fullscreen mode

Future Improvements

There is a lot you can do to improve this hook, but it really depends on how simple or advanced your needs are. Here are a few examples:

  1. Implementing a cache object with metadata like created_at or accessed_at. Might be worth just using another library at this point though.
  2. Clearing expired cache objects on startup to prevent dead objects from consuming a lot of space over time.
  3. Allow to storing objects and strings
  4. Allow for prioritizing a default value, over the local storage value. This could be useful if your input is also being used as an editMode for an existing value.

That's it!

Syncing React component state with Local Storage is a useful technique that can really enhance the user experience of your web applications. I'd recommend doing an audit of your application to see if you can also benefit from it!

Also, shameless plug 🔌. If you work in an agile dev team and use tools for your online meetings like planning poker or retrospectives, check out my free tool called Kollabe!

Top comments (9)

Collapse
 
y2x profile image
2X Y

just did same thing here

const useStateWithLocalStorage = <T>(
  key: string,
  initialValue: T,
): [T, React.Dispatch<React.SetStateAction<T>>] => {
  const [value, setValue] = useState<T>(() => {
    const localStorageValue = localStorage.getItem(key);
    if (localStorageValue !== null) {
      return localStorageValue;
    } else {
      return initialValue;
    }
  });
  useEffect(() => {
    localStorage.setItem(key, value);
  }, [value]);

  return [value, setValue];
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
phil_johnston profile image
Phil Johnston

Definitely need to include security in the considerations, as private data should never persist to local storage.

Collapse
 
mattlewandowski93 profile image
Matt Lewandowski

Good point, thanks I'll add it in.

Collapse
 
phil_johnston profile image
Phil Johnston

🙌 nice work

Collapse
 
sumitsaurabh927 profile image
Sumit Saurabh

Excellent article and I don't wanna highjack the comment section but what do you use for product notification in React? Like if I'm building an app in React, what are some of the tools/libraries I can use to embed notifications in my app?

Collapse
 
jonahunuafe profile image
Jonah Unuafe

Why not writing simple react code without adding typescript? Not everyone understands typescript.

Collapse
 
russpalms profile image
RussPalms

Because type safety is important even with simple implementations.

Collapse
 
d4r1ing profile image
Nguyễn Thế Cuân

You might need to listen "storage" event and update component state accordingly, as we can always change local storage directly.

Collapse
 
mattlewandowski93 profile image
Matt Lewandowski

Not in this case. We are using local storage for a back up. If our main goal was to sync state between windows, I’d use something like the BroadcastChannel API.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.