React Hook cannot be called inside a callback - reactjs

Problem Statement :
I am trying to setup a react component that will make an API call whenever a value is selected from the select box.
I tried to make that happen in the useEffect hook but I am getting errors based on the rule of hooks that we can not call any hook inside a callback. Can you please tell me how can I fix this issue and do the required API call on any of the user Input.
I am looking over the pointers that can help me prevent this error and at the same time make an API call to the backend to fetch the records
Here is my code :
Component
const component: React.FC<ComponentProps> = () => {
const { user } = useAppSelector((state) => state.auth);
const periods = getPeriodNames();
const [selectedPeriod, setSelectedPeriod] = React.useState(periods[0]);
const [records, setRecords] = React.useState([]);
const [columns, setColumns] = React.useState<any>();
React.useEffect(() => {
const [request] = React.useState<Request>({ // Throwing error: React Hook "React.useState" cannot be called inside a callback.
requester: user.alias,
accountingMonth: selectedPeriod,
limit: 300,
});
const { data, error, isLoading, isSuccess, isError } =
useQuery(request); // Throwing error : React Hook "useQuery" cannot be called inside a callback.
setRecords(data?.value);
}, [selectedPeriod, user.alias]);
const onPeriodSelect = (detail: SelectProps.ChangeDetail) => {
setSelectedPeriod(selectedOption);
};
React.useEffect(() => {
if (records) {
// do something
}
}, [records]);
return (
<>
<Select
selectedOption={selectedPeriod}
onChange={({ detail }) => onPeriodSelect(detail)}
options={periods}
selectedAriaLabel="Selected"
/>
</>
);
};
Setup to make an API Call
export const dynamicBaseQuery: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
const { mainApiUrl } = (api.getState() as RootState).settings.endpoints;
const rawBaseQuery = fetchBaseQuery({
baseUrl: mainApiUrl,
prepareHeaders: (headers, { getState }) => {
// Use getState to pull the jwtToken and pass it in the headers to the api endpoint.
const { jwtToken } = (getState() as RootState).auth;
headers.set("authorization", jwtToken);
return headers;
},
});
return rawBaseQuery(args, api, extraOptions);
};
export const mainApi = createApi({
reducerPath: "mainApi",
baseQuery: dynamicBaseQuery,
endpoints: () => ({}),
});
const useQuery = mainApi.injectEndpoints({
endpoints: (builder) => ({
query: builder.query<response, request>({
query: (request?: request) => ({
url: "/test_url",
body: request,
method: "POST",
}),
}),
}),
overrideExisting: false,
});
Any help would be really appreciated. Thanks

As the error tells, you should move your custom hook useQuery out of useEffect
You can add it on top of your component instead like below
const component: React.FC<ComponentProps> = () => {
const { user } = useAppSelector((state) => state.auth);
const [request, setRequest] = React.useState<Request | undefined>();
const periods = getPeriodNames();
const { data, error, isLoading, isSuccess, isError } =
useQuery(request); //when component get re-rendered, and request state is there, it will fetch data
const [selectedPeriod, setSelectedPeriod] = React.useState(periods[0]);
const [records, setRecords] = React.useState([]);
const [columns, setColumns] = React.useState<any>();
//fetched successfully
React.useEffect(() => {
if(data) {
setRecords(data.value);
}
}, [data])
React.useEffect(() => {
setRequest({
requester: user.alias,
accountingMonth: selectedPeriod,
limit: 300,
})
}, [selectedPeriod, user.alias]);
const onPeriodSelect = (detail: SelectProps.ChangeDetail) => {
setSelectedPeriod(selectedOption);
};
React.useEffect(() => {
if (records) {
// do something
}
}, [records]);
return (
<>
<Select
selectedOption={selectedPeriod}
onChange={({ detail }) => onPeriodSelect(detail)}
options={periods}
selectedAriaLabel="Selected"
/>
</>
);
};

You can put your API call inside a callback and call it inside your selectbox handler.
example:
const apiCall = (item) => {
// api call logic
}
const handleSelectBox = (selectedItem)=> {
apiCall(selectedItem)
}

Related

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 props are not passing for children... Why?

