Description
I'd like to propose a new rule for a testing-library
that I do not believe is currently covered in this plugin. I'm not sure if it would be part of testing-library/react
rules or if a new set of rules (testing-library/react-hooks
) should be introduced. Reading this, it's very possible that people will be using both @testing-library/react
and @testing-library/react-hooks
in the same project and want to have it in the same eslint config, so not sure how it would be best structured.
There is a common mistake people make when using @testing-library/react-hooks
that should be pretty simple to lint for. The issue looks like this:
const { result } = renderHook(() => useCounter());
const { value, increment } = result.current;
expect(value).toBe(0);
act(() => {
increment();
})
expect(value).toBe(1);
Assuming useCounter
works as intended, this test would fail because they have destructured result.current.value
, effectively locking its value to whatever it was when initially rendered.
(To be clear, people generally aren't testing the value before the act
they are just asking why it's wrong at the final expect
, but I wanted to be clear about where it goes wrong)
The test should be written as:
const { result } = renderHook(() => useCounter());
expect(result.current.value).toBe(0);
act(() => {
result.current.increment();
})
expect(result.current.value).toBe(1);
This allows result.current.value
to be updated when the internal component renders.
This logic is true for increment
as well as this test would also fail because increment
is using a stale closure for the update:
const { result } = renderHook(() => useCounter());
const { increment } = result.current;
expect(result.current.value).toBe(0);
act(() => {
increment();
})
expect(result.current.value).toBe(1);
act(() => {
increment();
})
expect(result.current.value).toBe(2);
There would be very few cases where destructuring any value from result.current
is needed. The only one that I can think of is if you wanted to capture the initial value to test that the new value was different (e.g. a randomly generated value), but in my experience, this is not very common. However, if the test does not perform any updates, the test could still pass, therefore making the destructuring technically valid, but in those cases, inlining result.current
would also work.
Let me know what you think, if it's possible and if I can help in any way towards implementing it. I've never worked on an eslint plugin before, but I'm willing to learn.
Activity
Belco90 commentedon May 5, 2021
Hey @mpeyper! Thanks for creating this issue, a really interesting idea (I've made this mistake many times).
I think it could be included in this plugin, but there are several things to handle:
dom
andreact
one since they are exclusive between them. We can update that message to indicatereact-hooks
is an additional one that can be enabled together withreact
one.react-hooks
Shareable Config and enable this new rule. We need to make sure we mention in the rule's doc that it's exclusive for that particular Testing Library framework (not the first time this happens, here you have an example of a rule exclusive for some framework)@testing-library/react
and@testing-library/react-hooks
in the same file right? (this could be a potential rule? 😅 ) Assuming this, we wouldn't need to change anything here for a first approach but would be interesting to improve it in the future to differentiate between Testing Library packages and react-hooks onerenderHook
andresult
tho (making sure the existing helpers for detectingrender
related stuff don't conflict with them, otherwiserenderHook
would be reported by Testing Libraryrender
related rules)Sorry, thinking a little bit out loud here! To sum it up: I think it makes sense to include rules related to
@testing-library/react-hooks
in this plugin (actually pretty excited about it), but we need to improve the base detection mechanisms + new Shareable Config to do so.What do you think?
mpeyper commentedon May 5, 2021
Sounds good to me. I'm not sure where to start, but let me know how I can help.
While it would be uncommon, there's technically no reason why you can't. I have seen examples of context providers being rendered with RTL and hooks consuming the context being rendered with RHTL in the same file, or making their own render helpers around
render
andrenderHook
what use common providers for both that all live together.Belco90 commentedon May 5, 2021
I think you can start by creating the necessary rule files first (it's a shame we don't have the scaffolding in place for it yet, but CONTRIBUTING should guide you through it) and extending
SUPPORTED_TESTING_FRAMEWORKS
to includereact-hooks
one (you'll have to update all existing rule configs to set it asfalse
). When you have that in place, you can implement the rule itself, and then we will make sure the current detection mechanisms are updated as mentioned beforeYou have tons of different utils in the codebase already:
node-utils
: this module contains several functions that will help you visiting AST nodes, navigating through them, checking their type etchelpers
: this bunch of helpers is injected as the 3rd argument increate
method of your rule when creating it withcreateTestingLibraryRule
. These helpers will help you to check things related to Testing Library: is this Identifier arender
util? is that node afindBy*
query? etc. I don't think there is any helper you can reuse since none of them aim to detect render-hooks stuff. However, this is where we will need to add the new detection helpers forrenderHook
andresult
, but it's ok for now if you just try to detect them within your rule itself, and then I'll help you to move it here.Additionally, I highly recommend you to have AST Explorer open so you can figure out the proper AST structure etc.
Are you happy with this approach?
Then we definitely need to separate current Testing Library imports detection from React Hooks Testing Library imports. We will figure that out when you have made some progress in the rule itself and we start moving things to detection helpers.
mpeyper commentedon May 5, 2021
Thanks for the pointers. I'll try to get started on this soon, but if anyone else reading this wants to take it on, please feel free to do so. I'm also available to give input, opinions and reviews for it if that helps.
Belco90 commentedon May 6, 2021
Cool! I'm busy with other issues at the moment, but I can take care of this one when I handle all of them.
Leaving here more pointers for the rule implementation:
no-destructure-result-current
VariableDeclarator
withid
of typeObjectPattern
init
comes from a reportablerenderHook
. It's not enough just checking theinit
is named"result.current"
since 1) that doesn't mean is related torenderHook
and 2) could have been renamedconst { result: { current: increment } } = renderHook(() => useCounter());
mpeyper commentedon May 6, 2021
I know I've used destructuring in all my examples, but the same would be true for a regular old variable declaration:
I assume that would still be a
VariableDeclarator
but with a differentid
type?Belco90 commentedon May 6, 2021
Of course, 🤦 that's a really good point!
Indeed, that's a
VariableDeclarator
with anid
of typeIdentifier
.You can compare both here: https://astexplorer.net/#/gist/4fc3a82a692a2a82cab52e7c8371b2bb/66e9e82eab4a9814cadd22631364847b0e2ad1bc
stale commentedon Jul 5, 2021
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
mpeyper commentedon Jul 5, 2021
This is still on my todo list, but I haven't had much time to look at it recently and not sure when I will have time.