React Query Invalid hook call when I introduce useQueryClient - reactjs

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']);
}
);
};

Related

Testing a custom hook not getting updated

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)
})
})
})

React Query invalidateQueries not updating the UI

My UI is not updating on the creation of a project with invalidateQueries. I have confirmed that the database updates are being made successfully and the onSuccess functions are being called. I'm unsure what I am doing wrong, and would love some help.
useProjects.ts
import { getProjects } from 'queries/get-projects';
import { useQuery } from 'react-query';
function useGetProjectsQuery() {
return useQuery('projects', async () => {
return getProjects().then((result) => result.data);
});
}
export default useGetProjectsQuery;
get-project.ts
import { supabase } from '../utils/supabase-client';
export const getProjects = async () => {
return supabase.from('projects').select(`*`);
};
useCreateProject.ts
import { useUser } from '#/utils/useUser';
import { createProject } from 'queries/create-project';
import { useMutation, useQueryClient } from 'react-query';
export const useCreateProject = () => {
const { user } = useUser();
const queryClient = useQueryClient();
return useMutation(
({ title, type, bgColorClass, pinned }: any) => {
return createProject(title, type, bgColorClass, pinned, user.id).then(
(result) => result.data
);
},
{
onSuccess: () => {
queryClient.invalidateQueries('projects');
}
}
);
};
create-project.ts
import { supabase } from '../utils/supabase-client';
export async function createProject(
title: string,
type: string,
bgColorClass: string,
pinned: boolean,
userId: string
) {
return supabase
.from('projects')
.insert([
{ title, type, bg_color_class: bgColorClass, pinned, user_id: userId }
]);
}
Home Component
const { data: projects, isLoading, isError } = useGetProjectsQuery();
const createProject = useCreateProject();
const createNewProject = async () => {
await createProject.mutateAsync({
title: projectName,
type: selectedType.name,
bgColorClass: _.sample(projectColors),
pinned: false
});
};
_app.tsx
export default function MyApp({ Component, pageProps }: AppProps) {
const [initialContext, setInitialContext] = useState();
const [supabaseClient] = useState(() =>
createBrowserSupabaseClient<Database>()
);
useEffect(() => {
document.body.classList?.remove('loading');
}, []);
const getUserDetails = async () =>
supabaseClient.from('users').select('*').single();
const getSubscription = async () =>
supabaseClient
.from('subscriptions')
.select('*, prices(*, products(*))')
.in('status', ['trialing', 'active'])
.single();
const getInitialData = async () => {
const userDetails = await getUserDetails();
const subscription = await getSubscription();
setInitialContext({
//#ts-ignore
userDetails: userDetails.data,
subscription: subscription.data
});
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0
}
}
});
useEffect(() => {
getInitialData();
}, []);
return (
<QueryClientProvider client={queryClient}>
<SessionContextProvider supabaseClient={supabaseClient}>
<MyUserContextProvider initial={initialContext}>
<SidebarProvider>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</SidebarProvider>
</MyUserContextProvider>
</SessionContextProvider>
</QueryClientProvider>
);
}
I have tried moving the onSuccess followup calls to the Home component, and within the hooks, neither one updates the UI. I'm unsure what I am doing wrong and the react query devtools is not helpful.
You are instantiating QueryClient on every render pass so the cache of query keys is being torn down and rebuilt often.
Instantiate this outside of render:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0
}
}
});
export default function MyApp({ Component, pageProps }: AppProps) {
const [initialContext, setInitialContext] = useState();
// ... etc
This will ensure the client is always stable.

React : Value inside useEffect not defined

So I am building an e-commerce website checkout page with commerce.js. I have a context that allows me to use the cart globally. But on the checkout page when I generate the token inside useEffect , the cart variables have not been set until then.
My context is as below
import { createContext, useEffect, useContext, useReducer } from 'react';
import { commerce } from '../../lib/commerce';
//Provides a context for Cart to be used in every page
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = 'SET_CART';
const initialState = {
id: '',
total_items: 0,
total_unique_items: 0,
subtotal: [],
line_items: [{}],
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
useEffect(() => {
getCart();
}, []);
const getCart = async () => {
try {
const cart = await commerce.cart.retrieve();
setCart(cart);
} catch (error) {
console.log('error');
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={state}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
Now on my checkout page
const CheckoutPage = () => {
const [open, setOpen] = useState(false);
const [selectedDeliveryMethod, setSelectedDeliveryMethod] = useState(
deliveryMethods[0]
);
const [checkoutToken, setCheckoutToken] = useState(null);
const { line_items, id } = useCartState();
useEffect(() => {
const generateToken = async () => {
try {
const token = await commerce.checkout.generateToken(id, {
type: 'cart',
});
setCheckoutToken(token);
} catch (error) {}
};
console.log(checkoutToken);
console.log(id);
generateToken();
}, []);
return <div> {id} </div>; //keeping it simple just to explain the issue
};
In the above code id is being rendered on the page, but the token is not generated since on page load the id is still blank. console.log(id) gives me blank but {id} gives the actual value of id
Because CheckoutPage is a child of CartProvider, it will be mounted before CartProvider and the useEffect will be called in CheckoutPage first, so the getCart method in CartProvider hasn't been yet called when you try to read the id inside the useEffect of CheckoutPage.
I'd suggest to try to call generateToken each time id changes and check if it's initialised first.
useEffect(() => {
if (!id) return;
const generateToken = async () => {
try{
const token = await commerce.checkout.generateToken(id, {type: 'cart'})
setCheckoutToken(token)
} catch(error){
}
}
console.log(checkoutToken)
console.log(id)
generateToken()
}, [id]);

React-redux store state is empty

I have axios making a get request to fetch a request a list of vessels and their information,
i am trying to use redux slice, and populate the data using dispute, however the state is always empty dispute not having any errors
the vesselSlice :
import { createSlice } from "#reduxjs/toolkit";
import { api } from "../components/pages/screens/HomeScreen";
const vesselSlice = createSlice({
name: "vessels",
initialState: {
vessels: [],
},
reducers: {
getVessels: (state, action) => {
state.vessels = action.payload;
},
},
});
export const vesselReducer = vesselSlice.reducer;
const { getVessels } = vesselSlice.actions;
export const fetchVessels = () => async (dispatch) => {
try {
await api
.get("/vessels")
.then((response) => dispatch(getVessels(response.data)));
} catch (e) {
return console.error(e.message);
}
};
the HomeScreen.js :
import React, { useEffect } from "react";
import VesselCard from "../../VesselCard";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { fetchVessels } from "../../../features/vesselSlice";
export const api = () => {
axios
.get("http://127.0.0.1:8000/api/vessels/info")
.then((data) => console.log(data.data))
.catch((error) => console.log(error));
};
function HomeScreen() {
const { vessels, isLoading } = useSelector((state) => state.vessels);
const dispatch = useDispatch();
// This part:
useEffect(() => {
fetchVessels(dispatch);
}, [dispatch]);
return (
<div>
Fleet vessels :
<div className="fleet-vessels-info">
{vessels.map((vessel) => (
<VesselCard vessel={vessel} />
))}
</div>
</div>
);
}
export default HomeScreen;
You receive dispatch this way. It is not being received from any middleware.
export const fetchVessels = async (dispatch) =>{}
If you want to continue using your approach, call function this way
useEffect(() => {
fetchVessels()(dispatch);
}, [dispatch]);
Api function error
export const api = async () => {
try {
const res = await axios.get("http://127.0.0.1:8000/api/vessels/info");
return res.data;
} catch (error) {
console.log(error);
}
};
export const fetchVessels = () => async (dispatch) => {
try {
await api()
.then((data) => dispatch(getVessels(data)));
} catch (e) {
return console.error(e.message);
}
};

how to test a hook with async state update in useEffect?

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);
})
})

Resources