I am trying make an "easy" weather app exercise, just get data from api and render it. I am using "google api map" to get the location from a post code to a latitude and longitud parameters so I can use those numbers and pass it to "open weather map" api to get the weather for that location.
It is working but with bugs...
First I used redux for "location" and "weather". Redux was working but useSelector() wasnt displaying the data properly.
Then I decide to make it easy, on "search" component I am calling an api an getting the location I need, I am storing it with redux and it is working, on "weatherFullDispaly" component I am calling an api for the "weather" details and just pass it as props for the children to render the data but they are not getting it.
The thing is, while the app is running, when I put a post code I get an error because the children are not receiving the data but, if I comment out the children on the parent component and then comment in again, all the data print perfect.
Any help please???
const WeatherFullDisplay = () => {
const [weatherDetails, setWeatherDetails] = useState();
const currentLocation = useSelector(getLocationData);
useEffect(() => {
getWeatherDetails();
}, []);
const getWeatherDetails = async () => {
const API_KEY = process.env.REACT_APP_OPEN_WEATHER_MAP_API_KEY;
const { lat, lng } = await currentLocation.results[0].geometry.location;
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lng}&exclude=minutely&units=metric&appid=${API_KEY}`
);
setWeatherDetails(response.data);
};
return (
<div className="weather-full-display-details">
<WeatherNow weatherDetails={weatherDetails} />
<HourlyWeather weatherDetails={weatherDetails} />
<FiveDaysWeather weatherDetails={weatherDetails} />
</div>
);
};
const FiveDaysWeather = ({ weatherDetails }) => {
const displayDailyWeather = () => {
const daysToShow = [
weatherDetails.daily[1],
weatherDetails.daily[2],
weatherDetails.daily[3],
weatherDetails.daily[4],
weatherDetails.daily[5],
];
return daysToShow.map((day, i) => {
return (
<WeatherSingleCard
key={i}
typeOfCard="daily"
weekDay={moment(day.dt * 1000).format("dddd")}
icon={day.weather[0].icon}
weather={day.weather[0].main}
temp={day.temp.day}
/>
);
});
};
return (
<div className="day-single-cards">{displayDailyWeather()}</div>
);
};
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
locationDetails: "",
};
const locationSlice = createSlice({
name: "location",
initialState,
reducers: {
setLocation: (state, action) => {
state.locationDetails = action.payload;
},
cleanLocation: (state) => {
state.locationDetails = ""
}
},
});
export const { setLocation, cleanLocation } = locationSlice.actions;
export const getLocationData = (state) => state.location.locationDetails;
export default locationSlice.reducer;
const SearchBar = () => {
const [postCode, setPostCode] = useState();
const [locationDetails, setLocationDetails] = useState();
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
getLocationDetails();
}, [postCode]);
const getLocationDetails = async () => {
const response = await axios.get(
"https://maps.googleapis.com/maps/api/geocode/json",
{
params: {
components: `country:ES|postal_code:${postCode}`,
region: "ES",
key: process.env.REACT_APP_GOOGLE_API_KEY,
},
}
);
setLocationDetails(response.data);
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(setLocation(locationDetails));
navigate("/detail-weather");
};
const handleChange = (e) => {
setPostCode(e.target.value);
};

React custom hook with callback parameter not picking up parameter change

I have a following generic custom hook. What I want to achieve is that hook itself exposes api with api functions, which I can use in a callback. I also want a hook to be dependent on a api function parameters changes.
export const useArticleApi = <TResult>(
callback: (committedOrderApi: ArticlesApi) => Promise<TResult>
): {
loading: boolean;
data: TResult | undefined;
} => {
const callbackRef = useRef<(articlesApi: ArticlesApi) => Promise<TResult>>();
const [data, setData] = useState<TResult | undefined>();
const [loading, setLoading] = useState(false);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
(async () => {
setLoading(true);
const response = await apiCallWithErrorHandling(
async () => callbackRef.current && (await callbackRef.current(articlesApi))
);
if (response.isSuccess) {
setData(response?.data);
setLoading(false);
}
})();
}, [callback]);
return { loading, data };
};
Hook usage:
const getArticlesForAllCategoriesCallback = useCallback((api: ArticlesApi) => api.getArticlesForAllCategories(
categories.map(c => ({ id: c.id, name: c.name, pageId: c.pageId }))
), [categories]);
const { data, loading } = useArticleApi<ArticleSearchBarViewData>(api => getArticlesForAllCategoriesCallback(api));
I missing something pretty obvious, but for some reason useEffect inside the hook doesn't detect the change of callback parameter and api method is run only once. Can you spot the issue?

Wait for useLazyQuery response

I need to call a query when submit button is pressed and then handle the response.
I need something like this:
const [checkEmail] = useLazyQuery(CHECK_EMAIL)
const handleSubmit = async () => {
const res = await checkEmail({ variables: { email: values.email }})
console.log(res) // handle response
}
Try #1:
const [checkEmail, { data }] = useLazyQuery(CHECK_EMAIL)
const handleSubmit = async () => {
const res = await checkEmail({ variables: { email: values.email }})
console.log(data) // undefined the first time
}
Thanks in advance!
This works for me:
const { refetch } = useQuery(CHECK_EMAIL, {
skip: !values.email
})
const handleSubmit = async () => {
const res = await refetch({ variables: { email: values.email }})
console.log(res)
}
After all, this is my solution.
export function useLazyQuery<TData = any, TVariables = OperationVariables>(query: DocumentNode) {
const client = useApolloClient()
return React.useCallback(
(variables: TVariables) =>
client.query<TData, TVariables>({
query: query,
variables: variables,
}),
[client]
)
}
You could also use the onCompleted option of the useLazyQuery hook like this:
const [checkEmail] = useLazyQuery(CHECK_EMAIL, {
onCompleted: (data) => {
console.log(data);
}
});
const handleSubmit = () => {
checkEmail({ variables: { email: values.email }});
}
In case someone wants to fetch multiple apis at single load, it could be achieved like this.
On Demand Load > e.g. onClick, onChange
On Startup > e.g. useEffect
import { useLazyQuery } from "#apollo/client";
import { useState, useEffect } from "react";
import { GET_DOGS } from "../../utils/apiUtils";
const DisplayDogsLazy = () => {
const [getDogs] = useLazyQuery(GET_DOGS);
const [data, setData] = useState([]);
useEffect(() => {
getAllData();
}, []);
const getAllData = async () => {
const response = await getDogs();
console.log("Awaited response >", response);
};
const handleGetDogsClick = async () => {
const response = await getDogs();
setData(response.data.dogs);
};
return (
<>
<button onClick={handleGetDogsClick}>Get Dogs</button>
{data?.length > 0 && (
<ul>
{data?.map((dog) => (
<li key={dog.id} value={dog.breed}>
{dog.breed}
</li>
))}
</ul>
)}
</>
);
};
export default DisplayDogsLazy;

React effect infinite re-renders when fetching data

I want to fetch an array from the backend using a Provider, Context and useEffect:
import React, {useState, useEffect} from 'react'
const UsersContext = React.createContext()
const fetchUsers = async () => {
const url = 'http://localhost:3000/users'
const response = await fetch(url)
console.log('response', await response.json())
return response
}
export const UsersProvider = ({children}) => {
// state
const [users, setUsers] = useState([])
// query data
const data = fetchUsers()
console.log('data', data)
// component updates
useEffect(() => {
if (data) {
// setUsers(data)
}
}, [data])
return (
<UsersContext.Provider value={users}>
{children}
</UsersContext.Provider>
)
}
If I set the users once I have the data back from the backend, I get infinite re-render. The issue is, data is always a promise, although I can see the response after the call is being made:
In the fetchUsers method:
console.log('response', await response.json())
{users: Array(1)}
users: Array(1)
0:
created_at: "2019-10-09T17:41:21.818Z"
email: "ash#email.com"
id: 1
name: "ash"
password_digest: "dafasfae"
updated_at: "2019-10-09T17:41:21.818Z"
In the UsersProvider:
console.log('data', data)
Promise {<pending>}
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: Response
body: (...)
bodyUsed: true
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "http://localhost:3000/users"
__proto__: Response
I would move the fetchUsers function inside the effect and try like this:
import React, { useState, useEffect } from "react";
const UsersContext = React.createContext();
export const UsersProvider = ({ children }) => {
// state
const [users, setUsers] = useState([]);
// component mounts
useEffect(() => {
const fetchUsers = async () => {
const url = "http://localhost:3000/users";
const response = await fetch(url);
const usersData = await response.json();
setUsers(usersData);
};
fetchUsers();
}, []);
return (
<UsersContext.Provider value={users}>{children}</UsersContext.Provider>
);
};
Here is a very good post about fetching data with react hooks:
https://www.robinwieruch.de/react-hooks-fetch-data
Your data fetch needs to happen inside the useEffect call. What's happening right now is every time your component renders, you are re-fetching the list of users which causes the data object to change and triggers a re-render. The code below will fetch the data only when the component mounts.
useEffect(() => {
let canceled = false;
const fetchData = async () => {
const users = await fetchUsers();
if (!canceled && data) {
setUsers(data);
}
};
fetchUsers();
return () => { canceled = true; }
}, []);
return only data
const fetchUsers = async () => {
const url = 'http://localhost:3000/users'
const response = await fetch(url)
.then( data => {
return data
})
.catch(err=>console.log(err));
return response;
}
hope this will help you.

Resources