Components Nested Inside Fragments don't HMR
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
Appcomponent 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
divand 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.
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