vitejs / vite-plugin-react-swc

Speed up your Vite dev server with SWC
MIT License
831 stars 54 forks source link

Inconsistent behavior with React/HMR after splitting & saving file #104

Closed shanecash closed 1 year ago

shanecash commented 1 year ago

Describe the bug

When I do this:

App.tsx:

import React from 'react';

export default function App() {
  return (
    <>
      <ExampleRoot>
        <ExampleChild />
      </ExampleRoot>
    </>
  );
}

type ExampleRootProps = {
  children: React.ReactNode;
};

export function ExampleRoot({ children }: ExampleRootProps) {
  let hasExampleChild = false;

  React.Children.forEach(children, (child) => {
    if (React.isValidElement(child) && child.type === ExampleChild) {
      hasExampleChild = true;
    }
  });

  if (!hasExampleChild) {
    // Not thrown.
    throw new Error('<ExampleChild/> is required');
  }

  return <div>{children}</div>;
}

export function ExampleChild() {
  return <div>Example Child</div>;
}

Everything works as expected. No error is thrown because <ExampleRoot/> is given <ExampleChild/>. The same is true when I save the file.

When I split <ExampleRoot/> and <ExampleChild/> into a separate Ui.tsx file, the initial render works as expected (no error thrown). However, when I save Ui.tsx, it throws the error:

App.tsx:

import { ExampleRoot, ExampleChild } from './Ui';

export default function App() {
  return (
    <>
      <ExampleRoot>
        <ExampleChild />
      </ExampleRoot>
    </>
  );
}

Ui.tsx:

import React from 'react';

type ExampleRootProps = {
  children: React.ReactNode;
};

export function ExampleRoot({ children }: ExampleRootProps) {
  let hasExampleChild = false;

  React.Children.forEach(children, (child) => {
    if (React.isValidElement(child) && child.type === ExampleChild) {
      hasExampleChild = true;
    }
  });

  if (!hasExampleChild) {
    // Thrown.
    throw new Error('<ExampleChild/> is required');
  }

  return <div>{children}</div>;
}

export function ExampleChild() {
  return <div>Example Child</div>;
}

Reproduction

https://github.com/ShaneAtJag/vite-react-bug

Steps to reproduce

  1. pnpm install
  2. pnpm dev
  3. No error thrown in browser.
  4. Open Ui.tsx, save it.
  5. Error thrown in browser.

System Info

System:
    OS: Linux 5.15 Ubuntu 20.04.4 LTS (Focal Fossa)
    CPU: (8) x64 Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
    Memory: 2.88 GB / 7.65 GB
    Container: Yes
    Shell: 5.0.17 - /bin/bash
  Binaries:
    Node: 16.15.1 - ~/.nvm/versions/node/v16.15.1/bin/node
    npm: 8.11.0 - ~/.nvm/versions/node/v16.15.1/bin/npm
  Browsers:
    Firefox: 112.0.2
  npmPackages:
    @vitejs/plugin-react-swc: ^3.0.0 => 3.3.0 
    vite: ^4.3.2 => 4.3.5

Used Package Manager

pnpm

Logs

No response

Validations

ArnaudBarre commented 1 year ago

Yeah probably something to do with how React internals works. The ExampleRoot is given an instance of the previous ExampleChild on rerender and the reference comparison fails. Not sure there is anything actionnable here.

My advice would be to use the name/displayName of the component as a check

shanecash commented 1 year ago

@ArnaudBarre That helps. Thank you. I figured it was something along those lines, but wanted to verify because my research returned nothing. A type-safe solution per your advice using displayName:

Ui.tsx:

import React from 'react';

type ExampleRootProps = {
  children: React.ReactNode;
};

const ExampleRoot = ({ children }: ExampleRootProps) => {
  let hasExampleChild = false;

  React.Children.toArray(children).forEach((child) => {
    if (
      React.isValidElement(child) &&
      typeof child.type !== 'string' &&
      'displayName' in child.type &&
      child.type.displayName === 'ExampleChild'
    ) {
      hasExampleChild = true;
    }
  });

  if (!hasExampleChild) {
    throw new Error('<ExampleChild/> is required');
  }

  return <div>{children}</div>;
};

ExampleRoot.displayName = 'ExampleRoot';

export { ExampleRoot };

const ExampleChild = () => {
  return <div>Example Child</div>;
};

ExampleChild.displayName = 'ExampleChild';

export { ExampleChild };