i have a simple hook that fetches the value and sets it to option as follows:
import Fuse from 'fuse.js'
import React from 'react'
// prefetches options and uses fuzzy search to search on that option
// instead of fetching on each keystroke
export function usePrefetchedOptions<T extends {}>(fetcher: () => Promise<T[]>) {
const [options, setOptions] = React.useState<T[]>([])
React.useEffect(() => {
// fetch options initially
const optionsFetcher = async () => {
try {
const data = await fetcher()
setOptions(data)
} catch (err) {
errorSnack(err)
}
}
optionsFetcher()
}, [])
// const fuseOptions = {
// isCaseSensitive: false,
// keys: ['name'],
// }
// const fuse = new Fuse(options, fuseOptions)
// const dataServiceProxy = (options) => (pattern: string) => {
// // console.error('options inside proxy call', { options })
// const optionsFromSearch = fuse.search(pattern).map((fuzzyResult) => fuzzyResult.item)
// return new Promise((resolve) => resolve(pattern === '' ? options : optionsFromSearch))
// }
return options
}
i am trying to test it with following code:
import { act, renderHook, waitFor } from '#testing-library/react-hooks'
import { Wrappers } from './test-utils'
import { usePrefetchedOptions } from './usePrefetchedOptions'
import React from 'react'
const setup = ({ fetcher }) => {
const {
result: { current },
waitForNextUpdate,
...rest
} = renderHook(() => usePrefetchedOptions(fetcher), { wrapper: Wrappers })
return { current, waitForNextUpdate, ...rest }
}
describe('usePrefetchedOptions', () => {
const mockOptions = [
{
value: 'value1',
text: 'Value one',
},
{
value: 'value2',
text: 'Value two',
},
{
value: 'value3',
text: 'Value three',
},
]
test('searches for appropriate option', async () => {
const fetcher = jest.fn(() => new Promise((resolve) => resolve(mockOptions)))
const { rerender, current: options, waitForNextUpdate } = setup({ fetcher })
await waitFor(() => {
expect(fetcher).toHaveBeenCalled()
})
// async waitForNextUpdate()
expect(options).toHaveLength(3) // returns initial value of empty options = []
})
})
the problem is when i am trying to assert the options at the end of the test, it still has the initial value of []. However if I log the value inside the hook, it returns the mockOptions. How do I update the hook after it is update by useEffect but in async manner.
I have also tried using using waitForNextUpdate where it is commented in the code. it times out with following error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:
Couple things, currently you're waiting for fetcher to be called in your tests, but the state update actually happens not after fetcher is called but after the promise that fetcher returns is resolved. So you'd need to wait on the resolution of that promise in your test
Also, you're destructuring the value of result.current when you first render your hook. That value is just a copy of result.current after that first render and it will not update after that. To query the current value of options, you should query result.current in your assertion instead.
const fetcherPromise = Promise.resolve(mockOptions);
const fetch = jest.fn(() => fetcherPromise);
const { result } = renderHook(() => usePrefetchedOptions(fetcher), { wrappers: Wrappers })
await act(() => fetcherPromise);
expect(result.current).toHaveLength(3)
Here's what worked for me whenI needed to test the second effect of my context below:
import React, {createContext, useContext, useEffect, useState} from "react";
import {IGlobalContext} from "../models";
import {fetchGravatar} from "../services";
import {fetchTokens, Token} from "#mylib/utils";
const GlobalContext = createContext<IGlobalContext>({} as IGlobalContext);
function useGlobalProvider(): IGlobalContext {
const [token, setToken] = useState<Token>(Token.deserialize(undefined));
const [gravatar, setGravatar] = useState<string>('');
useEffect(() => {
setToken(fetchTokens());
}, []);
useEffect(() => {
if (token?.getIdToken()?.getUsername()) {
fetchGravatar(token.getIdToken().getUsername())
.then(setGravatar)
}
}, [token]);
const getToken = (): Token => token;
const getGravatar = (): string => gravatar;
return {
getToken,
getGravatar
}
}
const GlobalProvider: React.FC = ({children}) => {
const globalContextData: IGlobalContext = useGlobalProvider();
return (
<GlobalContext.Provider value={globalContextData}>{children}</GlobalContext.Provider>
);
};
function useGlobalContext() {
if (!useContext(GlobalContext)) {
throw new Error('GlobalContext must be used within a Provider');
}
return useContext<IGlobalContext>(GlobalContext);
}
export {GlobalProvider, useGlobalContext};
corresponding tests:
import React from "react";
import {GlobalProvider, useGlobalContext} from './Global';
import {act, renderHook} from "#testing-library/react-hooks";
import utils, {IdToken, Token} from "#mylib/utils";
import {getRandomGravatar, getRandomToken} from 'mock/Token';
import * as myService from './services/myService';
import {Builder} from "builder-pattern";
import faker from "faker";
jest.mock('#mylib/utils', () => ({
...jest.requireActual('#mylib/utils')
}));
describe("GlobalContext", () => {
it("should set Token when context loads", () => {
const expectedToken = getRandomToken('mytoken');
const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
const wrapper = ({children}: { children?: React.ReactNode }) => <GlobalProvider>{children} </GlobalProvider>;
const {result} = renderHook(() => useGlobalContext(), {wrapper});
expect(spyFetchToken).toHaveBeenCalled();
expect(result.current.getToken()).toEqual(expectedToken);
})
it("should fetch Gravatar When Token username changes", async () => {
const expectedToken = getRandomToken('mytoken');
const expectedGravatar = getRandomGravatar();
const returnedGravatarPromise = Promise.resolve(expectedGravatar);
const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
const spyFetchGravatar = spyOn(myService, 'fetchGravatar').and.returnValue(returnedGravatarPromise);
const wrapper = ({children}: { children?: React.ReactNode }) =>
<GlobalProvider>{children} </GlobalProvider>;
const {result, waitForValueToChange} = renderHook(() => useGlobalContext(), {wrapper});
// see here
// we need to wait for the promise to be resolved, even though the gravatar spy returned it
let resolvedGravatarPromise;
act(() => {
resolvedGravatarPromise = returnedGravatarPromise;
})
await waitForValueToChange(() => result.current.getGravatar());
expect(spyFetchToken).toHaveBeenCalled();
expect(result.current.getToken()).toEqual(expectedToken);
expect(spyFetchGravatar).toHaveBeenCalledWith(expectedToken.getIdToken().getUsername());
expect(resolvedGravatarPromise).toBeInstanceOf(Promise);
expect(result.current.getGravatar()).toEqual(expectedGravatar);
})
})
Related
Trying to test a status hook that uses a promise that is not getting updated by my test.
screens.OnStart() should trigger setStatus with the value the promise returns.
When I log status it never changes.
import { useEffect, useState } from 'react'
import screens from '#utils/screen'
const useStatus = () => {
const [status, setStatus] = useState()
useEffect(() => {
const listener = screens.OnStart(
"HAPPEN",
({ status }) =>
setStatus(status)
)
return () => {
screens.removeListener("HAPPEN", listener)
}
}, [])
return {
status,
}
}
export default useStatus
Test
import React from 'react'
import { act, renderHook } from '#testing-library/react-hooks'
import useStatus from '#hooks/useStatus'
const mockedOnStart = jest.fn().mockImplementation((event, callback) => callback)
jest.mock('#utils/screens', () => ({
...jest.requireActual('#utils/screens'),
default: {
OnStart: () => mockedOnStart(),
},
__esModule: true,
}))
describe('useStatus', () => {
test('Renders', async () => {
mockedOnStart.mockReturnValueOnce(2)
const { result } = renderHook(() => useStatus())
await act(async () => {
console.log('result = ', result.current.status)
})
})
})
I am experiencing the below error when I introduce useQueryClient? Any ideas why this may be?
I am trying to invalidateQueries for a queryKey onSuccess of the useUpdateEmployee hook.
bundle.js:1427 Uncaught Error: 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:
Component
import { useFetchEmployee, useUpdateEmployee } from '../Users/Usershooks';
const User = () => {
const userData = {
name: 'test'
};
const { data } = useFetchEmployee(userID);
const { mutate } = useUpdateEmployee(userID, userData);
const saveChangesOnClick = () => {
mutate();
};
return (
<div>
...
</div>
);
};
export default User;
HookFile
import axios from 'axios';
import { useMutation, useQuery, useQueryClient } from 'react-query';
const queryClient = useQueryClient();
export const useFetchEmployers = () => useQuery(['fetchEmployers'], () => axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => response.data));
export const useFetchEmployee = (userID: any) => useQuery(['fetchEmployers', userID], () => axios.get(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.data));
export const useUpdateEmployee = (userID: any, userData: any) => useMutation(
() => axios.put(`https://jsonplaceholder.typicode.com/users/${userID}`, userData)
.then(response => response.data),
{
onSuccess: () => {
console.log("success");
queryClient.invalidateQueries(['fetchEmployers']);
}
}
);
useQueryClient is a hook, it has to be initialized in a React component or in a custom hook. Just move it inside the useUpdateEmployee.
export const useUpdateEmployee = (userID: any, userData: any) => {
const queryClient = useQueryClient();
return useMutation(
...,
onSuccess: () => {
queryClient.invalidateQueries(['fetchEmployers']);
}
);
};
Following are my code which includes the fetch API(getData) call with the useEffect and once get the response it will set the result into the setData using useState
I am trying to write the test case for the useEffect and useState but its failing and when I am seeing into the coverage ,I am getting the red background color with statements not covered for the useEffect block.
import { getData } from '../../api/data';
const [data, setData] = useState({});
useEffect(() => {
getData({ tableName }).then((response) => {
try {
if (response && response.result) {
const result = Array.isArray(response.result)
? response.result[0]
: response.result;
const createDate = result.createdDate;
result.name = result.firstName;
result.submittedDate = `${createDate}`;
result.attribute = Array.isArray(result.attribute)
? result.attribute
: JSON.parse(result.attribute);
setData(result);
}
} catch (error) {
const errorObj = { error: error.message || 'error' };
setData({ errorObj });
}
});
}, []);
And I tried to write the test cases as following for the above code.
import React from "react";
import {
shallowWithIntl,
loadTranslation,
} from "../../../node_modules/enzyme-react-intl/lib/enzyme-react-intl";
import ParentPage from "ParentPage";
import ChildPage from "ChildPage";
import mockResponse from "mockData";
import { shallow, mount } from "enzyme";
import { act } from "react-dom/test-utils";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockResponse),
})
);
describe("ParentPage", () => {
let useEffect;
let wrapper;
const mockUseEffect = () => {
useEffect.mockImplementationOnce((f) => f());
};
beforeEach(() => {
const defaultProps = {
tableName: "tableName",
};
wrapper = shallowWithIntl(<ParentPage {...defaultProps} />);
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect();
});
it("Should render", () => {
expect(wrapper).toMatchSnapshot();
});
it("Compenent render", async () => {
let wrapper;
await act(async () => {
const setWidgets = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation([mockResponse, setWidgets]);
wrapper = await mount(<ChildPage data={mockResponse} />);
await act(async () => {
wrapper.update();
});
console.log(wrapper);
});
});
});
But when I tried using npm run test,And check the coverage I am still getting the statements not covered for the useEffect and useState.
What should I do to achieve the coverage as maximum as possible?
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 have following hook
import axios from "axios";
import {useKeycloak} from "#react-keycloak/web";
import {useEffect, useState} from "react";
export const useAdminApi = () => {
const {keycloak} = useKeycloak();
const [axiosInstance, setAxiosInstance] = useState(undefined);
useEffect(() => {
let instance = axios.create({
baseURL: `${process.env.REACT_APP_ADMIN_API_URL}`,
headers: {
Test: 'test',
Authorization: 'Bearer ' + keycloak.token,
}
});
setAxiosInstance(instance);
return () => {
setAxiosInstance(undefined);
}
}, [keycloak.token]);
const getUsers = ({query}) => {
return axiosInstance.get(`/users${query}`)
};
const getUserDetail = ({userId}) => {
return axiosInstance.get(`/users/${userId}`)
};
const deleteUser = ({userId}) => {
return axiosInstance.delete(`/users/${userId}`)
};
return {
getUsers,
getUserDetail,
deleteUser
}
};
When I log instance it's logged with all config
From useAdminApi I'd like to export functions like getUserDetail, deleteUser, ...
Then in other component, I'd like to use this functions so I have following:
const UserForm = () => {
const {getUserDetail} = useAdminApi();
useEffect(() => {
if (!userId) {
setIsNew(true);
} else {
setIsNew(false);
getUserDetail({userId})
.then(result => setUserData(result.data))
.catch(error => pushError(push, error));
}
}, [userId]);
...
}
However, when I display the UserForm I'm getting following error: TypeError: Cannot read property 'get' of undefined which is pointing to this line return axiosInstance.get(`/users/${userId}`)
Can somebody please tell me what's wrong with this approach?
You're setting axiosInstance's initial value as undefined but TypeScript doesn't infer the type you want. useState is a generic function, so what you can do is pass the type yourself.
import axios, { AxiosInstance } from 'axios';
const [axiosInstance, setAxiosInstance] = useState<AxiosInstance | undefined>(
undefined
);
Then in your functions you still need to check if axiosInstance is undefined.
If you have TypeScript 3.7 or higher you can achieve this with Optional Chaining.
const getUsers = ({ query }: any) => {
return axiosInstance?.get(`/users${query}`);
};