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

fix: modified eslint to support Compound React Components #27900

Closed
wants to merge 5 commits into from

Conversation

amjadorfali
Copy link

@amjadorfali amjadorfali commented Jan 8, 2024

Summary

close #20700

Issue

A React component fn having a property that is a React component fn, is throwing an eslint error when using Hooks inside that component, due to the naming.

Solution

Added support in eslint for node of type MemberExpression, where they have a property.name in capital case, that is a ComponentName, and property.type as Identifier.

This also supports Objects, not just React Components:

const Obj = { Test: {} };

Obj.Test.Test2 = () => {
  useEffect(() => {});
}; 

How did you test this change?

  • I added new tests for the new changes.
  • I tested the old & new changes manually on an existing React project.
  • I ran all the test cases locally and compared the old and new values

@amjadorfali amjadorfali changed the title fix: react-hooks/rules-of-hooks should support compound component fix: modified eslint to support Compound React Components Jan 8, 2024
@react-sizebot
Copy link

react-sizebot commented Jan 8, 2024

Comparing: 49439b4...056f647

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 176.11 kB 176.11 kB = 54.85 kB 54.85 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 178.21 kB 178.21 kB = 55.49 kB 55.49 kB
facebook-www/ReactDOM-prod.classic.js = 571.71 kB 571.71 kB = 100.50 kB 100.50 kB
facebook-www/ReactDOM-prod.modern.js = 555.57 kB 555.57 kB = 97.61 kB 97.61 kB
test_utils/ReactAllWarnings.js Deleted 67.55 kB 0.00 kB Deleted 16.54 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.min.js +0.61% 27.00 kB 27.16 kB +0.45% 9.30 kB 9.35 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.min.js +0.61% 27.00 kB 27.16 kB +0.45% 9.30 kB 9.35 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.min.js +0.59% 27.59 kB 27.76 kB +0.49% 9.47 kB 9.52 kB
test_utils/ReactAllWarnings.js Deleted 67.55 kB 0.00 kB Deleted 16.54 kB 0.00 kB

Generated by 🚫 dangerJS against 056f647

@tobias-tengler
Copy link

tobias-tengler commented Jan 22, 2024

Thanks for working on this :)
Can you also include the other test case from #27881 in your test plan?

@sophiebits
Copy link
Collaborator

We don't really recommend writing components like this because it's bad for code splitting…

@tobias-tengler
Copy link

tobias-tengler commented Jan 22, 2024

Yes, but it's not a violation of the rules of hooks and should therefore not be treated as such.
IMO there are valid cases for this pattern, like ComboBox and ComboBox.Item.

@amjadorfali
Copy link
Author

@tobias-tengler @sophiebits @rickhanlonii @gaearon Apologies, I looked at the github issue labeled with Good First Issue, so I assumed this was something that the maintainers of this project wanted to fix

If not, I will close this PR.

Thanks!

@rickhanlonii
Copy link
Member

I think it's ok to support even though the pattern isn't recommended, because if you're going to do this pattern, we should at least lint so you don't have to also turn off the linter for the component.

@amjadorfali can you add more test cases? It should fail for things like Parent.lowerCaseComponent. Check out the other test cases for things to test.

@amjadorfali
Copy link
Author

@rickhanlonii Done. Can you please review again when possible

@gaearon
Copy link
Collaborator

gaearon commented Jan 31, 2024

Hmm. I feel strongly that we should not merge this.

We intentionally decided against cases like this when designing the rule and the heuristics.

It blurs the boundaries where it's easy to miss that something is being used as a component. (Extending it to Hooks is especially problematic because people start declaring them as class methods or object methods — and without extending it to Hooks, you'd have an inconsistent approach.)

If you like this pattern (which we do not recommend as @sophiebits mentioned), you can always do it like this:

const Obj = { Test: {} };

const Test2 = () => {
  useEffect(() => {});
}; 

Obj.Test.Test2 = Test2;

That serves as a sufficient escape hatch while keeping component definitions agreeable with the current rule.

@gaearon gaearon closed this Jan 31, 2024
@amjadorfali
Copy link
Author

@gaearon It's an honor to me that you read my PR, thank you! ❤️

@gaearon
Copy link
Collaborator

gaearon commented Jan 31, 2024

Haha no worries, I'm sorry for the churn!

@gaearon
Copy link
Collaborator

gaearon commented Jan 31, 2024

To clarify my concern.

If you allow this:

const Obj = { Test: {} };

Obj.Test.Test2 = () => {
  useEffect(() => {});
};

Then it's hard to justify why you shouldn't also allow this:

const Obj = {
  Test: {
    Test2: () => {
      useEffect(() => {});
    }
  }
};

and this:

const Obj = {
  Test: {
    Test2() {
      useEffect(() => {});
    }
  }
};

and this:

const Obj = {
  Test: {
    useTest2() {
      useEffect(() => {});
    }
  }
};

or even this:

class Obj {
  useTest() {
    useEffect(() => {});
  }
};

which is terrible because it's easy to write super broken code this way.

It's good that the linter tries to steer you away from it the moment you abandon writing plain functions and try to think of them as "methods" — and that it pushes you back into thinking of them as functions. You can still attach them after the definition if needed. But React has no concept of "compound components" and does not recommend such. (This concept doesn't work with lazy or with "use client" which is a good indication it's not a first-class concept to be encouraged.)

@amjadorfali
Copy link
Author

@gaearon Thanks for the clarification, i understand the issue now.

@gaearon
Copy link
Collaborator

gaearon commented Jan 31, 2024

One approximation of the concept of "compound components" that doesn't rely on attaching functions to each other and that works with all React features is to use the module system.

export default function Dropdown() {
  // ...
}

export function Item() {
  // ...
}
import Dropdown, { Item } from './dropdown'

<Dropdown>
  <Item />
</Dropdown>

Or:

export function Box() {
  // ...
}

export function Item() {
  // ...
}
import * as Dropdown from './dropdown'

<Dropdown.Box>
  <Dropdown.Item />
</Dropdown.Box>

Then you're just using the module system and all features work.

@rickhanlonii
Copy link
Member

rickhanlonii commented Feb 11, 2024

It's good that the linter tries to steer you away from it the moment you abandon writing plain functions and try to think of them as "methods" — and that it pushes you back into thinking of them as functions.

@gaearon yeah this makes sense to not support, but I think the error message could be improved then, because it doesn't steer you away from this specific pattern. Here's the error:

React Hook "useState" is called in function "A.B" that is neither a React function component
nor a custom React Hook function. React component names must start with an uppercase letter.
React Hook names must start with the word "use".

I can totally understand that anyone reading this error with this pattern would think it's a bug, because the function satisfies all of the requirement listed. If we want to say that this pattern not a "React function component" then this error could say something like:

React Hook "useState" is called in function "A.B".  React function components cannot be nested
inside of objects or classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: react-hooks/rules-of-hooks should support compound component pattern
7 participants