React custom hook with callback parameter not picking up parameter change - reactjs

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?

Related

useMemo causes hook to continuously fire infinitely

I have a custom react hook that I wrote to query data from a subgraph endpoint. It simply returns an array of objects.
const useAllLPTokens = (): GraphQLResponse<LPTokens> => {
const [status, setStatus] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>();
const [response, setResponse] = useState<any>();
const [payload, setPayload] = useState<LPTokens | undefined>();
const getLPTokenData = async () => {
setLoading(true);
try {
const res = await axios.post(subgraphEndpoint,
{
headers: { "Content-Type": "application/json" },
query: graphQuery
}
);
setStatus(res.status);
setResponse(res)
setPayload(res.data)
} catch (error) {
setError(error)
}
setLoading(false);
}
useMemo(() => {
getLPTokenData();
}, [])
return { status, loading, error, response, payload }
}
Component where it is used:
const Dashboard: React.FC = () => {
const { account } = useActiveWeb3React();
const { status: status1, loading: loading1, error: error1, response: response1, payload: payload1 } = useAllLPTokens();
console.log(payload1);
...
I'm not sure why when I use useMemo in the hook, it fires endlessly when I check the browser console. However when I use useEffect it doesn't. I didnt include any dependency for useMemo and I assumed it will only fire once when it is called. What is the reason for this?
Using useMemo() without the dependencies array will calculate the value on every render.
See this link for reference
https://reactjs.org/docs/hooks-reference.html#usememo
If no array is provided, a new value will be computed on every render.

React Hook cannot be called inside a callback

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

Can I ignore exhaustive-deps warning for useContext?

In my react-typescript application, I am trying to use a context provider that encapsulates properties and methods and exposes them for a consumer:
const StockPriceConsumer: React.FC = () => {
const stockPrice = useContext(myContext);
let val = stockPrice.val;
useEffect(() => {
stockPrice.fetch();
}, [val]);
return <h1>{val}</h1>;
};
The problem is the following warning:
React Hook useEffect has a missing dependency: 'stockPrice'. Either
include it or remove the dependency
array. eslint(react-hooks/exhaustive-deps)
To me it does not make any sense to include the stockPrice (which is basically the provider's API) to the dependencies of useEffect. It only makes sense to include actual value of stock price to prevent infinite calls of useEffect's functions.
Question: Is there anything wrong with the approach I am trying to use or can I just ignore this warning?
The provider:
interface StockPrice {
val: number;
fetch: () => void;
}
const initialStockPrice = {val: NaN, fetch: () => {}};
type Action = {
type: string;
payload: any;
};
const stockPriceReducer = (state: StockPrice, action: Action): StockPrice => {
if (action.type === 'fetch') {
return {...state, val: action.payload};
}
return {...state};
};
const myContext = React.createContext<StockPrice>(initialStockPrice);
const StockPriceProvider: React.FC = ({children}) => {
const [state, dispatch] = React.useReducer(stockPriceReducer, initialStockPrice);
const contextVal = {
...state,
fetch: (): void => {
setTimeout(() => {
dispatch({type: 'fetch', payload: 200});
}, 200);
},
};
return <myContext.Provider value={contextVal}>{children}</myContext.Provider>;
};
I would recommend to control the whole fetching logic from the provider:
const StockPriceProvider = ({children}) => {
const [price, setPrice] = React.useState(NaN);
useEffect(() => {
const fetchPrice = () => {
window.fetch('http...')
.then(response => response.json())
.then(data => setPrice(data.price))
}
const intervalId = setInterval(fetchPrice, 200)
return () => clearInterval(intervalId)
}, [])
return <myContext.Provider value={price}>{children}</myContext.Provider>;
};
const StockPriceConsumer = () => {
const stockPrice = useContext(myContext);
return <h1>{stockPrice}</h1>;
};
...as a solution to a couple of problems from the original appproach:
do you really want to fetch only so long as val is different? if the stock price will be the same between 2 renders, the useEffect won't execute.
do you need to create a new fetch method every time <StockPriceProvider> is rendered? That is not suitable for dependencies of useEffect indeed.
if both are OK, feel free to disable the eslint warning
if you want to keep fetching in 200ms intervals so long as the consumer is mounted:
// StockPriceProvider
...
fetch: useCallback(() => dispatch({type: 'fetch', payload: 200}), [])
...
// StockPriceConsumer
...
useEffect(() => {
const i = setInterval(fetch, 200)
return () => clearInterval(i)
}, [fetch])
...
The important concept here is that react compares the objects by reference equality. Meaning that every time the reference (and not the content) changes it will trigger a re-render. As a rule of thumb, you always need to define objects/functions that you want to pass to child components by useCallback and useMemo.
So in your case:
The fetch function will become:
const fetch = useCallback(() => {
setTimeout(() => {
dispatch({ type: 'fetch', payload: 200 });
}, 1000);
}, []);
The empty array means that this function will be only defined when the component is mounted. And then:
let {val, fetch} = stockPrice;
useEffect(() => {
fetch();
}, [val, fetch]);
This means the useEffect's callback will execute only when fetch or val changes. Since fetch will be defined only once, in practice it means only val changes are gonna trigger the effect's callback.
Also, I can imagine you want to trigger the fetch only when isNaN(val) so:
let {val, fetch} = stockPrice;
useEffect(() => {
if(isNaN(val)) {
fetch();
}
}, [val, fetch]);
All that being said, there's a bigger issue with this code!
You should reconsider the way you use setTimeout since the callback can run when the component is already unmounted and that can lead to a different bug. In these cases you should useEffect and clear any async operation before unmounting the component. So here's my suggestion:
import React, { useCallback, useContext, useEffect } from 'react';
interface StockPrice {
val: number;
setFetched: () => void;
}
const initialStockPrice = { val: NaN, setFetched: () => { } };
type Action = {
type: string;
payload: any;
};
const stockPriceReducer = (state: StockPrice, action: Action): StockPrice => {
if (action.type === 'fetch') {
return { ...state, val: action.payload };
}
return { ...state };
};
const myContext = React.createContext<StockPrice>(initialStockPrice);
const StockPriceProvider: React.FC = ({ children }) => {
const [state, dispatch] = React.useReducer(
stockPriceReducer,
initialStockPrice
);
const setFetched = useCallback(() => {
dispatch({ type: 'fetch', payload: 200 });
}, []);
const contextVal = {
...state,
setFetched,
};
return <myContext.Provider value={contextVal}>{children}</myContext.Provider>;
};
const StockPriceConsumer: React.FC = () => {
const stockPrice = useContext(myContext);
const {val, setFetched} = stockPrice;
useEffect(() => {
let handle = -1;
if(isNaN(val)) {
let handle = setTimeout(() => { // Or whatever async operation
setFetched();
}, 200);
}
return () => clearTimeout(handle); // Clear timeout before unmounting.
}, [val, setFetched]);
return <h1>{stockPrice.val.toString()}</h1>;
};

How to correctly call useFetch function?

I've successfully implemented a useFetch function to call an API Endpoint. It works perfectly if I add code like this to the root of a functional React component like this:
const [{ data, isLoading, isError }] = useFetch(
'http://some_api_endpoint_path'
);
export const useFetch = (url) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await axios.get(url);
setData(response.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }];
};
But let's say I want to check if a newly entered username exists, say upon the firing of an onBlur event of an input element. When I've tried implementing this, I get this error:
React Hook "useFetch" is called in function "handleBlur" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
I even tried this approach:
const [isChanged, setIsChanged] = useState(false);
useEffect(() => {
useFetch(
'http://some_api_endpoint_path'
);
}, [isChanged]);
But got the same error.
Then I tried this simplified version, which doesn't do anything useful but I was testing the React Hooks Rules:
useEffect(() => {
useFetch(
'http://some_api_endpoint_path'
);
}, []);
And still I got the same error.
In these last 2 cases especially, I feel that I am following the Rules of Hooks but apparently not!
What is the correct way to call useFetch in such a situation?
I suppose you call useFetch this way, right?
const onBlur = () => {
const [{ data, isLoading, isError }] = useFetch(
'http://some_api_endpoint_path'
);
...
}
If true, this is wrong. Check this link out:
🔴 Do not call in event handlers.
You may implement this way:
// Pass common initial for all fetches.
export const useFetch = (awsConfig, apiRoot, apiPathDefault) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// Just pass the variables that changes in each new fetch requisition
const fetchData = async (apiPath) => {
setIsError(false);
setIsLoading(true);
try {
const response = await axios.get(apiRoot + apiPath);
setData(response.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData(apiRoot + apiPathDefault);
}, [awsConfig, apiRoot, apiPathDefault]);
return [{ data, isLoading, isError }, fetchData];
};
And whenever you want to fetch again, you just call fetchData:
const [{ data, isLoading, isError }, fetchData] = useFetch(API_ROOT(), appStore.awsConfig, defaultPath);
const onBlur = () => {
fetchData(newPath);
...
}
I've used the same principle that Apollo team used when created useLazyQuey (open this link and search for useLazyQuery, please). Also, note that I pass all common and immutable variables when I call the hooks and pass just the mutable ones in the single fetch.

