DEV Community

Cover image for Autosaving with React Hooks
Brad
Brad

Posted on • Edited on • Originally published at bradcypert.com

Autosaving with React Hooks

This post is originally from bradcypert.com, but has been reformatted for your viewing pleasure on Dev.to!

React hooks have really changed the game for me when it comes to building react components. However, I've found that writing autosaving functionality is a little less obvious via hooks. Thankfully, it is still possible (and arguably a lot cleaner) when using hooks.

We can accomplish autosaving functionality through use of useEffect. You'll also need a way to post the data to your server. In my case, I'm using Apollo's useMutation hook as well. This allows me to post a graphql mutation from a hook-like interface.

The useEffect hook

The useEffect hook effectively replaces componentDidMount, componentWillUpdate, and componentWillUnmount. Here's how I remember the API for useEffect:

useEffect(() => {
  doWhateverIsHereOnMountandUpdate();

  return () => {
    doWhateverIsHereOnWillUnmount();
  }
}, [skipUntilThisStateOrPropHaschanged])

It's worth mentioning that the skipUntilThisStateOrPropHasChanged is optional, and leaving it out will cause it to process the hook on every render.

Implementing Autosave

Now that we understand our hook, the autosave functionality becomes fairly trivial. We'll use a couple of state hooks as well to help us manage the text that a user types in as well as the last value we saved (we can skip network requests if they're the same).

const [lastText, setLastText] = React.useState('');
const [text, setText] = React.useState('');

You'll see how we use lastText in our useEffect hook below, but for now, you just need to know that text represents the state of what the user has typed in. If you'd like more information on how this works, React's documentation for Controlled Components is a great place to start.

Now, we need a function to trigger to persist our data to the server. In my case, I'll use an Apollo mutation since the server API processes graphql.

const [updateContent] = useMutation(UPDATE_CHAPTER_CONTENT.MUTATION);

Finally, we can write our useEffect hook:

const AUTOSAVE_INTERVAL = 3000;
React.useEffect(() => {
    const timer = setTimeout(() => {
      if (lastText != text) {
        updateContent({ variables: { content: text, id: chapterId } });
        setLastText(text);
      }
    }, AUTOSAVE_INTERVAL);
    return () => clearTimeout(timer);
  }, [text]);

We're doing a couple of neat things here. First, we're setting up our useEffect hook. It creates a timer via setTimeout, and when that hook unmounts, it removes that timer. That's the "meat-and-potatoes" behind it. You'll notice that our setTimeout function checks to see if the text has changed before posting our data, and then sets the last text if it has changed.

We're also only triggering this useEffect when text has changed (as indicated by [text] as the second parameter. We probably could clean this up a bit by removing the if in the timeout function body, and updating the useEffect dependency array to be [text != lastText].

And that should do it! Hopefully, this helps if you're trying to build autosave functionality into React project.

If you'd like to learn more about React, you can find my other post on Facebook's awesome framework here.

Top comments (5)

Collapse
 
ehellman profile image
Erik Hellman

You should probably store the "lastText" value in a Ref instead since doing useState re-renders the component. If you update lastText and text at the same time you will get 2 re-renders every time you really just wanted one.

Collapse
 
bernardbaker profile image
Bernard Baker

Could you explain the theory behind using a Ref in this instance?

Collapse
 
wolverineks profile image
Kevin Sullivan

Doesn't React batch updates?

Collapse
 
kelsonpw profile image
Kelson Warner

Since lastText is never used in render/UI, wouldn’t using a ref instead of state for storing prev state save extra renders?

Collapse
 
wolverineks profile image
Kevin Sullivan

it looks like lastText != text is doing the same thing as [text], so everything lastText could probably be removed