I'm writing a test code with Jest for a custom hook in my web application.
It uses Recoil for state management, but the error message appears when I run npm run test.
This is the error message.
This component must be used inside a <RecoilRoot> component.
16 | const useIds = () => {
17 | // const [ids, setIds] = React.useState([]);
> 18 | const [ids, setIds] = useRecoilState(idsState);
| ^
This is the test code.
import * as React from 'react';
import { render, fireEvent } from '#testing-library/react';
import { useIds } from '#/hooks/useIds';
import { RecoilRoot } from 'recoil';
it('unit test for custom hook useIds', () => {
const TestComponent: React.FC = () => {
const ids = useIds();
return (
<RecoilRoot>
<div title='ids'>{ ids }</div>
</RecoilRoot>
)
}
const { getByTitle } = render(<TestComponent />);
const ids = getByTitle('ids');
})
This is the custom hook code
import * as React from 'react';
import { useRouter } from 'next/router';
import { atom, useRecoilState } from 'recoil';
import { fetchIdsByType } from '#/repositories';
const initialState: {
[type: string]: number[];
} = {};
export const idsState = atom({
key: 'idsState',
default: initialState,
});
const useIds = () => {
const [ids, setIds] = useRecoilState(idsState);
const router = useRouter();
const { type } = router.query;
React.useEffect(() => {
if (router.asPath !== router.route) {
// #ts-ignore
fetchIdsByType(type).then((ids: number[]) => {
setIds((prevState) => {
return {
...prevState,
// #ts-ignore
[type]: ids,
};
});
});
}
}, [router]);
// #ts-ignore
return ids[type];
};
export { useIds };
I know why the error is happening but I have no idea where the RecoilRoot should be in?
You might need to put where to wrap the component which is using your custom hook as following:
it('unit test for custom hook useIds', () => {
const TestComponent: React.FC = () => {
const ids = useIds();
return (
<div title='ids'>{ ids }</div>
)
}
const { getByTitle } = render(
// Put it here to wrap your custom hook
<RecoilRoot>
<TestComponent />
</RecoilRoot>
);
const ids = getByTitle('ids');
})
Related
What I want to do
We have created hooks that communicate useMutation and switch the display when a button is clicked in a React component.
This test code is created using jest.
Occurring problems
Error: Uncaught [TypeError: Cannot read properties of undefined (reading '0')]
Source code in question
# Todo.jsx
import React, { useState } from 'react';
export const Todo = () => {
const [token, setToken] = useState('')
const [mutateFunction] = useMutation(CREATE_TOKEN);
const changeTodo = async (agreement) => {
const createTokenData = await mutateFunction();
if (createTokenData.data?.token === null) {
setToken('')
return;
}
setToken(createTokenData.data?.token)
}
};
return (
<div>
<button onClick={() => changeTodo(true)}>
Change Todo
</button>
</div>
)
};
# Todo.test.jsx
import React from 'react';
import { MockedProvider } from '#apollo/client/testing';
import { render, screen, fireEvent } from '#testing-library/react';
import { Todo } from 'Todo'
jest.mock('#apollo/client');
describe('Click the button', () => {
test('The value of token is null', async () => {
const mocks = [
{
request: {
query: CREATE_TOKEN,
},
result: {
data: {
createPcfToken: null
},
},
},
];
render(
<MockedProvider mocks={mocks} addTypename={false}>
<Todo />
</MockedProvider>
);
const button = screen.getByRole('button');
fireEvent.click(button);
});
});
What we tried
I am creating it while referring to the apollo official website, but I cannot receive data from mutateFunction.
I have tried everything but it just fails. What should I do?
Two mockImplementation solved the problem!
const mocks = {
{
data: {
token: null
},
};
}
const mockMutation = jest.fn()
(useMutation as jest.Mock).mockImplementation(() => [mockMutation]);
(mockMutation as jest.Mock).mockImplementation(() => mocks);
I'm having the following problem with my react + nextJS project...
The component is something like this:
import React, { FC, useCallback, useEffect, useState } from 'react';
import InputMask, { Props } from 'react-input-mask';
import {
getPayersDetails,
PayerCompany,
PayerContact,
PayerDocuments,
} from 'services';
import { Formik } from 'formik';
import { Field, Loading, Page, Tooltip } from 'components';
import { Button, IconButton, Typography } from '#mui/material';
import { TextField, TextFieldProps } from '#material-ui/core';
import { SvgSelfCheckout } from 'images';
import {
FaEdit,
FaFileInvoiceDollar,
FaUserCheck,
FaUserAltSlash,
} from 'react-icons/fa';
import theme from 'styles/theme';
import * as S from './styles';
import { useRouter } from 'next/router';
import { PAYER_HOME } from 'src/routes';
const PayersDetails: FC = () => {
const [payerCompany, setPayerCompany] = useState<PayerCompany[]>([]);
const [payerContact, setPayerContact] = useState<PayerContact[]>([]);
const [payerDocument, setPayerDocument] = useState<PayerDocuments[]>([]);
const [loading, setLoading] = useState(true);
const [isActivePayer, setIsActivePayer] = useState(false);
const router = useRouter();
const getPayerDetails = useCallback(async (payerId: number) => {
setLoading(true);
const payerDetails = await getPayersDetails(payerId);
setPayerCompany(payerDetails.payerCompany);
setPayerDocument(payerDetails.payerDocument);
setPayerContact(payerDetails.payerContact);
setLoading(false);
}, []);
useEffect(() => {
if (!router.isReady) {
return;
}
const payerId = router.query.payerId as string;
try {
const safePayerId = parseInt(payerId);
getPayerDetails(safePayerId);
} catch (e) {
router.push(PAYER_HOME);
return;
}
}, [getPayerDetails, router]);
const contact = payerContact.length > 0 ? payerContact[0] : null;
const mobilePhone = payerContact
.filter(contact => contact.contactType.contactTypeName === 'mobile')
.map(contact => contact.value)[0];
return (
<Page
title="Detalhes do pagador"
pageTitle="Detalhes do pagador"
pageSubtitle="Dados pessoais"
pageSubitleColor={`${theme.palette.primary.light}`}
>
{loading ? (
<Loading show={loading} />
) : (
<Formik
enableReinitialize={true}
initialValues={{
name: contact?.contactName,
email: contact?.value,
}}
onSubmit={() => console.log('onSumit')}
>
....
)}
</Formik>
)}
</Page>
);
};
export default PayersDetails;
And I'm trying to test it with the following code:
import { render, screen, fireEvent } from '#testing-library/react';
import { getCompany } from 'services/companies';
import { getPayersDetails } from 'services/payers';
import PayersImport from '.';
import { useRouter } from 'next/router';
import userEvent from '#testing-library/user-event';
jest.mock('services/payers', () => ({
__esModule: true, // this property makes it work
default: 'mockedDefaultExport',
getPayersDetails: jest.fn(),
}));
jest.mock('next/router', () => ({
useRouter: jest.fn().mockImplementation(() => ({
route: '/',
pathname: '',
query: '',
asPath: '',
})),
}));
describe('payers details layout', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
describe('when rendering', () => {
let getPayersDetailsMock;
beforeEach(() => {
getPayersDetailsMock = {
...
};
getPayersDetails.mockResolvedValue(getPayersDetailsMock);
useRouter.mockImplementation(() => ({
route: '/',
pathname: '',
isReady: true,
query: { payerId: 1 },
asPath: '',
}));
render(<PayersImport />);
});
it('Calls details api with the correct id', () => {
expect(getPayersDetails).toHaveBeenCalledWith(1);
});
});
});
The issue is:
The component load ok when we go to a browser, but when I run it on jest I get the following error:
console.error
Warning: An update to PayersDetails inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at PayersDetails (/home/thiago/finnet/repos/welcome/apps/lunapay-front/src/layouts/Payers/PayersDetails/index.tsx:29:43)
43 | setPayerContact(payerDetails.payerContact);
44 |
> 45 | setLoading(false);
| ^
46 | }, []);
47 |
48 | useEffect(() => {
I get it is only a warning and it wouldn't be a problem for me, but the issue is that it goes into a infinite loop trying to render again.
What am I doing wrong??
I solved the issue, but it was not the best solution.
Basically the render was being triggered by the <Loading /> component being rendered again.
What I did was just adding a new property telling me if it was already fetched and canceling the loop.
const [payerDetailsResponse, setPayerDetailsResponse] =
useState<PayerDetailsResponse | null>(null);
const [payerCompany, setPayerCompany] = useState<PayerCompany[]>([]);
const [payerContact, setPayerContact] = useState<PayerContact[]>([]);
const [payerDocument, setPayerDocument] = useState<PayerDocuments[]>([]);
const [loading, setLoading] = useState(true);
const [isActivePayer, setIsActivePayer] = useState(false);
const router = useRouter();
const getPayerDetails = useCallback(async (payerId: number) => {
setLoading(true);
const payerDetails = await getPayersDetails(payerId);
setPayerDetailsResponse(payerDetails);
setPayerCompany(payerDetails.payerCompany);
setPayerDocument(payerDetails.payerDocument);
setPayerContact(payerDetails.payerContact);
setLoading(false);
}, []);
useEffect(() => {
if (payerDetailsResponse) {
return;
}
if (!router.isReady) {
return;
}
const payerId = router.query.payerId as string;
try {
const safePayerId = parseInt(payerId);
getPayerDetails(safePayerId);
} catch (e) {
router.push(PAYER_HOME);
return;
}
}, [router, getPayerDetails]);
Sorry not to be able to help more, but that was a solution for my problem :)
Sometimes jest fall into an infinite loop with useEffect, if that is your case, you can make a mock of it
import React from 'react'
const mockUseEffect = jest.fn()
jest.spyOn(React, 'useEffect').mockImplementation(mockUseEffect)
and try with that and/or adapt to your needs
I'm trying to make a simple blog.
What I want to do is conditionally import a specific component based on the url params (id below).
However this code only renders Loading, it never changes. Why is this?
import Layout from "../../components/Layout";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
const LoadingPost = () => {
return <h1>Loading</h1>;
};
const Post = () => {
const router = useRouter();
const { id } = router.query;
const [loaded, setLoaded] = useState(false);
let PostToShow = LoadingPost;
useEffect(() => {
if (id) {
import(`../../posts/${id}.tsx`).then(_ => {
PostToShow = () => _;
setLoaded(true);
});
}
}, [id]);
const renderPost = () => {
if (loaded) {
return <PostToShow />;
}
};
return (
<Layout>
<h1>This would be a post</h1>
<h2>The id of this post would be: {id}</h2>
{renderPost()}
</Layout>
);
};
export default Post;
Here is how you can do this
import(`../../posts/${id}.tsx`).then(module=> {
PostToShow = module.default;
setLoaded(true);
});
You're doing the following:
import(`../../posts/${id}.tsx`).then(_ => {
PostToShow = () => _;
setLoaded(true);
});
So you're changing the value for the PostToShow variable, then setting the state - which triggers a new render. However on ever render you do:
let PostToShow = LoadingPost;
Hence you always render the LoadingPost component.
The issue here is that we're importing a module, not just a component.
import Layout from "../../components/Layout";
import { useEffect, useState } from "react";
const LoadingPost = () => {
return <h1>Loading</h1>;
};
const Post = ({ pageProps }) => {
const { id } = pageProps;
const [loaded, setLoaded] = useState(false);
const [Component, setComponent] = useState(LoadingPost);
useEffect(() => {
if (id) {
import(`../../posts/${id}.tsx`).then(_ => {
setComponent(_);
setLoaded(true);
});
}
}, [id]);
const renderPost = () => {
if (loaded) {
// #ts-ignore
const Comp = Component.default;
// #ts-ignore
return <Comp />;
}
};
return (
<Layout>
<h1>This would be a post</h1>
<h2>The id of this post would be: {id}</h2>
{renderPost()}
</Layout>
);
};
Post.getInitialProps = ctx => {
const { id } = ctx.query;
return {
pageProps: {
id,
},
};
};
export default Post;
As other people mentioned, we can do setComponent(_.default) either, but I couldn't get that working as it gives: Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object..
The above works as intended:
I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}
I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});