I am fetching remote data from my React component. When data is ready, child components are rendered. While data is loading, the 'Data is loading ....' text is displayed.
When the component is rendered for the second time due to the props change, I set the previous data to null in order to show that new data is loading.
const List = (props) => {
const [items, setItems] = useState(null);
useEffect(() => {
setItems(null);
fetch(`http://some_url_to_fetch_items.com?PAGE=${props.page}`)
.then((data) => {
setItems(data);
})
}, [props.page]);
if (!items) {
return "Data is loading ......."
}
return (
<ul>
{items.map(item => (
<li>{item}</li>
))}
</ul>
)
}
The problem of this approach is that when the component is rendered for the second time the setItems(null); code is not executed immediately (probably because useEffect is executed asynchronously) and the component re-renderes 3 times instead of expected 2:
1-st re-render because of the props change (BUT with old data, since setItems(null) is executed too late)
2-nd re-render after setItems(null) is finally executed
3-rd re-render after data is fetched
I understand what the problem with my approach is. But I don't see another way.
It is mandatory to use hooks.
Any ideas?
A quick fix would be to add a key prop to your List:
<List key={page} page={page} />
When the value for page changes, the List component will be unmounted, and a new one rendered. The new one will not have any previous data in it, so you will only get 2 renders instead of 3.
This means you don't have to set previous data to null anymore.
If you do this, you'll need to check that the component is still mounted in your useEffect before calling setItems, otherwise you'll be trying to set state on an unmounted component. You can write:
if(setItems) {
setItems(data)
}
Changing the key prop is a bit hacky though, so I would investigate other ways to solve your issue.
Maybe you should pass the data into the list instead of the list being responsible for fetching data, and have the page state and data both inside the parent component?
For now though, changing the key using the page should work.
use this method :
useEffect(() => {
fetch(`http://some_url_to_fetch_items.com?PAGE=${props.page}`)
.then((data) => {
setItems(data);
})
}, []);
return (
<ul>
{items? items.map(item => (
<li key={item.id}>{item}</li>
)):"Data is Loading..."}
</ul>
)
Related
I have a little problem with my code. When I render my component I've got this error on console console
Maximumupdate depth exceeded. This can happen when a component repeatedly
calls setState inside componentWillUpdate or componentDidUpdate. React
limits the number of nested updates to prevent infinite loops.
Below is part of my sample code:
MainComponent:
const saveData = (data) => {
setDataArray(prevState => [...prevState, data]);
}
return (
<Fragment>
{dataArray.map((element, index) => {
return (<MyComponent data={element} index={index} save={saveData}/>)
})}
</Fragment>
)
dataArray is an array of objects
MyComponent:
const MyComponent = (props) => {
const [data, setData] = useState(props.data);
useEffect(() => {
props.save(data);
}, [data]);
}
Is the problem in updating the state up to main component? Or I can't render my child component using map()?
Notice what happens when a child component updates its data:
It calls parent.saveData
This triggers a state change in the parent because the data array has changed
Because you didn't pass a key prop to children, React has no choice but to re-render all children with the updated data
Each child notices that its data prop has changed. This triggers the useEffect, so it also calls parent.saveData
The process repeats while the original render is still taking replace, etc, etc
Solution
Pass a key prop to each child. Do not use the index as key, instead use something that reflects the data being passed to the components
Is there a point in having the initial value in useState be an empty array in this case :
const [products, setProducts] = useState([]);
useEffect(() => {
axios
.get('/shoes')
.then((res) => {
setProducts(res.data);
})
.catch((err) => {
console.error(err);
});
}, []);
When should i have initial values and when not?
Using an initial state which is the same shape as the result can make working with the stateful variable later easier. For example, if you use the empty array as the initial state, later, you'll be able to do:
return (
<div>
{products.map(prod => <span>{prod.name}</span>)}
</div>
);
Whereas if you didn't use an initial state, you'd have to make sure that products existed first:
return (
<div>
{products?.map(prod => <span>{prod.name}</span>)}
</div>
);
or
return (
<div>
{products && products.map(prod => <span>{prod.name}</span>)}
</div>
);
We usually handle our error scenario, our loading scenario within our initial API call itself. And API call is made from the useEffect().
But, useEffect() hook is invoked only after the initial render of our JSX. To handle all the above scenarios it is always best to keep the initial values of state with their appropriate data types.
Secondly, we could also have many useEffect() Hooks within the same React Component handling their respective tasks because useEffect() does take 2nd argument as list of states in an array - acting as componentDidUpdate() life cycle hook, so to have the knowledge of what state and its type we are going to use in later part of application and keeping it in useState() initially allows working with the data easy.
I've created a minimal cutting of my code to show the issue, seen below.
const PlayArea = (props) => {
const [itemsInPlay, setItemsInPlay] = useState([
{id: 'a'},
{id: 'b'}
]);
const onItemDrop = (droppedItem) => {
setItemsInPlay([...itemsInPlay, droppedItem]);
};
return (
<>
<Dropzone onDrop={onItemDrop} />
<div>
{itemsInPlay.map(item => (
<span
key={item.id}
/>
))}
</div>
</>
);
};
The dropzone detects a drop event and calls onItemDrop. However, for reasons I don't understand, I can only drop in one item. The first item I drop is correctly appended to itemsInPlay and it re-renders correctly with a third span in addition to the starting two.
However, any subsequent item I drop replaces the third item rather than being appended. It's as though onItemDrop had a stored reference to itemsInPlay which was frozen with the initial value. Why would that be? It should be getting updated on re-render with the new value, no?
The Dropzone sets its subscription token only once, when the component is initially rendered. When that occurs, the callback passed to setSubscriptionToken contains a stale value of the onCardDrop prop - it will not automatically update when the component re-renders, since the subscription was added only once.
You could either unsubscribe and resubscribe every time onCardDrop changes, using useEffect, or use the callback form of setItemsInPlay instead:
const onItemDrop = (droppedItem) => {
setItemsInPlay(items => [...items, droppedItem]);
};
This way, even if an old version of onItemDrop gets passed around, the function won't depend on the current binding of itemsInPlay being in the closure.
Another way to solve it would be to change Dropzone so that it subscribes not just once, but every time the onCardDrop changes (and unsubscribing at the end of a render), with useEffect and a dependency array.
Regardless of what you do, it would also be a good idea to unsubscribe from subscriptions when the PlayArea component dismounts, something like:
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
useEffect(
() => {
const callback = (topic: string, dropData: DropEventData) => {
if (wasEventInsideRect(dropData.mouseUpEvent, dropZoneRef.current)) {
onCardDrop(dropData.card);
setDroppedCard(dropData.card);
}
};
setSubscriptionToken(PubSub.subscribe('CARD_DROP', callback));
return () => {
// Here, unsubscribe from the CARD_DROP somehow,
// perhaps using `callback` or the subscription token
};
},
[] // run main function once, on mount. run returned function on unmount.
);
I am fetching Image URL list from a getUserCollection function which returns promise, initially state is empty in the console but once it gets the response from function,state is being updated in the console but components doesn't re-render for the updated state. I know that If i make a separate component and pass the state as prop then it sure gonna re-render for the updated state. But why it doesn't happen inside the same component? Can anybody help? Also when i pass state(userCollection) as second argument inside useEffect then its running infinite times.
import React, { useState, useEffect } from "react";
import { getUserCollection } from "../../firebase/firebase";
const CollectionPage = ({ userAuth }) => {
const [userCollection, setUserCollection] = useState([]);
useEffect(() => {
getUserCollection(userAuth.uid)
.then((response) => {
if (response) setUserCollection(response);
})
.catch((error) => console.log(error));
}, []);
console.log("updating???", userCollection); //its updating in console
return (
<main className="container">
<h3>collection page</h3>
{userCollection.map((url) => (
<div class="card-columns">
<div class="card">
<img
src={ url} // its not updating
class="card-img-top"
alt="user-collection"
width="100"
/>
</div>
</div>
))}
</main>
);
};
export default CollectionPage;
https://reactjs.org/docs/hooks-effect.html
"If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often. Also, don’t forget that React defers running useEffect until after the browser has painted, so doing extra work is less of a problem."
The 2nd argument is what triggers your component to rerender, setting that to your state variable userCollection is triggering your useEffect to run each time the state updates - causing an infinite loop because your useEffect updates the state.
What do you want to actually trigger the rerender? If it's supposed to be blank at first, and then only run once to load the images, you could change it to something like this:
useEffect(() => {
if(!userCollection){
getUserCollection(userAuth.uid)
.then((response) => {
if (response) setUserCollection(response);
})
.catch((error) => console.log(error));
}
}, [userCollection]);
With this logic, the useEffect will only update the state when it is empty. If you're expecting continuous rerenders, you should create a variable that updates when the rerenders should occur.
I have a context provider that I use to store a list of components. These components are rendered to a portal (they render absolutely positioned elements).
const A = ({children}) => {
// [{id: 1, component: () => <div>hi</>}, {}, etc ]
const [items, addItem] = useState([])
return (
<.Provider value={{items, addItem}}>
{children}
{items.map(item => createPortal(<Item />, topLevelDomNode))}
</.Provider>
)
}
Then, when I consume the context provider, I have a button that allows me to add components to the context provider state, which then renders those to the portal. This looks something like this:
const B = () => {
const {data, loading, error} = useMyRequestHook(...)
console.log('data is definitely updating!!', data) // i.e. props is definitely updating!
return (
<.Consumer>
{({addItem}) => (
<Button onClick={() => {
addItem({
id: 9,
// This component renders correctly, but DOESN'T update when data is updated
component: () => (
<SomeComponent
data={data}
/>
)
})
}}>
click to add component
</Button>
)}
</.Consumer>
)
}
Component B logs that the data is updating quite regularly. And when I click the button to add the component to the items list stored as state in the provider, it then renders as it should.
But the components in the items list don't re-render when the data property changes, even though these components receive the data property as props. I have tried using the class constructor with shouldComponentUpdate and the the component is clearly not receiving new props.
Why is this? Am I completely abusing react?
I think the reason is this.
Passing a component is not the same as rendering a component. By passing a component to a parent element, which then renders it, that component is used to render a child of the parent element and NOT the element where the component was defined.
Therefore it will never receive prop updates from where I expected it - where the component was defined. It will instead receive prop updates from where it is rendered (although the data variable is actually not coming from props in this case, which is another problem).
However, because of where it is defined. it IS forming a closure over the the props of where it is defined. That closure results in access to the data property.