Skip to content

ServerSideRender: refactor fetchData to use useCallback and refs #69237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

yogeshbhutkar
Copy link
Contributor

@yogeshbhutkar yogeshbhutkar commented Feb 19, 2025

What?

Closes #69220

The ServerSideRender does not re-render when the attributes change. This issue can be best reproduced with the Calendar block where the Background property does not reflect on the editor upon making changes.

Why and How?

According to the useDebounce implementation, if any of the arguments change, including the function to debounce, the scheduled call gets canceled.

Ref:

useEffect( () => () => debounced.cancel(), [ debounced ] );

In our case, the fetchData function changes. The debounce function should ideally be wrapped within a useCallback as recommended here:

* Debounces a function similar to Lodash's `debounce`. A new debounced function will
* be returned and any scheduled calls cancelled if any of the arguments change,
* including the function to debounce, so please wrap functions created on
* render in components in `useCallback`.

This alone won't suffice as the change in props causes the fetchData to re-create/change again. It's best to mimic the props with refs which should be used within fetchData with an empty dependency array. The ref will act as a "pointer" to current props without causing function recreation and this approach won't cancel the debounce calls as reflected in the screencast below.

Testing Instructions

  1. Add Calendar Block.
  2. Change the text color & background color.
  3. Confirm the changes happen both on the editor and front-end.

Note: A similar issue involving the Latest Comments block is also fixed.

Screencast

calendar-fix.mov

@yogeshbhutkar yogeshbhutkar marked this pull request as ready for review February 19, 2025 07:41
Copy link

github-actions bot commented Feb 19, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: yogeshbhutkar <yogeshbhutkar@git.wordpress.org>
Co-authored-by: Mayank-Tripathi32 <mayanktripathi32@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@t-hamano t-hamano added [Type] Bug An existing feature does not function as intended [Package] Server Side Render /packages/server-side-render labels Feb 20, 2025
@yogeshbhutkar
Copy link
Contributor Author

Hi @t-hamano,

Could you please test this PR and share your feedback on the approach? I tested it on the Calendar and Latest Comments blocks, and it appears to resolve the issue comprehensively.

Looking forward to your thoughts!

Copy link
Contributor

@Mayank-Tripathi32 Mayank-Tripathi32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, I do wonder why the previous implementation skipped on useCallback though.

Copy link
Contributor

@t-hamano t-hamano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

This approach seems good to me, but I wonder why this issue occurs only for certain attributes. Do you know anything about this?

@yogeshbhutkar
Copy link
Contributor Author

yogeshbhutkar commented Apr 11, 2025

For example, in the Calendar block, some attributes like text and background color don’t update after the initial creation, while others like link color and font size do.

Without this PR, changes to a server-side rendered (SSR) block’s attributes don’t trigger fetchData, meaning the block doesn’t re-render. However, I believe the attributes like font size and link color appear to update because they're applied via CSS classes applied by blockProps.

In the case of font-size, it only works before the first save. It happens because:

  1. blockProps applies the relevant class to the wrapper div
    Ref.
  2. After the first save, these classes are also applied to the inner elements, in this case, to the inner SSR component, which takes precedence. Since the SSR component isn’t re-rendered, those inner styles don’t update—so changes like font-size stop reflecting after the block is saved once.

To summarize, the fetchData, which should execute every time an attribute changes, never executes. Some attributes seems to update because some corresponding classes might be applied by the block props. Some update only before the first save, because, the classes that were applied by block props are now getting overridden by the previously saved classes at inner wrappers.

P.S. All of this can be verified by simply looking at the classes applied on the DOM structure.

@yogeshbhutkar yogeshbhutkar force-pushed the fix-69220/fix-cancelled-debounced-calls branch from dc4e40c to 022c0c1 Compare April 11, 2025 05:36
@t-hamano
Copy link
Contributor

t-hamano commented Apr 11, 2025

However, I believe the attributes like font size and link color appear to update because they're applied via CSS classes applied by blockProps.

I understand this point.

To summarize, the fetchData, which should execute every time an attribute changes, never executes.

For example, if you change the setting of "Display avatar" in the Latest Comment block, fetchData() function will always be executed. Do you know why the problem occurs only with block support such as Color and Typography?

@yogeshbhutkar
Copy link
Contributor Author

yogeshbhutkar commented Apr 11, 2025

