I have this action
export function fetchBranches() {
return async dispatch => {
const result = await axios.get('https://localhost:5002/Branches')
dispatch({ type: FETCH_BRANCHES, payload: result.data.value })
}
}
and such reducer
export const branchReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_BRANCHES: {
return { ...state, branches: action.payload }
}
default: return state
}
}
In my component, I'm try to do such thing
const dispatch = useDispatch()
dispatch(fetchBranches())
const branches = useSelector(state => state.branches.branches)
return <>
some component that uses branches
</>
So my problem is i'm getting infinite number of request trying to fetch (I can see them in redux dev tools).
My page are not getting updated, but if go to other page and then return to one that tries perform this fetch, I'm can see values in store and at the page. So questions are:
Where and how should I dispatch actions to fetch some data to render it then?
Why I'm getting that much requests and how can I fix it?
UPD:
Thanks a lot for your answers, but I still see behavior that my component rendered before I received data from api. I wanted to try useState and set state in useEffect, but I can't use useSelector. What should I do to re-render component as soon as my data loaded?
UPD2: now my component look like
function BranchList() {
const [isLoaded, setIsLoaded] = useState(false)
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchBranches())
setIsLoaded(true)
}, [])
const branches = useSelector(state => state.branches.branches)
const headerNames = ["Id", "Name"]
if (!isLoaded) {
return <>Loading...</>
}
return (
<EditableTable
data={branches}
headerNames={headerNames}
onEditConfirm={(row) => dispatch(updateBranch(row))}
onDelete={(id) => dispatch(deleteBranch(id))} />
)
}
Dispatching generally should never be done directly in render, but in a useEffect or an event callback.
In your case,
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchBranches());
},
[dispatch]
)
const branches = useSelector(state => state.branches.branches)
Also please note that you are writing a pretty old style of redux here - please read the official tutorials to learn the recommended "modern" style of redux we are officially recommending. You'll end up writing a lot less code.
Perhaps if you try this:
function BranchList() {
const [isLoaded, setIsLoaded] = useState(false)
const dispatch = useDispatch()
useEffect(() => {
if(!isLoaded) {
dispatch(fetchBranches())
.then(() => setIsLoaded(true))
}
}, [isLoaded])
const branches = useSelector(state => state.branches.branches)
const headerNames = ["Id", "Name"]
if (!isLoaded) {
return <>Loading...</>
}
return (
<EditableTable
data={branches}
headerNames={headerNames}
onEditConfirm={(row) => dispatch(updateBranch(row))}
onDelete={(id) => dispatch(deleteBranch(id))} />
)
}
Your HTTP request action causes side effects in the component. Every state change causes re-rendering the component. To avoid side effects, you should use useEffect hook in your component.
In your component,
const dispatch = useDispatch()
const onFetchBranches = useCallback(() => dispatch(fetchBranches()), [dispatch]);
const branches = useSelector(state => state.branches.branches)
useEffect(() => {
onFetchBranches();
}, [onFetchBranches]);
return <>
some component that uses branches
</>
You should check Reactjs documentation to understand useEffect hook better.
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
dispatch an action into useEffect hook to solve your issue.
Try:
useEffect(() => dispatch(fetchBranches()), [])
You should try putting your dispatch action into an useEffect, so the code will be only executed once like so:
const dispatch = useDispatch()
React.useEffect(() => {
dispatch(fetchBranches())
}, [])
Documentation here.
Related
could you provide your feedback on the code below:
export function useUnmountSafeReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>] {
const [mounted, setMounted] = useState(true);
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
return () => {
setMounted(false);
};
}, []);
return [state, mounted ? dispatch : () => {}];
}
I am trying to write own reducer which will not use dispatch if component is unmounted.
Try with a ref instead of a state.
const mounted = useRef(true)
useEffect(() => {
return () => {
mounted.current = false
}
}, [])
The reason is that using setMounted is a memory leak used in the destroy function of useEffect. Keep in mind if the component is unmounted, you are not supposed to use any internal method after that. Actually avoiding the memory leak is your reason to implement this mounted at the first place, isn't it?
disabled dispatch
Now the question is can you return a new dispatch after the unmount?
return [state, mounted ? dispatch : () => {}]
After the unmount, there probably won't be any more update to the UI . So the way to get it working is to disable the existing dispatch but not providing an empty one.
const _dispatch = useCallback((v) => {
if (!mounted || !mounted.current) return
dispatch(v)
}, [])
return [state, _dispatch]
The useCallback there might be optional.
I tried to dispatch the API call using redux in useEffect hooks. After the response came to redux-saga response goes to reducer and the reducer updated the state successfully but my component is not refreshing.
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import SubscriptionComponent from '../../Components/Subscription/Subscription';
import SubscriptionActions from '../../Redux/Subscription/Actions';
import {
getMySubscriptions,
getMySubscriptionByName,
getMySubscriptionByGroup,
} from '../../Redux/Subscription/Selectors';
const Subscription = (props) => {
const { navigation } = { ...props };
const [visible, setVisible] = useState(false);
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const dispatch = useDispatch();
const [data, setData] = useState(subscriptionList);
const payload = {
memberId: '604f2ad047bc495a0a7fad26',
vendorId: '5fd484c39590020dc0dfb82a',
vendorOrgId: '5fd484439590020dc0dfb829',
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
const onHandleSubscriptionByName = () => {
setVisible(false);
const subscription = getMySubscriptionByName(data);
setData(subscription);
};
const onHandleSubscriptionByGroup = () => {
setVisible(false);
const subscription = getMySubscriptionByGroup(data);
setData(subscription);
};
return (
<SubscriptionComponent
list={data}
navigation={navigation}
onPressList={(val) =>
navigation.navigate('SubscriptionDetails', { _id: val._id, name: val.name })
}
visible={visible}
openMenu={() => setVisible(!visible)}
closeMenu={() => setVisible(!visible)}
sortByName={() => onHandleSubscriptionByName()}
sortBySub={() => onHandleSubscriptionByGroup()}
/>
);
};
export default Subscription;
used reselect to get the state from redux.
export const getMySubscriptions = createSelector(mySubscriptionSelector, (x) => {
const mySubscriptions = x.map((item) => {
return {
_id: item._id,
image: 'item.image,
description: item.description,
name: item.name,
subscriptionGroup: item.subscriptionGroup,
subscriptionAmount: item.subscriptionAmount,
status: item.status,
delivery: item.delivery,
product: item.product,
};
});
return mySubscriptions ;
});
Why component is not refreshing.
Thanks!!!
You're storing the selection result in local state.
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const [data, setData] = useState(subscriptionList);
useState(subscriptionList) will only set data initially not on every update.
EDIT:
Your setup is a little odd:
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
Using data in the dependency array of useEffect, will cause refetching the data, whenever data is updated. Why? I looks like your sorting is working locally, so no need to refetch?
I would suggest to store the sort criteria (byName, byGroup) also in Redux and eliminate local component state, like that:
// ToDo: rewrite getMySubscriptions so that it considers sortCriteria from Redux State
const subscriptionList = useSelector(getMySubscriptions);
const dispatch = useDispatch();
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
// Empty dependency array, so we're only fetching data once when component is mounted
}, []);
const onHandleSubscriptionByName = () => {
dispatch(SubscriptionActions.setSortCriteria('byName'));
};
const onHandleSubscriptionByGroup = () => {
dispatch(SubscriptionActions.setSortCriteria('byGroup'));
};
As mentioned in the comments you will need to add a new action setSortCriteria and reducer to handle the sorting and adjust your selector, so that it filters the subscription list when a sortCriteria is active.
You do not update data after fetching new subscription.
const [data, setData] = useState(subscriptionList);
Only initializes data, but does not update it, you need to add useEffect to update data:
useEffect(() => {
setData(subscriptionList);
}, [JSON.stringify(subscriptionList)]);
JSON.stringify only used for deep compare complex objects, since useEffect only runs shallow compare and might miss, changes in objects.
-----EDIT------
Other problem might be that your getMySubscriptions function might need deep compare, since useSelector by itself doesn't do that, example might be:
import { useSelector, shallowEqual } from 'react-redux';
const subscriptionList = useSelector((state) => getMySubscriptions(state), shallowEqual);
Note that both solutions must be used.
I found many similar questions here about React Hook useEffect has a missing dependency. I have already checked them, but I didn't find solutions as I faced. I want to pass redux thunk function as a parameter to React custom hook.
Below is my code and it is working fine. But, I got dependency missing warning, I don't want to add ignore warning eslint. If I add dispatchAction to dependency array list, it is dispatching again and again because redux thunk asyn function has fulfilled, reject, pending.
Custom Hook
const useFetchData = (dispatchAction, page) => {
const dispatch = useDispatch();
const [loadMoreLoading, setLoadMoreLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState();
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
fetchData();
}, [dispatch, page]);
return [loadMoreLoading, errorMsg]; // it is asking for adding dispatchAction.
My component
const SomeListing = ({userId}) => {
const [page, setPage] = useState(1);
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}),
page,
);
}
So, is there any way to be able to add redux thunk function in react custom hook?
The function fetchPropertyByUserId, when called i.e. fetchPropertyByUserId({userId: userId, page: page}), returns an "actionCreator" function.
Hence, when you call this function at the place of first parameter of your hook useFetchData, it returns a new "actionCreator" function each time (we know that hooks are called at each render):
In SomeListing.jsx:
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}), // <-- Here: it returns a new "actionCreator" function at call (render)
page,
);
And, as soon as you put this function (first parameter of the hook i.e. dispatchAction) as a dependency of useEffect, it should cause an infinite execution of the effect because, now we know, that dispatchAction is getting created (hence, changed) at every render.
In useFetchData.js:
export const useFetchData = (dispatchAction, page) => {
// ...
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true)
const resultAction = await dispatch(dispatchAction)
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message)
}
setLoadMoreLoading(false)
}
fetchData()
}, [dispatch, dispatchAction, page]) // <-- "dispatchAction" added here
// ...
How to fix it?
Pass a memoized actionCreator function:
In SomeListing.jsx:
export const SomeListing = ({ userId }) => {
const [page, setPage] = useState(1)
// Here: "fetchPropertyByUserIdMemo" is memoized now
const fetchPropertyByUserIdMemo = useMemo(
() => fetchPropertyByUserId({ userId: userId, page: page }),
[page, userId]
)
const [loadMoreLoading, errorMsg] = useFetchData(fetchPropertyByUserIdMemo, page)
// ...
}
How about extracting the fetch method from useEffect?:
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
useEffect(() => {
fetchData();
}, [fetchData]);
So I am getting this Warning:-
*Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Products (created by Context.Consumer)*
Well, it's occurring in the Products Component When I Reroute to Product Edit component!
Products component is used to list all products and product edit is used to edit product details and both these components are connected to the same context API using useContext.
My Context API provider looks like this
*import React, { useState , createContext , useEffect} from 'react';
import firebase from "../firebase";
export const ProdContext = createContext();
const ProdContextProvider = props => {
const [products , setproducts]= useState([])
const [loading , setloading] = useState(true)
const [subcribe , setsubcribe] = useState(false)
const unsub =()=>{
setsubcribe(false);
console.log("unsubcribe--"+subcribe)
}
const sub =()=>{
setsubcribe(true);
console.log("subcribe--"+subcribe)
}
useEffect(() => {
let mounted = true;
setloading(true)
async function fetchData() {
setloading(true);
await firebase.firestore()
.collection("products")
.onSnapshot((snapshot)=>{
const data = snapshot.docs.map((doc)=>(
{
id : doc.id,
...doc.data()
}
))
console.log("b4 if--"+subcribe)
if(subcribe){
console.log("in if--"+subcribe)
setproducts(data)
setloading(false)
}
})
}
fetchData();
return () => mounted = false;
}, [subcribe])
console.log("after getting bottom"+subcribe)
return (
<ProdContext.Provider value={{subcribe:subcribe,prodloading:loading, products: products, loading:loading , sub:sub , unsub:unsub}}>
{props.children}
</ProdContext.Provider>
);
}
export default ProdContextProvider;*
And my products Component looks like this:
export default function Products(props){
const {products , loading ,sub , unsub,subcribe}= useContext(ProdContext)
const [selectid, setselectid] = useState("")
const [ShowLoading, setShowLoading] = useState(true);
const [showAlert2, setShowAlert2] = useState(false);
const [redirect , setredirect] = useState(false)
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
sub();
console.log("product mound--"+subcribe)
}, [])
useEffect(() => {
return () => {
console.log("product unsub--"+subcribe)
unsub();
console.log("product unmound--"+subcribe)
};
}, []);
if (redirect) {
return <Redirect push to={{
pathname: `/products/${selectid}`,
}} />;
}
return ( .........)}
Product Edit Component:
const Productedit = (props) => {
const {products,loading , subcribe} = useContext(ProdContext);
const { sub,unsub } = useContext(ProdContext);
const [formData, setformData] = useState({});
const {category} = useContext(CatContext)
const [showLoading, setShowLoading] = useState(false);
const [mainurl, setmainurl] = useState(null);
const [imggal, setimggal] = useState([]);
const [situation , setsituation] = useState("")
const [redirect , setredirect] = useState(false)
const [showAlert, setShowAlert] = useState(false);
const [msg, setmsg] = useState(true);
useEffect(() => {
sub();
console.log("productedit mound--"+subcribe)
return () => unsub();
}, [])
...........
Well I think the issue is that products component is still subscribed to getproduct provider even when it is unmounted but I cant get to solve the issue anyone can help
Error message Details and console log?
The issue is not related to Firestore rather this seems to be a common issue with react.
We just no need to update the state in the callback if the component is not mounted already.
It's better to check before updating the state by adding an if block in the async function
if(mounted) {// Code inside the async tasks}
Please refer here [1] for additional information about this warning.
[1] https://www.debuggr.io/react-update-unmounted-component/#state-updates
(This should have been a comment as I don't have enough reputation hence posting as an answer)
Hope I understood the question right and the suggestion helps!
You are not using mounted except only initializing it, you should not update your react state if your component is unmounted. You should do this inside your callback after successful API request:
if(mounted){
// your state update goes here
}
But this is not the right though, what you should do is cancel your API request when component unmounts, I know little bit about request cancellation while using axios. You should search the web about API request cancellation in firebase or whatever you are using because if you are not updating your react state if component unmounts then still API request continue to run in the background and that is the main issue and that's why react throws this warning.
I am trying to dispatch an action to fecth an API when the home screen appears but I have an infinite loop.
My project :
React Native app
Redux
redux-axios-middleware
React Navigation (I am using a Drawer)
What I tried:
const dispatch = useDispatch();
useEffect(() => {
const fetchNewMails = () =>
return dispatch(ACTIONS.mailActions.fetchMails());
};
fetchNewMails();
}, [dispatch]);
-------
const dispatch = useDispatch();
const fetchNewMails = useCallback(() => {
return dispatch(ACTIONS.mailActions.fetchMails());
}, [dispatch]);
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
fetchNewMails();
});
return unsubscribe;
}, [fetchNewMails, navigation]);
A simple console.log is not producing infinite loop but the dispatch action yes ..
Thanks in advance if you have any idea.
The answer is specific to my project but here it is:
I displayed a LoadingScreen in the App.js depending of Redux variable value, it was causing re-rendering.
Solution : Keep the LoadingScreen independent and delete it from App.js.
Issue solved.