prefresh icon indicating copy to clipboard operation
prefresh copied to clipboard

Components Nested Inside Fragments don't HMR

Open rmorse opened this issue 8 months ago • 1 comments

After having HMR issues for the longest time I finally managed to track down why it was so buggy.

Issue

What I've found is:

  • Components that are direct descendants of fragments won't HMR
  • It starts with a parent component that returns a fragment
  • Any direct descendant component won't HMR, even though the parent does

I did some digging around the code (as much as I could understand it) and I think the issue might be the way Preact handles fragments, I think the previous fragment node could already be detached/destroyed by the time Prefresh's replaceComponent function tries to process the vnode - I could be totally wrong though!

Video

https://github.com/user-attachments/assets/c47ca49e-adc7-43e6-9179-65866a9c5821

Steps to reproduce

Spin up this project: https://stackblitz.com/edit/vitejs-vite-vwbgvaxv?file=src%2Fchild-component.jsx

  • Update Child Component or even Fragment Component
    • Notice they don't update unless you reload the preview
  • Update something in the parent App component and it does successfully update.
  • This is the demo setup from ViteJS website and it actually shows the issue quite well because the main App already returns a fragment.
  • If you change the fragment to a div and reload, HMR starts working

Work Around / Temporary Solution

I've found that if we give fragments a key attribute, everything works as intended.

In my project I'm not using Vite, but Webpack + Babel.

I've created this Babel plugin which fixes the issue for me without having to modify all my fragments manually: https://github.com/rmorse/babel-plugin-preact-add-fragment-key

It looks for functional components that return fragments (only top level, not nested) and adds a key to it - I'm using this in my dev build.

Usage via babel.config.js (it should go before jsx transforms)

module.exports = (api) => {

	const isDevelopment = api.env() === 'development'; 
	api.cache(false);
	
	return {
		plugins: [
			isDevelopment && require.resolve('./babel-plugin-preact-add-fragment-key.js'),
			[
				'@babel/plugin-transform-react-jsx',
				{
					runtime: 'automatic',
					importSource: 'preact',
				},
			],
		].filter(Boolean),
	};
};

Let me know if you'd like me to submit a PR and add this functionality to @prefresh/babel-plugin.

rmorse avatar Apr 26 '25 15:04 rmorse

So after testing the theories in this ticket I realised that the issue also does not exist when using Preact 10.18.1, but does exist when using 10.18.2

Here is a test setup with Prefresh working correctly with fragments with Preact 10.18.1: https://stackblitz.com/edit/vitejs-vite-bstgmew8?file=src%2Fchild-component.jsx

rmorse avatar May 01 '25 09:05 rmorse