Skip to content

iAPI router: Synchronous getter execution prevents state access when store is initialized after hydration #70874

@luisherranz

Description

@luisherranz

Description

Interactivity API's derived state uses getters in the store, which in turn use computed from @preact/signals.

These computeds are executed the first time they are accessed, but afterwards, they are executed synchronously every time a signal they depend on changes its value, without waiting to be called/accessed again. This behavior can be clearly seen in this StackBlitz.

In the Interactivity API, we are using a computed wrapper structure to be able to switch between signals (usually serialized values from the server) and getters (derived state).

  • Computed wrapper: switches between signal (value) and signal (getter)
    • signal: value serialized from the server
    • signal: getter added to the store in JS

This synchronous computed execution is causing a problem in the stores that are loaded after hydration, since the computed wrapper has already been executed, and therefore, when it is invalidated because the store adds a getter to that store property, it is executed synchronously without giving time for the store call to finish executing and return the state that the getter is accessing in their logic.

I believe the first thing we should try is to have a version of the computeds that doesn't run synchronously but stays in an invalidated state and runs the first time they are called after that invalidation. We can ask the Preact Core team if it would be possible to do something like that without them having to expose any additional API in the @preact/signals package, similar to what we did for our own useSignalEffect version.

If that doesn't work or it's not possible, then we could try to get rid of the wrapper computeds and invalidate by changing the value of the signal when the signal is no longer used. Although this would be less ideal because we couldn't do the optimization of using the getters to mutate the DOM directly instead of invalidating the whole Directives component.

Step-by-step reproduction instructions

Create a block with a view.js file that contains a store similar to this one:

setTimeout(() => {
  const { state } = store( '...', {
    get prop() {
      // This fails because `state` is not available yet when the getter is executed during the store execution.
      return state.someValue; 
    }
  } );
}, 2000);

You can see the error reproduced in this stackblitz.

Image

Screenshots, screen recording, code snippet

No response

Environment info

No response

Please confirm that you have searched existing issues in the repo.

  • Yes

Please confirm that you have tested with all plugins deactivated except Gutenberg.

  • Yes

Please confirm which theme type you used for testing.

  • Block
  • Classic
  • Hybrid (e.g. classic with theme.json)
  • Not sure

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions