Call useSubscription after loading the state value - reactjs

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

Related

Use data populated by useEffect in conditional useSWR

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

Prevent useSWR from fetching until mutate() is called

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.

react apollo - clear data object before refetch

When using refetch of useQuery hook the data object is still defined. And when using infinite scrolling, only the first page will be refetched.
Is it possible to clear the data object before calling the refech so that we can start fresh?
const { data, loading, error, fetchMore, refetch } = useQuery(GET_ALL_ITEMS, {variables});
getNextPage = async () => { // merges results for infinite scrolling
await fetchMore({ variables,
updateQuery: (previousResult, { fetchMoreResult }) => {
const oldEntries = previousResult.items;
const newEntries = fetchMoreResult.items;
fetchMoreResult.items = [...oldEntries, ...newEntries];
return fetchMoreResult;
},
)
}
Can I do something like refresh = () => { data = null; refetch(); } but without directly mutating state?
I couldn't find a way to clear data but found a different approach in Apollo Client v3 docs.
Basically it's to ignore loaded data when loading or in case of error:
const { data: loadedData, loading, error, fetchMore, refetch } = useQuery(
GET_ALL_ITEMS,
{
variables,
notifyOnNetworkStatusChange: true, // important for adequate loading state
}
);
const data = (loading || error) ? undefined : loadedData;
Note, that in order to make loading work for refetch, you need to pass notifyOnNetworkStatusChange: true option.
It's also possible to get networkStatus from useQuery result and check it like this:
networkStatus === NetworkStatus.refetch
but it seems to work for me with loading and error only.

react promise in functional component with UseEffect and UseState doesn't work

I'm having issue fetching data and setting them to state in a functional component using useEffect and useState.
My problem is that I would like to keep the data fetching done with axios async/await in a separate file for improving application scalability but then I don't understand how to update the state in case the promise is resolved (not rejected).
In particular I'm trying to retrieve from the promise an array of table rows called data in state, but I can't figure out how to set the result of the responce in the state
Here's the code in the component file:
const [data, setData] = React.useState([]);
useEffect(() => {
const { id } = props.match.params;
props.getTableRows(id).then((res) => {
setData(res);
});
//or is it better:
//props.getTableRows(id).then(setData); ?
}, []);
and my action.js:
export const getTableRows = (id, history) => async (dispatch) => {
try {
const res = await axios.get(`/api/test/${id}`);
dispatch({
type: GET_TEST,
payload: res.data.rows,
});
} catch (error) {
history.push("/test");
}
};
In the above picture it can be seen that the rows array inside the promise response called in action.js is present.
This code unfortunately doesn't work, error: Uncaught (in promise) TypeError: Cannot read property 'forEach' of undefined
I've found out another solution which is the define the promise in the useEffect method like this:
useEffect(() => {
const { id } = props.match.params;
const fetchData = async () => {
const result = await axios.get(`/api/test/${id}`);
setData(result.data.rows);
};
fetchData();
}, []);
this code is working in my app but as I said I don't like having the promises in the components files I would like instead to have them all the promise in action.js for app scalability (in case url change I don't have to change all files) but in that case I don't know where to put the setData(result.data.rows); which seems the right choise in this last example
Any suggestions?
Thanks
You still need to use async/await. The .then() is executed when the value is returned, however your function will continue rendering and won't wait for it. (causing it to error our by trying to access forEach on a null state). After it errors the promise via .then() will update the values and that is why you can see them in the console.
useEffect(() => {
async function getData() {
const { id } = props.match.params;
await props.getTableRows(id).then((res) => {
setData(res);
});
}
getData()
}, []);
Additionally, before you access a state you can check for null values (good practice in general).
if (this.state.somestate != null) {
//Run code using this.state.somestate
}
I don't see you return anything from getTableRows. You just dispatch the result, but hadn't return the res for the function call.
And it will be helpful if you provided error trace.

react native lazy initial state from asyncstorage

I try to get a string from asyncStorage and set it as initial state in a functional component.
I found in the documentation that we can provide a function like initial state. I tried some solutions with asyncStorage and sqlite and don't get results. This is some code, What is wrong and is there some other solution for storing active theme in storage.
const retrieveData = async () => {
try {
const value = await AsyncStorage.getItem("theme");
return value;
} catch (error) {}
};
const [isTheme, setIsTheme] = useState(retrieveData);
useEffect(() => {
AsyncStorage.setItem("theme", isTheme);
}, [isTheme]);
I think retrieveData is still a promise. You should get the value by doing await(retrieveData)

Resources