When updating color, the props passed to SSR change twice, whereas toggling Display Avatar causes only a single change. This can be verified by logging them within an effect inside the SSR component. When the props change twice, the debouce gets cancelled. (often, the multiple changes do not have different data)

After tracing the render flow, I can confirm that foundational components like the color panel and respective palettes don’t cause redundant changes to the attributes post execution of onChange.

The issue arises from how some blocks use SSR. On each render, objects passed as props are often recreated, which causes SSR to detect prop changes—even when the actual data hasn't changed—leading to multiple updates and cancelled debounced calls.

For example:

  1. Calendar block:
    <ServerSideRender
    block="core/calendar"
    attributes={ { ...attributes, ...getYearMonth( date ) } }
    />
attributes={ { ...attributes, ...getYearMonth( date ) } }

This spreads a new object every time on every render. Passing attributes={attributes} or memoizing the merged object resolves the issue for this block.

  1. Latest Comments block:
    <ServerSideRender
    block="core/latest-comments"
    attributes={ serverSideAttributes }
    // The preview uses the site's locale to make it more true to how
    // the block appears on the frontend. Setting the locale
    // explicitly prevents any middleware from setting it to 'user'.
    urlQueryArgs={ { _locale: 'site' } }
    />

Both attributes and urlQueryArgs are re-created on each render.

It’s unclear why Display Avatar doesn’t trigger the same behavior as guessing which attribute causes re-rendering and under which circumstances is quite challenging.

I think it's best to solve it with the currently proposed ref approach as that I believe is the best way to approach something like this. Of course, this might not become an issue in the first place if the consumers memorize the props before passing, but that can't be expected from every consumers, especially the external ones.

Copy link
Contributor

@t-hamano t-hamano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation! I think your explanation makes sense.

Two suggestions before shipping:

  • Do we really need to use useEffect hook? Can we simplify the code to the following?
    diff --git a/packages/server-side-render/src/server-side-render.js b/packages/server-side-render/src/server-side-render.js
    index d5885ec318..2c6ed55325 100644
    --- a/packages/server-side-render/src/server-side-render.js
    +++ b/packages/server-side-render/src/server-side-render.js
    @@ -107,10 +107,7 @@ export default function ServerSideRender( props ) {
           const [ isLoading, setIsLoading ] = useState( false );
            const latestPropsRef = useRef( props );
    
    -       // Store props in ref to prevent stale closures in the fetchData callback.
    -       useEffect( () => {
    -               latestPropsRef.current = props;
    -       }, [ props ] );
    +       latestPropsRef.current = props;
     
            const fetchData = useCallback( () => {
                    if ( ! isMountedRef.current ) {
  • This package is primitive and rarely updated. It might be a good idea to update the CHANGELOG to highlight this bug fix.

@yogeshbhutkar yogeshbhutkar force-pushed the fix-69220/fix-cancelled-debounced-calls branch from 022c0c1 to d1c5193 Compare April 21, 2025 04:42
@yogeshbhutkar
Copy link
Contributor Author

Thanks for the review, @t-hamano.

Do we really need to use useEffect hook? Can we simplify the code to the following?

You're right! The useEffect is redundant here. I've fixed this in the latest commit.

@t-hamano t-hamano merged commit 7506dbb into WordPress:trunk Apr 21, 2025
59 checks passed
@github-actions github-actions bot added this to the Gutenberg 20.8 milestone Apr 21, 2025
@yogeshbhutkar yogeshbhutkar deleted the fix-69220/fix-cancelled-debounced-calls branch April 21, 2025 06:23
@Mamaduka
Copy link
Member

Side note: using (layout) effect to write refs is a known pattern. It's also recommended to avoid reading or writing refs during render.

See:

@yogeshbhutkar
Copy link
Contributor Author

Apologies for missing it, and thanks for pointing that out. I'll be creating a follow-up PR to fix this ASAP.

chriszarate pushed a commit to chriszarate/gutenberg that referenced this pull request Jul 1, 2025
…ordPress#69237)

* refactor fetchData to use `useCallback` and `refs`

* refactor: use `latestPropsRef` directly

* fix: remove redundant `useEffect`

* chore: add `CHANGELOG`

Co-authored-by: yogeshbhutkar <yogeshbhutkar@git.wordpress.org>
Co-authored-by: Mayank-Tripathi32 <mayanktripathi32@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Server Side Render /packages/server-side-render [Type] Bug An existing feature does not function as intended
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ServerSideRender: Some attribute updates are not reflected immediately
4 participants