Persistence Pays Off: React Components with Local Storage Sync 🔄🦸🏻‍♂️
Tags: react, webdev, javascript, beginners
Published: 2024-07-13 22:21:57

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](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) 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():

```tsx
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..."
    />
  );
};
```

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](https://kollabe.com/retrospectives). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzh8xgyx2lltmnmcxhmf.gif)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1k627spvbb5oz2ybsnwn.gif)

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

```tsx
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;
};
```

## 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](https://kollabe.com/)!