React TypeScript 16.8 How to add a dependency to useEffect()

In useEffect() I make some keys then try and call the function addKeysToState() that is not in the useEffect() block and it's causing an error.
I've tried adding 'addKeysToState' and addKeysToState() into the array at the end of useEffect() but with no avail.
The error I get is...
React Hook useEffect has a missing dependency: 'addKeysToState'. Either include it or remove the dependency array react-hooks/exhaustive-deps
the code snippet...
const FeedbackForm: React.FC<props> = ({publicKey}) => {
const [formState, setState] = useState();
useEffect(() => {
const genRandomKey = async () => {
const tempPrivateKey = await ecc.randomKey();
if (tempPrivateKey) {
const tempPublicKey = await ecc.privateToPublic(tempPrivateKey);
if (tempPublicKey) {
addKeysToState(tempPrivateKey, tempPublicKey);
}
}
};
genRandomKey();
}, []);
const addKeysToState = (tempPrivateKey: string, tempPublicKey: string) => {
setState({
...formState,
tempPrivateKey,
tempPublicKey,
})
}
How about putting addKeysToState inside the hook? It looks like it's not a dependency, but rather an implementation detail.
Note that since addKeysToState uses the previous state, we should use the callback form instead, to avoid racing conditions.
const FeedbackForm: React.FC<props> = ({publicKey}) => {
const [formState, setState] = useState();
useEffect(() => {
const addKeysToState = (tempPrivateKey: string, tempPublicKey: string) => setState((prevState) => ({
...prevState,
tempPrivateKey,
tempPublicKey,
))
const genRandomKey = async () => {
const tempPrivateKey = await ecc.randomKey();
if (tempPrivateKey) {
const tempPublicKey = await ecc.privateToPublic(tempPrivateKey);
if (tempPublicKey) {
addKeysToState(tempPrivateKey, tempPublicKey);
}
}
};
genRandomKey();
}, []);

Resources