wait until all elements in map function have been rendered react - javascript

I have a react component that maps over an array of objects whose content is then displayed on screen. This works perfectly, however when I check the ul children in the componentDidMount lifecycle method it is an empty array, however, a second later it contains all the items.
Does anyone know how I can wait until everything has been rendered?
I have tried componentDidUpdate but as there is a setInterval method running regularly this is firing too often.
componentDidMount() {
// this is an empty array here
console.log(this.items.children);
setInterval(() => {
this.setState((prevState) => {
return {
left: prevState.left - 1
};
});
}, 20);
}
render() {
let items = this.props.itemss.map(item => {
return (
<Item left={this.state.left} content={item.content} key={item.date} />
);
});
return (
<div>
<ul ref={(el) => { this.items = el; }} >
{ items }
</ul>
</div>
);
}

The reason you're having this issue (besides the spelling errors) is because the ref callback will not assign a value to this.items until after that component has rendered. componentDidMount is called after the first time render is called and not again until the component unmounts and remounts.
Instead, use componentDidUpdate which is called after a fresh render, in that method check to see if this.items has a value. If so, proceed, if not, just return from the function until the next the render where the ref callback succeeds.

Related

React render children incrementally

Let's say I have a React element <Parent> and I need to render an array of <FooBar>
My code currently looks something like
Parent = React.createClass({
getChildren() {
var children = this.props.messages; // this could be 100's
return children.map((child) => {
return <FooBar c={child} />;
});
},
render() {
return (
<div>
{this.getChildren()}
</div>
);
}
});
This is really slow when there are 100's of children because Parent waits for all the children to render. Is there a workaround to render the children incrementally so that Parent does not have to wait for all its children to render?
You can take a subset of messages at a time for render, and then queue up further updates with more children via a count in state.
Using requestIdleCallback or setTimeout to queue will allow you to escape React's state batching from intercepting the current browser paint, which would be a problem if you did setState directly from componentDidUpdate
Heres something to get you going
const Parent = React.createClass({
numMessagesPerRender = 10
constructor(props) {
super(props)
this.state = {
renderedCount: 0
}
}
componentWillReceiveProps(props) {
// must check that if the messages change, and reset count if you do
// you may also need to do a deep equality of values if you mutate the message elsewhere
if (props.messages !== this.props.messages) {
this.setState({renderedCount: 0})
}
}
getChildren() {
// take only the current
const children = this.props.messages.slice(0, this.state.renderedCount);
return children.map(child => <FooBar c={child} />);
},
render() {
return (
<div>
{this.getChildren()}
</div>
);
}
renderMoreMessagesPlease() {
// you MUST include an escape condition as we are calling from `componentDidXYZ`
// if you dont your component will get stuck in a render loop and crash
if (this.state.renderedCount < this.props.messages.length) {
// queue up state change until the frame has been painted
// otherwise setState can halt rendering to do batching of state changes into a single
// if your browser doesnt support requestIdleCallback, setTimeout should do same trick
this.idleCallbackId = requestIdleCallback(() => this.setState(prevState => ({
renderedCount: prevState.renderedCount + this.numMessagesPerRender
})))
}
}
componentDidMount() {
this.renderMoreMessagesPlease()
}
componentDidUpdate() {
this.renderMoreMessagesPlease()
}
componentDidUnmount() {
// clean up so cant call setState on an unmounted component
if (this.idleCallbackId) {
window.cancelIdleCallback(this.idleCallbackId)
}
}
});

Having trouble with React rendering a series of images from a map function

