Skip to content
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

Avoid bad hooks dependency usage in MarkdownHooks #891

Closed
wants to merge 3 commits into from

Conversation

remcohaszing
Copy link
Member

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and discussions and couldn’t find anything or linked relevant results below
  • I made sure the docs are up to date
  • I included tests (or that’s not needed)

Description of changes

As some comments point out in #890, there are some bad React hook usages in those changes. A useEffect(), useMemo(), or useCallback() should always specify all of its dependencies in the dependency array. But the dependencies need to be referentially equal in order to not trigger the hook, which is often not possible, given that plugin arrays are often created during render.

This change replaces the useEffect() logic with a useRef(). Using useRef(), we can persist an object instance across rerenders. We can then perform our own props comparison logic to update or not instead of relying on a dependency array. We also avoid setState(), which triggered another render and could cause flashes of empty content.

The processor is updated if remarkRehypeOptions, rehypePlugins, or remarkPlugins changed. The promise is updated if the processor or children has changed.

TODO

  • Deep comparison of plugin options? Or to some depth?
  • Deep comparison of remarkRehypeOptions??
  • Add tests?

wooorm and others added 3 commits February 14, 2025 15:38
This commit adds 2 new components that support
turning markdown into react nodes,
asynchronously.
There are different ways to support async things in React.
Component with hooks only run on the client.
Components yielding promises are not supported on the client.
To support different scenarios and the different ways the future
could develop,
these choices are made explicit to users.
Users can choose whether `MarkdownAsync` or `MarkdownHooks` fits
their use case.

Closes GH-680.
Closes GH-682.
As some comments point out in #890, there are some bad React hook usages
in those changes. A `useEffect()`, `useMemo()`, or `useCallback()`
should always specify all of its dependencies in the dependency array.
But the dependencies need to be referentially equal in order to not
trigger the hook, which is often not possible, given that plugin arrays
are often created during render.

This change replaces the `useEffect()` logic with a `useRef()`. Using
`useRef()`, we can persist an object instance across rerenders. We can
then perform our own props comparison logic to update or not instead of
relying on a dependency array. We also avoid `setState()`, which
triggered another render and could cause flashes of empty content.

The processor is updated if `remarkRehypeOptions`, `rehypePlugins`, or
`remarkPlugins` changed. The promise is updated if the processor or
`children` has changed.
@github-actions github-actions bot added 👋 phase/new Post is being triaged automatically 🤞 phase/open Post is being triaged manually and removed 👋 phase/new Post is being triaged automatically labels Feb 17, 2025
@remcohaszing
Copy link
Member Author

I realized the use API was introduced in React 19. We could choose to update the peer dep, which would be a breaking change. Or we could require React 19 for MarkdownHooks only. Or we may need to look for an alternative to resolve the promise after all.

Copy link
Member

@wooorm wooorm left a comment

Choose a reason for hiding this comment

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

I think it is sad that we would implement such caching equivalence mechanisms; I believe that is what frameworks are supposed to do for you.
As you know, caching is difficult. That shows here too as there are several problems with this equivalence function: a) plugins support several parameters not just options; b) presets.
But, most importantly: you only have a return true on L464: you can remove the entire function as it will always be the tail return false, just with more or less work.

@remcohaszing
Copy link
Member Author

I think it is sad that we would implement such caching equivalence mechanisms; I believe that is what frameworks are supposed to do for you. As you know, caching is difficult.

I somewhat agree here. Just React works great, but it can be hard to integrate external libraries that weren’t built with the React mindset. This would be a lot simpler if we didn’t allow MarkdownHooks to accept arguments to configure a processor, but instead it would work on a pre-existing processor.

That shows here too as there are several problems with this equivalence function: a) plugins support several parameters not just options; b) presets. But, most importantly: you only have a return true on L464: you can remove the entire function as it will always be the tail return false, just with more or less work.

Good finds! This is really going to need tests.

@remcohaszing remcohaszing mentioned this pull request Feb 18, 2025
6 tasks
Base automatically changed from async-plugins to main February 20, 2025 11:52
@wooorm wooorm closed this in 6ce120e Feb 20, 2025

This comment has been minimized.

@wooorm wooorm added the 💪 phase/solved Post is done label Feb 20, 2025
@wooorm wooorm deleted the avoid-hook-dependencies branch February 20, 2025 11:53
@github-actions github-actions bot removed the 🤞 phase/open Post is being triaged manually label Feb 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💪 phase/solved Post is done
Development

Successfully merging this pull request may close these issues.

3 participants