≡ Menu

⚛️ Tutorial: Building an Auto-Save Component with useEffectEvent

This tutorial demonstrates the ideal use case for the experimental useEffectEvent hook: creating stable, non-reactive callbacks that access fresh state or props without forcing your primary useEffect hook to re-run.

The Goal: Stable Auto-Saving

We want an editor component that automatically saves its content to an API every 5 seconds.

  • Requirement 1 (Stability): The 5-second timer/interval must be set up only once when the component mounts. It should not reset every time the user types.

  • Requirement 2 (Freshness): The save function must always read the latest text the user has typed (the latest content state).

Step-by-Step Implementation

Step 0: Setup and API Mock

We start with the basic component structure and mock the API function that handles the saving logic.

import { useEffect, useState, useEffectEvent } from 'react';

// Mock API function for demonstration
const api = { 
  save: (data) => console.log('Saving latest content:', data.substring(0, 20) + '...') 
};

function DocumentEditor() {
  const [content, setContent] = useState('');
  // ... (Steps 1 & 2 will go here)
  
  return (
    <div>
      <h3>Document Editor (Saves every 5s)</h3>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows="10"
        cols="50"
        placeholder="Start typing..."
      />
    </div>
  );
}

Step 1: Define the Effect Event (The Non-Reactive Action)

The core saving logic is defined here. By wrapping it in useEffectEvent, we ensure that the function doSave always has a stable identity across re-renders, yet the code inside (which uses content) always sees the latest state.

// 1. Define the Effect Event (The Stable Function)
const doSave = useEffectEvent(() => {
  //  This function ALWAYS sees the LATEST 'content' state.
  if (content.trim() !== '') {
    api.save(content);
  }
});
  • Benefit: This function can be safely called from inside useEffect without being listed as a dependency.


Step 2: Define the Effect (The Reactive Setup)

Now we set up the interval using a standard useEffect. Since the function we call (doSave) is stable, the dependency array remains empty, fulfilling our stability requirement.

// 2. Define the Effect (The Setup Logic)
useEffect(() => {
  console.log('--- Setting up auto-save interval ---');
  
  // The interval calls the stable 'doSave' event
  const intervalId = setInterval(() => {
    doSave(); // Calls the stable event which reads the fresh 'content'
  }, 5000);

  // Cleanup: Clears the interval when the component unmounts or effect re-runs
  return () => {
    console.log('--- Clearing auto-save interval ---');
    clearInterval(intervalId);
  };
}, []); //  Dependency Array: Empty means it only runs ONCE on mount.
  • Result: The useEffect sets the timer once. Typing into the textarea updates content, but it does not trigger the useEffect to re-run and reset the timer.

Every 5 seconds, the stable doSave function runs and accesses the current, fresh content.

Summary of the Complete Component:

import { useEffect, useState, useEffectEvent } from 'react';

const api = { 
  save: (data) => console.log('Saving latest content:', data.substring(0, 20) + '...') 
};

function DocumentEditor() {
  const [content, setContent] = useState('');
  
  // 1. STABLE ACTION: This reads fresh state (content) but has a fixed identity.
  const doSave = useEffectEvent(() => {
    if (content.trim() !== '') {
      api.save(content);
    }
  });

  // 2. STABLE SETUP: This sets up the non-restarting timer.
  useEffect(() => {
    console.log('--- Setting up auto-save interval ---');
    const intervalId = setInterval(() => {
      doSave(); // Calls the stable event
    }, 5000);

    return () => {
      console.log('--- Clearing auto-save interval ---');
      clearInterval(intervalId);
    };
  }, []); // Only runs once.

  return (
    <div>
      <h3>Document Editor (Saves every 5s)</h3>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows="10"
        cols="50"
        placeholder="Start typing..."
      />
    </div>
  );
}

Have any questions about the code above?

Leave them in the comments section below!

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 0 comments… add one }

Leave a Comment