I have an expo react-native web component that renders fine on web but fails under test:
import React, { useState, useEffect } from "react";
import { View } from "react-native";
const timePickerWeb = (date: Date, setDate: React.Dispatch<React.SetStateAction<Date>>) => {
const getHours = (date.getHours() % 12 || 12).toString()
const [hours, setHours] = useState(getHours)
useEffect(() => {
let newdate = date
newdate.setHours(parseInt(hours))
setDate(newdate)
}, [hours])
return (
<View testID="webTimePicker" style={{justifyContent:"center", flexDirection:"row"}}>
<TimeSelector hours={hours} setHours={setHours} />
</View>
)
}
export default timePickerWeb;
test:
import { screen, render } form '@testing-library/react-native';
import timePickerWeb from './timePickerWeb'
it('renders the web picker', () => {
const mockSetEventDate = jest.fn();
const testDate = new Date(1680854389743);
const tree = render(timePickerWeb(testDate, mockSetEventDate)).toJSON();
expect(tree).toMatchSnapshot();
expect(screen.getByTestId('webTimePicker')).toBeTruthy();
});
this fails when it gets to the first useState call in timePickerWeb:
console.error
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
4 | const timePickerWeb = (date: Date, setDate: React.Dispatch<React.SetStateAction<Date>>) => {
5 | const getHours = (date.getHours() % 12 || 12).toString()
> 6 | const [hours, setHours] = useState(getHours)
| ^
7 |
8 | useEffect(() => {
9 | let newdate = date
TypeError: Cannot read properties of null (reading 'useState')
4 | const timePickerWeb = (date: Date, setDate: React.Dispatch<React.SetStateAction<Date>>) => {
5 | const getHours = (date.getHours() % 12 || 12).toString()
> 6 | const [hours, setHours] = useState(getHours)
| ^
7 |
8 | useEffect(() => {
9 | let newdate = date
This is only a problem when testing this component. Other components that have useState or useEffect in them pass their tests without issue. When I remove the useState and useEffect then it works. I don't think this is a hooks issue because if I add useContext or useNavigation (without useState or useEffect) then there is no issue.
I checked for multiple versions of react with npm ls react
and found that I did have 18.1.0 for everything expect react
as a dependency of react-test-renderer
as a dependency of jest-expo
. I fixed that by changing jest-expo
from 48 to 47 and adding peerDependencies
for react: 18.1
to my package.json file. There is no 18.2 in the npm ls react
or in package-lock
.
There's a few things going on here, so let's pick through them.
As the error message is reporting, hooks can only be called inside the body of a function component.
Your function timePickerWeb
is not correctly-defined to be used as a function component:
const timePickerWeb = (date: Date, setDate: React.Dispatch<React.SetStateAction<Date>>) =>
{ /* ... */ }
A regular function component should take a single argument – an object containing the props. It's also recommended that the name of the component starts with a capital letter – this allows React to distinguish custom components from the base HTML/SVG tags (which start with a lowercase letter). So, you might define your component with:
const TimePickerWeb = (
{ date, setDate }:
{ date: Date, setDate: React.Dispatch<React.SetStateAction<Date>> }) =>
{ /* ... */ }
In your test, you call:
const tree = render(timePickerWeb(testDate, mockSetEventDate)).toJSON();
This isn't compatible with the revised definition of TimePickerWeb
. We can update it to match:
const tree = render(TimePickerWeb({
date: testdate, setDate: mockSetEventDate
})).toJSON();
... but we'll still get the same error about hooks, because TimePickerWeb
is being used as a function, not as a component. To use it as a component, just utilise the familiar JSX syntax:
const tree = render(
<TimePickerWeb date={testdate} setDate={mockSetEventDate} />
).toJSON();
At this point we get a different error –
...(...).toJSON is not a function
This is an easier one to fix. toJSON()
is used by react-test-renderer
to prepare snapshots, but you're using render()
from the React Native Testing Library, which can use the render result directly for snapshotting:
const tree = render(
<TimePickerWeb date={testdate} setDate={mockSetEventDate} />
);
At this point you hopefully have a passing test, although you'll presumably need to patch the rest of your code to match the updated interface to the TimePickerWeb
component.
Just to wrap up – it's reasonable to wonder why the incorrectly-defined component works okay on web but fails in your test.
The short story is that when you're calling timePickerWeb(..)
from another component (say ComponentX
), React treats the hook calls in timePickerWeb
as though they were being called directly in ComponentX
. In your test though you're not rendering another component around the call to timePickerWeb
, so React reports the hook abuse error.
It's possible to go into a lot more detail on hook mechanics but hopefully this answer should get you on the right track.