I'm trying to design a component where the user can click a button which will trigger a giphy API call that will eventually render a series of gif images.
So far I'm able to successfully get everything done except the actual image rendering. Here's my code so far:
retrieveURLs() {
*bunch of stuff to make API call and return an array of URLs related
to the category of the button the user pushes*
}
renderGifs() {
this.retrieveURLs().then(function(results) {
console.log(results); //logs array of URLs
return results.map(function(url, index) {
console.log(url); //logs each url
return (<img key={index} src={url} alt="" />)
}, this);
});
}
render() {
return(
<div id="gif-div">
{this.renderGifs()}
</div>
)
}
Nothing gets rendered despite each console.log() event indicating that the URL(s) are at least being passed properly.
I do something similar for the parent component to render the buttons from an array of categories that looks like this:
btnArray = [*bunch of categories here*];
renderButtons() {
return btnArray.map(function(item, i) {
let btnID = btnArray[i].replace(/\s+/g, "+");
return <button type='button' className="btn btn-info" onClick={this.handleCategorySelect} key={i} id={btnID} value={btnID}>{btnArray[i]}</button>
}, this)
}
The buttons are rendered properly, but my images are not. Neither the renderbuttons nor the rendergifs metohds alter the state. Honestly I can't see a meaningful difference between the two, so I'd like to have some help figuring out why one works but the other doesn't.
This is the nature of asynchronous functions; you can't return a value from within a callback to the original call site. If you were to write:
const res = this.retrieveURLs().then(function(results) {
return results;
});
you'd only be changing the resolution value of the promise. res won't be assigned the value of results, but rather it will be assigned the promise created by this.retrieveURLs(), and the only way to retrieve the value of a resolved promise is by attaching a .then callback.
What you could do is this:
this.retrieveURLs().then(results => {
this.setState({ images: results });
});
Now your internal state will be updated asynchronously, and your component will be told to re-render with the new data, which you can use in your render function by accessing the state.
Note: I'm using an arrow function to create the callback in the above example, because otherwise this won't be bound to the right context. Alternatively, you could do the old that = this trick, or use Function#bind.
The problem lies with the rendering function for the images and the way React does diffing between the previous and current state. Since the fetch is asynchronous and the render is not, when the fetch is completed React doesn't know that it needs to re-render you component. There are ways to force a re-render in this case, but a better solution is to use the functionality that's already part of the shouldUpdate check.
Your implementation might change to look something like the following:
class Images extends Component {
state = {
images: [],
error: null
}
componentDidMount() {
return this.retrieveImages()
.then(images => this.setState({images}))
.catch(error => this.setState({error}))
}
... rest of your class definition
render () {
return (
<div>
{this.state.images.map(image => <img src={image.url} />)}
</div>
)
}
}
I would also have some handling for bad results, missing key / values, etc. I hope that works for you, if not let me know! :)
first of all you forgot the return statement:
renderGifs() {
return this.retrieveURLs().then(function(results) {
...
}
but this won't solve anything as it is returning a Promise.
You need to save request results in the state and then map it:
constructor(props){
super(props);
this.state = { images: [] };
}
componentDidMount() {
this.renderGifs();
}
renderGifs() {
this.retrieveURLs().then(function(results) {
console.log(results); //logs array of URLs
this.stateState({ images: results });
});
}
render() {
return(
<div id="gif-div">
{
this.state.images.map((url, index) => (<img key={index} src={url} alt="" />);
}
</div>
)
}

Loop through objects React

My React Component has the following render
componentWillMount () {
var url = 'https://gist.githubusercontent.com/hart88/198f29ec5114a3ec3460/raw'
Request.get(url)
.then(data => {
this.setState({cakes: data.text});
})
}
render() {
return(
<div>
{this.state.cakes} //prints this ok
{
this.state.cakes.map(cake =>{ // error here
return <p>{cake.title}</p>;
})
}
</div>
);
}
i am trying to loop through this.state.cakes which is an array of objects.
What am i doing wrong here ?
Update - an abbreviated example of this.state.cakes:
[
{
"title": "Lemon cheesecake",
"desc": "A cheesecake made of lemon",
"image":"https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
},
{
"title":"Banana cake",
"desc":"Donkey kongs favourite",
"image":"http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
}
]
Thanks
If the state is set as the resutl of a fetch you might not be able to access the data immediately due to the async operation. You can catch this by inspecting the state and if it has no length return a message or a spinner component to indicate the data's on its way.
Once state.cakes is updated with the data from the fetch operation the component will re-render.
constructor(props) {
super(props);
this.state = { cakes: [] };
}
componentDidMount() {
fetch('/cakes')
.then(res => res.json())
.then(cakes => this.setState({ cakes }));
}
render() {
if (!this.state.cakes.length) return <Spinner />
return (
<div>
{this.state.cakes.map(cake => {
return <p>{cake.title}</p>;
})};
</div>
)
}
As the others have mentioned it's also good practice to add keys to your iterated elements.
Here:
{this.state.cakes.map((cake, i) => <p key={i}>{cake.title}</p>;)}
Do not forget to add the key attribute.
Ps: It would be better to use an unique Id instead of the array index. SO if you have an id for each array item, better write:
{this.state.cakes.map(cake => <p key={cake.id}>{cake.title}</p>;)}
I believe that you've used curly braces (understandably) where React actually requires parentheses. Since you're getting the data from a fetch, be sure to set your constructor with a preliminary cakes object as well. Try this:
constructor(props) {
super(props)
this.state = {
cakes: []
}
}
render() {
if (this.state.cakes.length > 0){
return(
<div>
{
this.state.cakes.map(cake => (
return <p>{cake.title}</p>;
))
}
</div>
);
}
return null
}
The issue is that the component is rendering and you're telling it to do something with an array called this.state.cakes, but this.state.cakes hasn't been defined yet because the fetch hasn't returned yet. Setting your constructor like this passes an empty array to the render so it doesn't freak out, and then when your data loads and your state updates, it will re-render with your data.
The reason {this.state.cakes} was, on its own, rendering just fine is because for the first split second of the component's existence, that value was undefined, which means that React basically just ignored it - once the data loaded, it rendered. However, the map method failed because you cannot pass an undefined array into map.
And as Ha Ja suggested, you should probably add a key attribute to the <p> elements.
You missed brackets inside of your map
{this.state.cakes.map(cake =>{ // errors here
return <p> {cake.title} </p>;
})}

Passing value into callback from Parent Component to Child without Binding

I have a React component built like so, that takes in a callback function from its Parent component. When the onClick fires, it calls the callback with a value from the items being mapped over:
class Child extends Component {
static propTypes = {
...
}
render = () => {
return (
<div>
{this.props.data.map((el, idx) => {
return <section onClick={() = this.props.cb(el.val)}></section>
}
)}
</div>
);
}
}
What I'm wondering is how I can accomplish passing a value from that map to the callback without using this syntax () => this.props.cb(item.val) or binding a value to the callback. I can't just pass the callback to onClick because it fires immediately with the value, either.
The current syntax works but breaks the rules I have setup in my linter.
An alternative to binding in render is breaking out a new component which takes the callback and the value to be passed:
class Section extends React.Component {
handleClick = () => {
this.props.onClick(this.props.val)
}
render() {
return <section onClick={this.handleClick}></section>
}
}
(I would suggest just disabling that lint rule, though)

Append to infinite scroll in reactJs

I'm making a infinite scroll in ReactJs with posts.
I have a react class called AllPosts and Post. AllPosts render multiple Posts.
I have this code:
ReactDOM.render(
<AllPosts data={posts} />,
document.querySelector(render_to)
);
And below is the method
// Render all posts
var AllPosts = React.createClass({
render: function () {
return (
<div>
{this.props.data.map(function(element, i) {
return <Post data={element} />
})}
</div>
); .....
But, I have an event in scroll and I want to append another react Post. How can I do this?
This is one of those awesome things React is great at :)
On the assumption that you don't want to use a Flux/Redux implementation, I would set the posts data as the state on your root component. That way, when posts changes, the component will re-render:
var AllPosts = React.createClass({
getInitialState() {
// Start with an empty array of posts.
// Ideally, you want this component to do the data fetching.
// If the data comes from a non-react source, as in your example,
// you can do `posts: this.props.posts`, but this is an anti-pattern.
return { posts: [] }
},
componentWillMount() {
// Fetch the initial list of posts
// I'm assuming here that you have some external method that fetches
// the posts, and returns them in a callback.
// (Sorry for the arrow functions, it's just so much neater with them!)
ajaxMethodToFetchPosts(posts => {
this.setState({ posts: posts });
})
},
componentDidMount() {
// Attach your scroll handler here, to fetch new posts
// In a real example you'll want to throttle this to avoid too many requests
window.addEventListener('scroll', () => {
ajaxMethodToFetchPosts( posts => {
this.setState({
posts: this.state.posts.slice().concat(posts)
});
});
});
},
render() {
// Render method unchanged :)
return (
<div>
{this.props.data.map(function(element, i) {
return <Post data={element} />
})}
</div>
);
}
});
With other frameworks, you have to deal with scroll position (if the element gets completely re-rendered, the elements disappear momentarily and your scroll position is reset).
React's render function doesn't actually just render its output to the DOM; it compares the potential output with what's already there, and only applies the difference. This means that only new posts are added to the DOM, and your scroll position will remain unaffected.

Categories