I have a small problem with act() error in react-testing-library. In useEffect I try to call an function that is a promise. Promise returns some data and displays it, but even if the tests pass, the act error is still displayed.
Component:
export function getUser() {
return Promise.resolve({ name: "Json" });
}
const Foo = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const loadUser = async () => {
const userData = await getUser();
setUser(userData);
};
loadUser();
}, []);
return (
<div>
<p>foo</p>
{user ? <p>User is: {user.name}</p> : <p>nothing</p>}
</div>
);
};
Also I have my Foo component inside of App Component, like that:
import Foo from "./components/Foo";
function App() {
return (
<div>
some value
<Foo />
</div>
);
}
export default App;
TEST:
test("should display userName", async () => {
render(<Foo />);
expect(screen.queryByText(/User is:/i)).toBeNull();
expect(await screen.findByText(/User is: JSON/i)).toBeInTheDocument();
});
Do You have any ideas to resolve it?
EDIT:
here's an error message
console.error
Warning: An update to Foo 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 */
Instead of act use waitFor to let the rendering resolve.
import { render, waitFor } from '#testing-library/react-native';
import { useEffect, useState } from 'react';
import { Text } from 'react-native';
describe('useState', () => {
it('useState', () => {
function MyComponent() {
const [foo] = useState('foo');
return <Text testID="asdf">{foo}</Text>;
}
const { getByTestId } = render(<MyComponent></MyComponent>)
expect(getByTestId("asdf").props.children).toBe("foo");
});
it('useState called async', async () => {
function MyComponent() {
const [foo, setFoo] = useState('foo');
useEffect(() => {
(async () => {
setFoo(await Promise.resolve('bar'))
})()
}, []);
return <Text testID="asdf">{foo}</Text>;
}
const {getByTestId} = await waitFor(()=>render(<MyComponent></MyComponent>))
expect(getByTestId("asdf").props.children).toBe("bar");
});
});
In addition to the above answer for situations where the update does not occur immediately, (i.e. with a timeout) you can use await act(()=>Promise.resolve()); (note this generates a warning due improper typing but the await is needed for it to work.
Here's an example with renderHook
it("should return undefined", async () => {
const { result } = renderHook(() => {
const [loaded, loadedFonts] = useExpoFonts([]);
return useReplaceWithNativeFontCallback(loaded, loadedFonts);
}, {});
const replaceWithNativeFont0 = result.current;
expect(replaceWithNativeFont0({})).toBeUndefined();
await act(() => Promise.resolve());
const replaceWithNativeFont1 = result.current;
expect(replaceWithNativeFont1({})).toBeUndefined();
});
And with the typical render
it("should render fonts", async () => {
function MyComponent() {
const [loaded, loadedFonts] = useExpoFonts([]);
const replaceWithNativeFont = useReplaceWithNativeFontCallback(
loaded,
loadedFonts
);
const style = replaceWithNativeFont({
fontFamily: "something",
fontWeight: "300",
});
return <View testID="blah" style={style} />;
}
const { unmount } = render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
await act(() => Promise.resolve());
expect(screen.getByTestId("blah").props.style).toStrictEqual({
fontFamily: "something",
fontWeight: "300",
});
unmount();
});
Related
Say I have a hook function that provides a useEffect
export function useTokenCheckClock(authState: AuthState, timeout: number = 60) {
const [lastCheckTime, updateLastCheckTime] =
useReducer(updatePerSecondReducer, Math.ceil(Date.now()/1000) * 1000);
useEffect(()=>{
... do something ...
return ()=> { ... do cleanup ... }
}, [authState, timeout] // <-- trying to determine if I need this
}
And a component uses this function as:
function MyComponent() {
const [authState, setAuthState] = useState(...);
useTokenCheckClock(authState);
... some handler logic or useEffect that may alter authState ...
return <>...</>
}
When the authState changes and triggers a render. Would the following occur?
the useEffect hook cleanup function in useTokenCheckClock is called
then the useEffect hook in useTokenCheckClock is called again
It needs it otherwise the effects won't trigger when the parameters change.
This is demonstrated by the following Jest file
import { cleanup, fireEvent, render } from '#testing-library/react-native';
import React, { useEffect, useState } from 'react';
import { Pressable, Text } from 'react-native';
afterEach(cleanup);
beforeEach(() => {
jest.useFakeTimers({ advanceTimers: true });
});
it("does not work with empty dep list", () => {
const useEffectCallback = jest.fn();
const useEffectCleanup = jest.fn();
const rendered = jest.fn();
function useMyHook(state: string, nonState: number) : number{
const [ myState, setMyState ] = useState(Date.now());
useEffect(()=>{
useEffectCallback();
setMyState(Date.now());
return () => useEffectCleanup();
}, [])
return myState;
}
function MyComponent() {
const [authState, setAuthState] = useState("AuthState.INITIAL");
const lastCheckTime = useMyHook(authState, 10);
rendered({authState, lastCheckTime});
return <Pressable onPress={() => { setAuthState("AuthState.AUTHENTICATED") }} ><Text testID="lastCheckTime">{lastCheckTime}</Text></Pressable>
}
jest.setSystemTime(new Date("2025-01-01T20:00:00Z"));
const {getByTestId, unmount} = render(<MyComponent />);
expect(getByTestId("lastCheckTime")).toHaveTextContent("1735761600000")
expect(useEffectCallback).toBeCalledTimes(1)
expect(useEffectCleanup).toBeCalledTimes(0)
expect(rendered).toBeCalledTimes(1)
jest.advanceTimersByTime(1000);
fireEvent.press(getByTestId("lastCheckTime"))
// remains the same
expect(getByTestId("lastCheckTime")).toHaveTextContent("1735761600000")
expect(useEffectCallback).toBeCalledTimes(1)
expect(useEffectCleanup).toBeCalledTimes(0)
// rendered still because of auth state change
expect(rendered).toBeCalledTimes(2)
unmount();
expect(useEffectCallback).toBeCalledTimes(1)
expect(useEffectCleanup).toBeCalledTimes(1)
expect(rendered).toBeCalledTimes(2)
})
it("works with dep list", () => {
const useEffectCallback = jest.fn();
const useEffectCleanup = jest.fn();
const rendered = jest.fn();
function useMyHook(state: string, nonState: number) : number{
const [ myState, setMyState ] = useState(Date.now());
useEffect(()=>{
useEffectCallback();
setMyState(Date.now());
return () => useEffectCleanup();
}, [state, nonState])
return myState;
}
function MyComponent() {
const [authState, setAuthState] = useState("AuthState.INITIAL");
const lastCheckTime = useMyHook(authState, 10);
rendered({authState, lastCheckTime});
return <Pressable onPress={() => { setAuthState("AuthState.AUTHENTICATED") }} ><Text testID="lastCheckTime">{lastCheckTime}</Text></Pressable>
}
jest.setSystemTime(new Date("2025-01-01T20:00:00Z"));
const {getByTestId, unmount} = render(<MyComponent />);
expect(getByTestId("lastCheckTime")).toHaveTextContent("1735761600000")
expect(useEffectCallback).toBeCalledTimes(1)
expect(useEffectCleanup).toBeCalledTimes(0)
expect(rendered).toBeCalledTimes(1)
jest.advanceTimersByTime(1000);
fireEvent.press(getByTestId("lastCheckTime"))
expect(getByTestId("lastCheckTime")).toHaveTextContent("1735761601000")
expect(useEffectCallback).toBeCalledTimes(2)
expect(useEffectCleanup).toBeCalledTimes(1)
// authenticated which then calls set state to trigger two renders
expect(rendered).toBeCalledTimes(3)
unmount();
expect(useEffectCallback).toBeCalledTimes(2)
expect(useEffectCleanup).toBeCalledTimes(2)
expect(rendered).toBeCalledTimes(3)
})
Hello I'm trying to test a call of a function that is inside a setTimeout. This is my code:
const CreateContact = (): JSX.Element => {
const navigate = useNavigate();
const { createContact } = useContactsApi();
const [formData, setFormData] = useState(formDataInitialState);
const handleSubmit = async (event: SyntheticEvent) => {
event.preventDefault();
try {
await createContact(formData);
setTimeout(() => {
navigate("/home");
}, 2500);
} catch (error) {}
};
// More code...
};
I have made many tries and I can't pass the coverage on the line where is called navigate("/home");.
Here is my last test:
const navigate = jest.fn();
beforeEach(() => {
jest.spyOn(router, "useNavigate").mockImplementation(() => navigate);
});
test("Then it should call the navigate function", async () => {
const newText = "test#prove";
render(
<MemoryRouter>
<Provider store={store}>
<CreateContact />
</Provider>
</MemoryRouter>
);
const form = {
name: screen.getByLabelText("Name") as HTMLInputElement,
};
await userEvent.type(form.name, newText);
const submit = screen.getByRole("button", { name: "Create contact" });
await userEvent.click(submit);
jest.runAllTimers();
setTimeout(() => {
expect(navigate).toHaveBeenCalledTimes(1);
}, 4000);
});
The test runs ok, but the coverage doesn't pass inside the setTimeout.
First i would mock the useNavigate hook in your test file.
const navigateMock = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => navigateMock
}));
Then in your test i would simply check if navigateMock is called, without using any setTimout, since you call jest.runAllTimers()
test("Then it should call the navigate function", async () => {
const newText = "test#prove";
render(
<MemoryRouter>
<Provider store={store}>
<CreateContact />
</Provider>
</MemoryRouter>
);
const form = {
name: screen.getByLabelText("Name") as HTMLInputElement,
};
await userEvent.type(form.name, newText);
const submit = screen.getByRole("button", { name: "Create contact" });
await userEvent.click(submit);
jest.runAllTimers();
expect(navigateMock).toHaveBeenCalledTimes(1);
});
I would like to run customFunction only when customEffect has finished setting isReady state. And customFunction should only run once no matter if the isReady was set to false or true as long as it was ran after it was set.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
customFunction();
}, []);
return (
<>Hello World</>
)
}
I tried to add isReady as second useEffect argument, but then my customFunction is being run before the customEffect finishes and then again after the isReady is being set.
Also tried having in a separate useEffect, but still seems to run before the customEffect finishes.
Set initial value to null and use separate useEffect as Kevin suggested (only without checking isReady true/false).
In this case setIsReady will change isReady from null to true/false and the second useEffect will be called.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(null);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response);
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (null === isReady) {
return;
}
customFunction();
}, [isReady]);
return (
<>Hello World</>
)
}
Since you want to cue an effect to run after the isReady state is set, and the value of isReady is irrelevant you can to use a second state value to indicate the first effect and state update has completed.
This will trigger the second effect to invoke customFunction but you don't want your component to remain in this state as from here any time the component rerenders the conditions will still be met. You'll want a third "state" to indicate the second effect has been triggered. Here you can use a React ref to indicate this.
export const smallComponent = () => {
const [readySet, setReadySet] = useState(false);
const [isReady, setIsReady] = useState(false);
const customFunctionRunRef = useRef(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
setReadySet(true); // to trigger second effect callback
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (readySet && !customFunctionRunRef.current) {
// won't run before readySet is true
// won't run after customFunctionRunRef true
customFunction();
customFunctionRunRef.current = true;
}
}, [readySet]);
return (
<>Hello World</>
);
}
Better solution borrowed from #p1uton. Use null isReady state to indicate customFunction shouldn't invoke yet, and the ref to keep it from being invoked after.
export const smallComponent = () => {
const [isReady, setIsReady] = useState(null);
const customFunctionRunRef = useRef(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (isReady !== null && !customFunctionRunRef.current) {
// won't run before isReady is non-null
// won't run after customFunctionRunRef true
customFunction();
customFunctionRunRef.current = true;
}
}, [isReady]);
return (
<>Hello World</>
);
}
I'm not sure if I understood you correctly, but this is how I would use a separate useEffect.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response);
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (!isReady) {
return;
}
customFunction();
}, [isReady]);
return (
<>Hello World</>
)
}
Have you tried using this package, isMounted?
I used that in my projects.
import React, { useState, useEffect } from 'react';
import useIsMounted from 'ismounted';
import myService from './myService';
import Loading from './Loading';
import ResultsView from './ResultsView';
const MySecureComponent = () => {
const isMounted = useIsMounted();
const [results, setResults] = useState(null);
useEffect(() => {
myService.getResults().then(val => {
if (isMounted.current) {
setResults(val);
}
});
}, [myService.getResults]);
return results ? <ResultsView results={results} /> : <Loading />;
};
export default MySecureComponent;
https://www.npmjs.com/package/ismounted
I'm trying to render a header.
First, in InnerList.js, I make an API call, and with the data from the API call, I set a list in context.
Second, in Context.js, I take the list and set it to a specific data.
Then, in InnerListHeader.js, I use the specific data to render within the header.
Problem: I currently get a TypeError undefined because the context is not set before rendering. Is there a way to wait via async or something else for the data to set before loading?
My code block is below. I've been looking through a lot of questions on StackOverflow and blogs but to no avail. Thank you!
InnerList.js
componentDidMount() {
const { dtc_id } = this.props.match.params;
const {
setSpecificDtcCommentList,
} = this.context;
MechApiService.getSpecificDtcCommentList(dtc_id)
.then(res =>
setSpecificDtcCommentList(res)
)
}
renderSpecificDtcCommentListHeader() {
const { specificDtc = [] } = this.context;
return (
<InnerDtcCommentListItemHeader key={specificDtc.id} specificDtc={specificDtc} />
)
}
Context.js
setSpecificDtcCommentList = (specificDtcCommentList) => {
this.setState({ specificDtcCommentList })
this.setSpecificDtc(specificDtcCommentList)
}
setSpecificDtc = (specificDtcCommentList) => {
this.setState({ specificDtc: specificDtcCommentList[0] })
}
InnerListHeader.js
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
In general, you should always consider that a variable can reach the rendering stage without a proper value (e.g. unset). It is up to you prevent a crash on that.
For instance, you could rewrite you snippet as follows:
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{Boolean(specificDtc.dtc_id) && specificDtc.dtc_id.dtc}
</div>
</div>
);
}
When you make an api call you can set a loader while the data is being fetched from the api and once it is there you show the component that will render that data.
In your example you can add a new state that will pass the api call status to the children like that
render() {
const { specificDtc, fetchingData } = this.props;
if (fetchingData){
return <p>Loading</p>
}else{
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
}
``
in my case, i am calling external api to firebase which lead to that context pass undefined for some values like user. so i have used loading set to wait untile the api request is finished and then return the provider
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
updateProfile
} from 'firebase/auth';
import { auth } from '../firebase';
import { useNavigate } from 'react-router';
import { create_user_db, get_user_db } from 'api/UserAPI';
import { CircularProgress, LinearProgress } from '#mui/material';
import Loader from 'ui-component/Loader';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState();
const [user_db, setUserDB] = useState();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const createUser = async (email, password) => {
const user = await createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const googleSignIn = async () => {
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const logout = () => {
setUser();
return signOut(auth).then(() => {
window.location = '/login';
});
};
const updateUserProfile = async (obj) => {
await updateProfile(auth.currentUser, obj);
return updateUser(obj);
};
const updateUser = async (user) => {
return setUser((prevState) => {
return {
...prevState,
...user
};
});
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
setLoading(true);
if (currentUser) {
const user_db = await get_user_db({ access_token: currentUser.accessToken });
setUserDB(user_db);
setUser(currentUser);
setIsAuthenticated(true);
}
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
if (loading) return <Loader />;
return (
<UserContext.Provider value={{ createUser, user, user_db, isAuthenticated, logout, signIn, googleSignIn, updateUserProfile }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
I need help because I get the following error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method. in createCategory (at themeProvider.js:39)
/* Imports */
import React, { useContext, useState, useEffect } from 'react';
import AsyncStorage from '#react-native-community/async-storage';
import THEMES from '#app/theme/themes.json';
/* /Imports/ */
const STORAGE_KEY = 'THEME_ID';
const ThemeContext = React.createContext();
/* Exports */
export const ThemeContextProvider = ({ children }) => {
const [themeID, setThemeID] = useState();
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
})();
}, []);
return (
<ThemeContext.Provider value={{ themeID, setThemeID }}>
{!!themeID ? children : null}
</ThemeContext.Provider>
);
};
export function withTheme(Component) {
function TargetComponent(props) {
const { themeID, setThemeID } = useContext(ThemeContext);
const getTheme = themeID => THEMES.find(theme => theme.key === themeID);
const setTheme = themeID => {
AsyncStorage.setItem(STORAGE_KEY, themeID);
setThemeID(themeID);
};
return (
<Component
{...props}
themes={THEMES}
theme={getTheme(themeID)}
setTheme={setTheme}
/>
);
}
TargetComponent.navigationOptions = Component.navigationOptions;
return TargetComponent;
}
/* /Exports/ */
If you don't already know - you can return a function at the end of your useEffect hook. That function will be called whenever that effect is fired again (e.g. when the values of its dependencies have changed), as well as right before the component unmounts. So if you have a useEffect hook that looks like this:
useEffect(() => {
// logic here
return () => {
// clean up
};
}, []); // no dependencies!
Is equivalent to this:
class SomeComponent extends React.Component {
componentDidMount() {
// logic here
}
componentWillUnmount() {
// clean up
}
}
So in your code I'd add this:
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
// fetch logic omitted...
const data = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
} catch (e) {
throw new Error(e)
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [themeID]);
Try this
let unmounted = false;
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (!unmounted) {
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
}
})();
return () => {
unmounted = true;
};
}, []);