I am just diving deep into React. But the useEffect React hook still got me confused. I know that I can pass dependencies as an array to it to control rendering of the component. I have used props and local state to do and it works.
What's got me still confused is when I pass redux reducer as a dependency. It causes an infinite loop of rendering the component.
users component
const usersComp = () => {
const users = useSelector(state => state.users);
useEffect(
// Fetch users and update users state
useDispatch().dispatch(getUsers)
,[users]) // <-- Causes an infinite loop!!
if(users.length){
return( users.map(user => <p>{user}</p>))
}
}
getUsers Redux Thunk function
export async function getUsers(dispatch, getState) {
fetch(endpoint)
.then(response => response.json())
.then(users => {
dispatch({type: GET_USERS, payload: users})
}).catch(err => console.error("Error: ", err));
}
users reducer
export default function usersReducer(state = [], action) {
switch (action.type) {
case GET_USERS : {
return [...state, action.payload]
}
}
}
From what I understand, users start off as an empty array, and then gets filled with data from an API call. So useEffect should fire twice; when the component has just mounted and then when users state changes from the API call. So what's causing the infinite loop?
Remove users from the useEffect dependency, because you want to fetch users when component mounts, not each time the users is changed.
useEffect(
useDispatch().dispatch(getUsers)
,[]) // Now, it will fetch users ONLY ONCE when component is mounted
Explanation:
// Case 1
useEffect(() => {
console.log("Mounted") // Printed only once when component is mounted
}, [])
// Case 2
useEffect(() => {
console.log("users changed") // Printed each time when users is changed
}, [users])
So, if you do fetch in Case 2, it will change users which will retrigger the hook which will fetch the users again which changes the users and causes the hook to retrigger → This is an infinite loop.
Update:
Why is state.users getting changed (in this code), as detected by useEffect, even when values of state.users are "SAME" (Same values)?
Whenever GET_USERS action is dispatched, reducer returns new state ({ ...state, users: action.payload }). It does so even when value of action.payload holds the same value of users.
This is why useEffect receives new users array. (They do shallow comparison.)
Note that, [1, 2,3] is not equal to [1, 2,3] i.e. [1, 2,3] === [1, 2,3] returns false.
If for some reason, you want to return the same redux state, do return state in the reducer. This is often what we do in the default case of switch of reducer.
Related
I am using the shopify-buy SDK, which allows me to retrieve the current cart of the user. I am trying to store that cart in my CartProvider which is then used in my Cart component. The problem is when I retrieve information from the cart it's acting a little slow so my component needs to be updated when the state changes, currently I have the following in my getShopifyCart function which is located in my CartProvider.
const [cartItems, setCartItems] = useState([])
const getShopifyCart = () => {
return client.checkout
.fetch(currentVendor.cartId)
.then((res) => {
const lineItemsData = res.lineItems.map((item) => {
return {
title: item.title,
quantity: item.quantity,
}
})
setCartItems(lineItemsData)
setLoading(false)
})
.catch((err) => console.log(err))
}
In my Cart component I have the following useEffect.
useEffect(() => {
getShopifyCart()
}, [cartItems])
But this causes an infinite loop, even though the cartItems state isn't changing.
You are setting the state cartItems inside getShopifyCart which you are calling inside a useEffect which has cartItems as a dependency. Even though the content of the data has not changed, you are creating a new object, hence its hash has changed as well which causes the useEffect to be called again.
If you want to initially fetch the data and set the state, then you need to pass an empty dependency array.
useEffect(() => {
getShopifyCart()
}, [])
Consider the code :
import React, { useState, useEffect } from 'react';
........ More stuff
const ProductContext = React.createContext();
const ProductConsumer = ProductContext.Consumer;
const ProductProvider = ({ children }) => {
const [state, setState] = useState({
sideBarOpen: false,
cartOpen: true,
cartItems: 10,
links: linkData,
socialIcons: socialData,
cart: [],
cartSubTotal: 0,
cartTax: 0,
cartTotal: 0,
.......
loading: true,
cartCounter: 0,
});
const getTotals = () => {
// .. Do some calculations ....
return {
cartItems,
subTotal,
tax,
total,
};
};
const addTotals = () => {
const totals = getTotals();
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
};
/**
* Use Effect only when cart has been changed
*/
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
..... More code
return (
<ProductContext.Provider
value={{
...state,
............... More stuff
}}
>
{children}
</ProductContext.Provider>
);
};
export { ProductProvider, ProductConsumer };
This is a Context of a Shopping cart ,whenever the user add a new item to the cart
this piece of code runs :
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
And updates the state , however the setState function doesn't update state
when running :
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
Inside addTotals , even though this function is being called automatically when UseEffect detects that state.cartCounter has been changed.
Why aren't the changes being reflected in the state variable ?
Without a stripped down working example, I can only guess at the problems...
Potential Problem 1
You're calling a callback function in useEffect which should be added to it's [dependencies] for memoization.
const dep2 = React.useCallback(() => {}, []);
useEffect(() => {
if(dep1 > 0) {
dep2();
}
}, [dep1, dep2]);
Since dep2 is a callback function, if it's not wrapped in a React.useCallback, then it could potentially cause an infinite re-render if it's changed.
Potential Problem 2
You're mutating the state object or one of its properties. Since I'm not seeing the full code, this is only an assumption. But Array methods like: splice, push, unshift, shift, pop, sort to name a few cause mutations to the original Array. In addition, objects can be mutated by using delete prop or obj.name = "example" or obj["total"] = 2. Again, without the full code, it's just a guess.
Potential Problem 3
You're attempting to spread stale state when it's executed. When using multiple setState calls to update an object, there's no guarantee that the state is going to be up-to-date when it's executed. Best practice is to pass setState a function which accepts the current state as an argument and returns an updated state object:
setState(prevState => ({
...prevState,
prop1: prevState.prop1 + 1
}));
This ensures the state is always up-to-date when it's being batch executed. For example, if the first setState updates cartTotal: 11, then prevState.cartTotal is guaranteed to be 11 when the next setState is executed.
Potential Problem 4
If state.cartCounter is ever updated within this component, then this will cause an infinite re-render loop because the useEffect listens and fires every time it changes. This may or may not be a problem within your project, but it's something to be aware of. A workaround is to trigger a boolean to prevent addTotals from executing more than once. Since the prop name "cartCounter" is a number and is rather ambiguous to its overall functionality, then it may not be the best way to update the cart totals synchronously.
React.useEffect(() => {
if (state.cartCounter > 0 && state.updateCart) {
addTotals();
...etc
}
}, [state.updateCart, state.cartCounter, addTotals]);
Working demo (click the Add to Cart button to update cart state):
If neither of the problems mentioned above solves your problem, then I'd recommend creating a mwe. Otherwise, it's a guessing game.
reducer shows that I've received doc data from Firestore. docStore.subscribe is listening, but is not updating with latest doc data.
Expected outcome: upon page load, will get docId from URL, and query Firestore. Upon receiving data, update the store, and subscribe to populate the view with doc information.
homepage.js
const Homepage = ({ docId }) => {
const [doc, setdoc] = useState(false);
console.log(docId); // <-- 123
docStore.subscribe(() => {
console.log('docStore state changed:', docStore.getState()); // <-- docStore state changed: undefined
setdoc(docStore.getState());
})
return (
<div>
<div>{docId}</div> {/* 123 */}
<div>{doc.docName}</div> {/* blank */}
</div>
);
};
reducer.js
export default function reducer(state = {}, action) {
switch (action.type) {
case docTypes.LOAD_DOC_PAGE:
firebase.firestore().collection("docs")
.where('docId', '==', action.payload.docId.toLowerCase())
.get()
.then(function (data) {
if (data.docs.length === 1) {
state = data.docs[0].data();
}
console.log('gotten doc', state) // <-- gotten doc data
return state;
});
}
}
Your reducer is very broken. Reducers must never make async calls!. That API call needs to be moved somewhere else entirely, and then you should dispatch an action that will cause the reducer to run and calculate an updated state.
Also, you generally shouldn't subscribe to the store yourself. Use the React-Redux connect and useSelector APIs to extract data needed by components from the store state.
This is a function which has to be called when the component mounts on DOM
const [dashboardData, setDashboardData] = useState('');
const loadDashboardData = () => {
console.log("Loading Dashboard Data ", campaign);
Utils.request({
url: `campaign/user/info`
}).then(
res => {
console.log("dashboard data" , res.data)
setDashboardData(res.data);
},
err => console.log(err)
)
}
useEffect(() => {
loadDashboardData();
console.log("campaigndata",dashboardData);
}, []);
when I console dashboardData in useEffect, it shows nothing but a string i.e campaigndata which I passed as the first argument in console.log. what I think that my dashboard state variable is not getting updated
Answer
Write another useEffect just for dashboardData.
useEffect(() => {
console.log("campaigndata",dashboardData);
}, [dashboardData]);
Explanation
Your useEffect is taking an empty array as the second argument, which makes it run only the first time (as DidMount), so it won't re-run on component re-render after changing the state, that's why it is showing empty string as the initial state.
Writing another useEffect for the variable dashboardData will run as many times as the dashboardData changes.
You can just pass the 'dashboard' to the existing useEffect as dependency. The existing useEffect will work as both componentDidMount and componentDidUpdate.
So no need to write another useEffect.
useEffect(() => {
loadDashboardData();
console.log("campaigndata",dashboardData);
}, [dashboardData]);
I have a React functional component with two state variables (itemsData & itemsCollections). The variables are updated in the useEffect method. But after useEffect occur one of the state variables is null.
Upon switching the setStateFunctions (setItemsData & setItemsCollect) call order both arguments are inialized as expected.
How's that?
const MyComponent = ({itemsIds}) => {
const [itemsData, setItemsData] = useState([]);
const [itemsCollections, setItemsCollect] = useState({});
useEffect(() => {
fetchItemsData({ itemsIds }).then(({ items, itemCollect }) => {
setItemsData(items);
setItemsCollect(itemCollect);
})
}, [itemsIds]);
...
console.log('itemsData', itemsData) // the expected array
console.log('itemCollect', itemCollect) // empty objecy
State after useEffect: itemCollect = {}, itemsData = [{value:...},...]
Switching the order of the calls:
const MyComponent = ({itemsIds}) => {
...
useEffect(() => {
fetchItemsData({ itemsIds }).then(({ items, itemCollect }) => {
setItemsCollect(itemCollect); // <--> switched rows
setItemsData(items); // <--> switched rows
})
}, [itemsIds]);
...
console.log('itemsData', itemsData) // the expected array
console.log('itemCollect', itemCollect) // the expected object
State after useEffect: itemCollect = { someValue: ...} , itemsData = [{value:...},...]
There is a performance optimization called batching, which can change between React versions. When this optimization is applied, multiple setState calls will be batched together before the next render (and the order does not matter).
When not applied (e.g. inside a Promise as in your case, see Does React batch state update functions when using hooks?), then each state update will trigger a new render (and the order matters).
=> console.log('itemCollect', itemCollect) may log different data in each render.
If you need to force a single state update, then calling a single dispatch from useReducer might be the best option.