The useEffect hook from React (available in Gutenberg as @wordpress/element‘s useEffect) is the standard mechanism for running side effects inside block edit components: fetching data from the REST API when an attribute changes, initialising third-party JavaScript libraries, setting up event listeners that must be cleaned up when the block unmounts, or triggering attribute updates based on external changes. In Gutenberg blocks, useEffect runs after each render inside the editor, and its dependency array determines when the effect re-runs. The most common pitfall is an infinite update loop: calling setAttributes() inside a useEffect without a proper dependency array causes the effect to re-run on every render, which triggers a new render, which re-runs the effect, and so on.
Problem: A custom block shows a live preview of an RSS feed URL entered by the user. When the feedUrl attribute changes, the block should fetch the feed's title from a custom REST endpoint and store it as another attribute. The fetch must not run on every keystroke — only after the user stops typing.
Solution: Use useEffect with feedUrl as the dependency. Debounce the fetch to avoid excessive API calls. Use a cleanup function to abort in-flight requests when the dependency changes before the request completes.
// edit.js
import { useEffect, useState } from '@wordpress/element';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, Spinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
export default function Edit( { attributes, setAttributes } ) {
const { feedUrl, feedTitle } = attributes;
const [ isLoading, setIsLoading ] = useState( false );
const blockProps = useBlockProps();
// ── Fetch feed title when feedUrl changes ──────────────────────────
useEffect( () => {
// Do nothing if no URL
if ( ! feedUrl ) return;
// AbortController lets us cancel the fetch if the component
// re-renders (e.g. user types another character) before it completes
const controller = new AbortController();
let debounceTimer;
// Debounce: wait 500ms after last change before fetching
debounceTimer = setTimeout( async () => {
setIsLoading( true );
try {
const res = await fetch(
'/wp-json/my-plugin/v1/feed-title?url=' + encodeURIComponent( feedUrl ),
{ signal: controller.signal }
);
if ( res.ok ) {
const data = await res.json();
setAttributes( { feedTitle: data.title } );
}
} catch ( err ) {
if ( err.name !== 'AbortError' ) {
console.error( 'Feed fetch failed:', err );
}
} finally {
setIsLoading( false );
}
}, 500 );
// Cleanup: cancel the fetch and clear the debounce timer
// when feedUrl changes before the 500ms timer fires,
// or when the component unmounts
return () => {
clearTimeout( debounceTimer );
controller.abort();
};
}, [ feedUrl ] ); // ← only re-run when feedUrl changes
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Feed Settings', 'textdomain' ) }>
<TextControl
label={ __( 'Feed URL', 'textdomain' ) }
value={ feedUrl }
onChange={ ( val ) => setAttributes( { feedUrl: val } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
{ isLoading && <Spinner /> }
{ feedTitle && <h3>{ feedTitle }</h3> }
{ ! feedTitle && ! isLoading && <p>{ __( 'Enter a feed URL.', 'textdomain' ) }</p> }
</div>
</>
);
}
NOTE: The useEffect cleanup function (the function returned from the effect callback) runs both when the dependency changes and when the component unmounts. Always return a cleanup function when the effect sets up subscriptions, timers, or in-flight fetch requests — failing to do so causes memory leaks and state updates on unmounted components, which React logs as a warning. The dependency array is critical: an empty array [] means the effect runs once on mount only; omitting the array entirely means the effect runs on every render (almost always wrong in Gutenberg); listing specific values means it runs when any of them changes. Use @wordpress/data's useSelect and resolvers system for fetching WordPress-specific data (posts, terms, settings) rather than raw fetch — it handles caching and deduplication automatically.