I have a React application which uses SWR + Axios for data fetching (https://swr.vercel.app/docs/data-fetching). The issue is that my custom hook which uses useSwr is fetching all data initially whenever the hook is initialized. My goal is to fetch only when I call mutate. Currently the initial fetch is happening even without calling mutate. Any suggestion on how to achieve my goal here?
My application is wrapped in SWRConfig:
<SWRConfig
value={{
fetcher,
}}
>
<App/>
</SWRConfig>
The fetcher is described as so:
const dataFetch = (url) => axios.get(url).then((res) => res.data);
function fetcher(...urls: string[]) {
if (urls.length > 1) {
return Promise.all(urls.map(dataFetch));
}
return dataFetch(urls);
}
My custom hook using useSwr
import useSWR, { useSWRConfig } from "swr";
export function useCars(registrationPlates: number[]): ICars {
const { mutate } = useSWRConfig();
const { data: carData} = useSWR<Car[]>(
carsToFetchUrls(registrationPlates), // returns string array with urls to fetch
{
revalidateOnFocus: false,
revalidateOnMount: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
}
);
const getCar = (
carRegistrationPlate: number,
): Car => {
console.log(carData) // carData contains data from fetch even before calling mutate()
void mutate();
...
}
Usage: (this will be located in some component that wants to use the useCars hook)
const { getCar } = useCars(carsRegistrationPlates);
You can use conditional fetching in the useSWR call to prevent it from making a request.
From useSWR Conditional Fetching docs:
Use null or pass a function as key to conditionally fetch data. If the
function throws or returns a falsy value, SWR will not start the
request.
export function useCars(registrationPlates: number[], shouldFetch): ICars {
const { data: carData} = useSWR<Car[]>(
shouldFetch ? carsToFetchUrls(registrationPlates) : null,
{ // Options here }
);
// ...
return { carData, /**/ }
}
You can then use it as follows to avoid making the initial request.
const [shouldFetch, setShouldFetch] = useState(false);
const { carData } = useCars(carsRegistrationPlates, shouldFetch);
Then, when you want the make the request simply set shouldFetch to true.
setShouldFetch(true)
Here's a possible way of implementing what you are hoping to achieve.
I've used a similar approach in one of my production app.
Start by creating a custom swr hook as so
const useCars = (registrationPlates: number[]) => {
const fetcher = (_: string) => {
console.log("swr-key=", _);
return dataFetch(registrationPlates);
};
const { data, error, isValidating, revalidate, mutate } = useSWR(`api/car/registration/${JSON.stringify(registrationPlates)}`, fetcher, {
revalidateOnFocus: false,
});
return {
data,
error,
isLoading: !data && !error,
isValidating,
revalidate,
mutate,
};
};
export { useCars };
Now, you can call this hook from any other component as
const { data, error, isLoading, isValidating, revalidate, mutate } = useCars(carsRegistrationPlates);
You now control what you want returned by what you pass to useCars above.
Notice what is passed to the first argument to useSwr in our custom swr hook, this is the key swr uses to cache values and if this remains unchanged then swr will transparently returned the cached value.
Also, with this custom hook you are getting states such as loading, error etc. so you can take appropriate action for each of these states in your consuming component.
Related
I'm trying to resolve this issue, and I'm almost there. I'm getting the correct data from the API, and it's updating when it should, but on initial load useSWR is hitting the API with all null data.
The data come from useContext, and are set in a useEffect hook in a parent of the component that calls useSWR.
I guess what's happening is that the since useEffect isn't called until after initial hydration, the component with useSWR is being rendered before it has data.
But if the context setter isn't wrapped in a useEffect, I get
Warning: Cannot update a component (`ContestProvider`) while rendering a different component (`PageLandingPage`). To locate the bad setState() call inside `PageLandingPage`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
and it's stuck in an infinite loop.
I could probably stop this by putting some checks in the fetcher, but that seems like a hack to me. The useSWR documentation addresses the case of fetching data server side and making it available to multiple components right in the Getting Started section, but what's the correct way to get data from the client that needs to be used in multiple components, including ones that want to fetch data from the server based on the client data?
EDIT: Since originally asking the question, I've discovered conditional fetching, and the third option there seems nearly a perfect fit, but I'm using a complex key to a custom fetcher, and the data for the key aren't coming from another useSWR call, as in the example — they're coming from the useContext which has the unfortunate difference that, unlike the example, the data are null instead of undefined, so it won't throw.
How can I use this conditionality with data coming in from the useContext?
Here's the app hierarchy:
<MyApp>
<ContestEntryPage>
<ContestProvider> // context provider
<PageLandingPage> // sets the context
<Section>
<GridColumn>
<DatoContent>
<ContestPoints> // calls useSWR with data from the context
Here's the useSWR call:
// /components/ContestPoints.js
const fetcher = async ({pageId, contestId, clientId}) => {
const res = await fetch(`/api/getpoints?pageId=${pageId}&clientId=${clientId}&contestId=${contestId}`);
if (!res.ok) {
const error = new Error('A problem occured getting contest points');
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
}
const ContestPoints = () => {
const { contestState } = useContest();
// XXX should be conditional on the `contestState` parameters
const { data: points, error } = useSWR({
pageId: contestState.pageId,
contestId: contestState.contestId,
clientId: contestState.clientId
}, fetcher);
if (error) {
logger.warn(error, `Problem getting contest points: ${error.status}: ${error.info}`);
}
return (
<p>{points?.points || 'Loading...'}</p>
)
}
export default ContestPoints
It seems like finding a way to make that do the conditional fetching is likely best, but in case it's more elegant to leave the useSWR call as is, and address this farther up the chain, here are the other relevant pieces of code.
The context is being set based on information in localStorage:
// /components/PageLandingPage.js
import { useContest } from '../utils/context/contest';
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest(); // wrapper around useContext which uses useReducer
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
const storedCodes = getItem('myRefCodes', 'local'); //utility function to retrieve from local storage
const refCodes = storedCodes ? JSON.parse(storedCodes)?.refCodes : [];
const registration = refCodes
.map((code) => {
const [ contestId, clientId ] = hashids.decode(code);
return {
contestId: contestId,
clientId: clientId
}
})
.find((reg) => reg.contestId && reg.clientId)
dispatchContest({
payload: {
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId,
registrationUrl: landingPage?.registrationPage?.slug || ''
},
type: 'update'
})
}, [data, dispatchContest])
...
And the context wrapper is initialising with null state:
const initialState = {
contestId: null,
clientId: null
};
const ContestContext = createContext(initialState);
function ContestProvider({ children }) {
const [contestState, dispatchContest] = useReducer((contestState, action) => {
return {
...contestState,
...action.payload
}
}, initialState);
return (
<ContestContext.Provider value={{ contestState, dispatchContest }}>
{children}
</ContestContext.Provider>
);
}
function useContest() {
const context = useContext(ContestContext);
if (context === undefined) {
throw new Error('useContest was used outside of its provider');
}
return context;
}
export { ContestProvider, useContest }
I'm not sure it's the best solution, but I ended up resolving this by using the first example in the documentation, using null and a new field in the context:
const { data: points, error } = useSWR(contestState?.isSet ? {
pageId: contestState.pageId,
contestId: contestState.contestId,
clientId: contestState.clientId
} : null, fetcher);
and the contestState.isSet gets set in the context:
const initialState = {
isSet: false,
contestId: null,
clientId: null
};
and update it when all the other fields get set:
dispatchContest({
payload: {
isSet: true,
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId,
registrationUrl: landingPage?.registrationPage?.slug || ''
},
type: 'update'
})
I have tried everything I could so far, but can't get to make this manual data revalidation/mutation to work using const { mutate } = useSWRConfig().
I have tried setting GlobalConfig on my app, and not, but to no changes.
For example: In my "AddressesList" component, I fetch addresses list using the hook:
const { data, isValidating, error} = useSWR('v1/addresses', fetchMyAddresses, {
revalidateOnFocus: false,
revalidateIfStale: false,
});
where the fetcher is a simple is a simple fetchMyAddresses = () => axiosApiInstance.get('v1/addresses').then(res => res.data);
Then in another component, after creating a new address, I use:
const { mutate } = useSWRConfig();
...
await createNewAddress();
if (success) {
mutate('v1/addresses');
redirect('AddressesList');
}
and on success, I am redirected to the AddressesList. But my data list is not updated with the new address... how is that supposed to work?
I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.
const {authState,loading:authLoading}=useAuthState();
const {
data,
loading,
error
} = useSubscription(USER_NOTIFICATIONS, { variables: { userId:authState.userId} });
In the above code, I am getting
Cannot read property 'userId' of null because the authState is null when the component loads.
Is there any way to call the useSubscription hook only after the authState is loaded and it is not empty.
I read useSubscription apollo document, there is an attribute named skip which accepts a boolean value.
Is skip will solve this issue? In that case, Is there any code example available?
I cant use like this, because this is against react hooks
useEffect(() => {
async function fetchData(){
if(await authState){
const {
data,
loading,
error
} = useSubscription(USER_NOTIFICATIONS, { variables: { userId:authState.userId}});
}
}
if(!authLoading) fetchData();
}, [authState,authLoading])
I want to pass data into my useState, the problem is that, useEffect hasn't been able to to perform this operation too well...
This is what i have
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
const [pubRequested, setPubRequested] = useState<Ipublication[]>([]);
useEffect(() => {
setPubRequested(Publications);
}, []);
console.log(pubRequested);
`;
return ()
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
So, the logic is quite simple, i'm gettin data by using getStaticProps, that is server side rendering, but i want to use SWR hook, so i'm using initialData so i can use it, and, the las thing is, i'm using useEffect and i want to pass Publications data from swr to my useState, the problem is that, Publications data is slow enough to not be able to pass the data.
As you can see, i have a console.log() and it returns me undefined
What can i do about it, i need that data in my useState, any ideas ?
Thanks people
You need to wait for the data to load.
useEffect(() => {
if(data){
setPubRequested(Publications);
}
}, [data]);