React hooks - How to call useEffect on demand? - reactjs

I am rewriting a CRUD table with React hooks. The custom hook useDataApi below is for fetching data of the table, watching the url change - so it'll be triggered when params change. But I also need to fetch the freshest data after delete and edit. How can I do that?
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl)
const [state, dispatch] = useReducer(dataFetchReducer, { data: initialData, loading: true })
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' })
const result = await instance.get(url)
dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
}
fetchData()
}, [url])
const doFetch = url => {
setUrl(url)
}
return { ...state, doFetch }
}
Since the url stays the same after delete/edit, it won't be triggered. I guess I can have an incremental flag, and let the useEffect monitor it as well. But it might not be the best practice? Is there a better way?

All you need to do is to take the fetchData method out of useEffect and call it when you need it. Also make sure you pass the function as param in dependency array.
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl)
const [state, dispatch] = useReducer(dataFetchReducer, { data: initialData, loading: true })
const fetchData = useCallback(async () => {
dispatch({ type: 'FETCH_INIT' })
const result = await instance.get(url)
dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
}, [url]);
useEffect(() => {
fetchData()
}, [url, fetchData]); // Pass fetchData as param to dependency array
const doFetch = url => {
setUrl(url)
}
return { ...state, doFetch, fetchData }
}

Related

Can we make API call to set the initial state of reducer?

let initialData = {
products:[]
}
const ItemReducer = (state = initialData, action) => {
switch (action.type) {
case "FETCHDATA":
return {
...state,
products: action.payload,
};
}
return state;
}
let [productdata, setProductdata] = useState();
const dispatch = useDispatch();
useEffect(()=>{
axios.get("https://fakestoreapi.com/products")
.then((res) => setProductdata(res.data))
.catch((err) => console.log("error"))
},[]);
dispatch({type:"FETCHDATA",payload: productdata});
Initial state of ItemReducer must contain the "product details" which needs to be fetch from api call. While am using the above code, its returning undefined.
One possibility is to do the fetch in a parent component and only render the children (that has the useDispatch) when the productData is present. That way it can also handle error and loading states, something like this:
const ProductDetailsList = ({ products }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch({ type: "FETCHDATA", payload: products });
}, [dispatch])
return <div>...</div>;
};
const ProductsDetails = () => {
const [products, setProducts] = React.useState([])
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
useEffect(() => {
setLoading(true)
axios
.get("https://fakestoreapi.com/products")
.then((res) => setProducts(res.data))
.catch(setError)
.finally(() => setLoading(false))
}, []);
if (loading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error: {error.message}</div>
}
return <ProductDetailsList products={products} />
}
Related to the docks you have to use
fetch('https://fakestoreapi.com/products')
.then(res=>res.json())
.then(json=>console.log(json))

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 can i use useReducer to assign initial state after calling custom Datafetch hook? I keep getting null

I created a custom datafetch hook but when i use the reducer function to set it as initial state it says its null.
Component where i call the custom Hook.
const collection = 'items'
const whereClause = { array: "lists", compare: 'array-contains', value: 'Pantry' }
const res = useDataFetchWhere(collection, whereClause)
const data = res.response
const [state, dispatch] = useReducer(reducer, data)
When I console.log(state) I get null.
My custom data fetch hook
const useDataFetchWhere = (collection, whereClause) => {
const [response, setResponse] = useState(null)
const [error, setError] = useState(null)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
setError(false)
try {
await db.collection(collection).where(whereClause.array, whereClause.compare, whereClause.value).get()
.then(data => {
setResponse(data.docs.map(doc => ({ ...doc.data(), id: doc.id })))
setIsLoading(false)
console.log('hello where')
})
} catch (error) {
setError(error)
}
}
fetchData()
return function cleanup() {
console.log('cleaned up check')
};
}, [])
return { response, error, isLoading }
}
Is there anything i need to do or call in a different way?
Thanks.
The problem is that useDataFetchWhere does not immediately return the result of the data fetching, but only after a while the request is done and then the setResponse will set the actual data. So you cannot set the response as initial state for the useReducer call.
You need to wait until the request is done before using it's result. You could create an action (e.g. SET_DATA) for the reducer that sets the result once it's there.
You already have the isLoading flag available:
const [state, dispatch] = useReducer(reducer, null);
useEffect(() => {
if (!isLoading) {
const data = res.response;
dispatch({type: 'SET_DATA', data});
}
}, [isLoading]);

redux useEffect and useDispatch not working (max call stack)?

I am new to redux hooks and react hooks and this is my code that doesn't stop rendering?
const App: React.FC = () => {
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const fetchUsers = async (page: number) => {
dispatch({
type: FETCH_USERS_REQUEST,
payload: { page }
});
try {
const { data, ...result } = await api.fetchUsers(page);
const user = new schema.Entity('data');
const users = normalize(
{ users: data },
{
users: [user]
}
);
dispatch({
type: FETCH_USERS_SUCCESS,
payload: {
...result,
users
}
});
} catch (error) {
dispatch({ type: FETCH_USERS_FAILURE, payload: { error } });
}
};
useEffect(() => {
fetchUsers(1);
}, [fetchUsers]);
const users = useSelector((state: RootState) => state.users);
console.log('asd', users);
return (
<div className="vh-100 vw-100">
<header>Users</header>asdasd
</div>
);
};
fetchUsers is an async method that i plan to use multiple times on loadMore and pagination, however, this is not working, how do i make it work?
Your fetchUsers is changing on each rerender that is casing your useEffect with that fetch to trigger.
Try this:
useEffect(() => {
fetchUsers(pageNumber);
}, [pageNumber]);

Resources