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

Use react-is to check component validity #5095

Closed

Conversation

elliottsj
Copy link
Contributor

Fixes #4055

To try this out, check out my branch, then:

npm install
npm link
cd examples/hello-world/
npm install
npm link next
npm run dev

Then edit examples/hello-world/pages/index.js:

export default React.forwardRef((props, ref) => (
  <div ref={ref}>
    Hello World. <Link href='/about'><a>About</a></Link>
  </div>
));

Visit http://localhost:3000 and the example should render without error.

@timneutkens
Copy link
Member

timneutkens commented Sep 5, 2018

Looks like the tests failed, at least:

      ✕ should recover after exporting an invalid page (4063ms)
      ✕ should recover after a bad return from the render function (1153ms)

You can run them manually using:

yarn testonly --testPathPattern "basic" -t "should recover after exporting an invalid page"

@timneutkens timneutkens closed this Sep 5, 2018
@timneutkens timneutkens reopened this Sep 5, 2018
@elliottsj elliottsj force-pushed the react-is-valid-element-type branch 2 times, most recently from aa0decc to ceff43f Compare September 5, 2018 15:36
@timneutkens
Copy link
Member

I wonder if we can make this check development only, and only load react-is in that case to save bundle size

@elliottsj
Copy link
Contributor Author

elliottsj commented Sep 6, 2018

That sounds like a good idea. Does Next.js have an existing pattern to do that?

One way is to add a NODE_ENV check before the isValidElementType check, and rely on webpack's dead code elimination to omit the react-is module when it detects that its exports aren't used:

if (process.env.NODE_ENV === 'development' && !isValidElementType(Component)) {
  throw new Error(`The default export is not a React Component in page: "${pathname}"`)
}

I'm trying this out, adding this to rules in build/webpack.js:

{
  include: /react-is/,
  sideEffects: false,
},

But react-is still seems to be included in the production bundle in the hello-world example.

I don't have too much experience using webpack's tree shaking, so not sure if I'm missing something.

@timneutkens
Copy link
Member

timneutkens commented Sep 6, 2018

I guess something like this should work:

if (process.env.NODE_ENV === 'development') {
  const {isValidElementType} = require('react-is')
  if(!isValidElementType(Component)) {
	throw new Error(`The default export is not a React Component in page: "${pathname}"`)
  }
}

In general, I always wrap the process.env.NODE_ENV in an outer if so we're 100% sure uglify will remove the dead code.

The test previously used the string value "not-a-page" to verify that
Next.js renders the error "The default export is not a React Component".
This no longer works because "not-a-page" is technically an allowable
element now, which renders an HTML element called <not-a-page>.

Fixed by replacing the value with an object: { not: "a-page" }
@elliottsj
Copy link
Contributor Author

Trying it out now, and it appears that react-is is still included in the production bundle even with the suggested outer if statement.

I've pushed my changes to this branch, and you can verify with:

npm install
npm link
cd examples/hello-world/
npm install
npm link next

# Build for production:
npm run build
npm start

Open http://localhost:3000 and look at the commons JS asset (mine is http://localhost:3000/_next/static/chunks/commons.792d7d9de2a7d11ba82f.js). You'll find the minified source of react-is in here.

@timneutkens
Copy link
Member

I wonder if it’s shipped with React?

@timneutkens
Copy link
Member

I’ll have a look tomorrow 👍🏻

@elliottsj
Copy link
Contributor Author

I suspect that the reason why webpack/uglify's dead code elimination is not working is that Next.js's compiled sources use CJS modules instead of ES modules. e.g. in node_modules/next/dist/client/index.js:

var _reactIs = _interopRequireDefault(require("react-is"));

Not 100% sure though; I'll have more time to verify this weekend.

@timneutkens
Copy link
Member

Uglify's dead code elimination works by removing blocks that have if(false) for example, webpack replaced process.env.NODE_ENV === 'development' compiles to 'development' === 'development' which is also removed by uglify

@timneutkens
Copy link
Member

Fixed in 5ddd91c

I had to add IgnorePlugin for that specific file. Apparently webpack still bundles the require otherwise 🙏

@elliottsj
Copy link
Contributor Author

Awesome, thanks 🙏

@timneutkens
Copy link
Member

Looks like the tests are failing though.

@elliottsj
Copy link
Contributor Author

Looks like the should recover after exporting an invalid page test is failing again, now due to the fact that the isValidElementType check doesn't occur on production:

  ● Basic Features › Error Recovery › should recover after exporting an invalid page

    expect(received).toMatch(expected)

    Expected value to match:
      /The default export is not a React Component/
    Received:
      "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

The test suite doesn't enter the if (process.env.NODE_ENV === 'development') {} block, so React's own error is raised instead upon render.

Should I update the test to expect React's error instead? Or maybe change the test suite so specific tests are executed using process.env.NODE_ENV === 'development'

@nassimbenkirane
Copy link

Hello 😄This PR seems great 👍

Do you think we could consider having react-is in the production bundle if/when the tree-shakeable version is merged and released facebook/react#13321

Or is it better to keep react-is outside of the production bundle and we should rather change the tests ?

@timneutkens
Copy link
Member

I'm going to close this PR as it has merge conflicts and the test was breaking, but I'll add good first issue / help wanted labels to the original issue and post that you already implement most here so that someone can take a look 💯

timneutkens pushed a commit that referenced this pull request Dec 17, 2018
…#5857)

Resolves #4055 

Credit: #5095

I didn't use the ignore webpack plugin from the original PR and tested bundle size with #5339 - seems to be safe on that front.

Was able to get tests to pass locally, unsure of what goes wrong in CI 🤷‍♂️ 

**Questions**
1) The initial PR didn't include changes to `next-server/lib/router` in `getRouteInfo()`. Should the same changes be made within?

2) Should we add a test for rendering a component created via `forwardRef()`?

`component-with-forwardedRef`:
```javascript
export default React.forwardRef((props, ref) => <span {...props} forwardedRef={ref}>This is a component with a forwarded ref</span>);
```

some test:
```javascript
test('renders from forwardRef', async () => {
  const $ = await get$('/component-with-forwardedRef')
  const span = $('span')
  expect(span.text()).toMatch(/This is a component with a forwarded ref/)
})
```
@lock lock bot locked as resolved and limited conversation to collaborators Jan 10, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Home-rolled "is Component?" test is throwing false negatives for React 16.3.0 features
3 participants