I have a container element, which is also a React component, with a specific height. I also have an API that returns blocks of contents of variable length, based on the requested ID.
<Contents/>
I want to request a new block of content from the API until the container is overflowing.
My code works, but it rerenders all content blocks and adds one in each render, until the container is full. This seems like a bad approach.
class Contents extends React.Component {
constructor() {
super();
this.state = {
numElements: 0
};
}
render() {
const elements = [];
for(let i = 0; i < this.state.numElements; i++) {
elements.push(this._getElementContents(i));
}
return(<div id="contents">
{elements.map(element => element)}
</div>);
}
componentDidMount() {
// start the 'filling loop'
this._addElement();
}
componentDidUpdate() {
// keep adding stuff until container is full
if(document.getElementById('contents').clientHeight < window.outerHeight - 400) {
this._addElement();
}
}
_addElement() {
// setState will cause render() to be called again
this.setState({numElements: this.state.numElements + 1});
}
_getElementContents(i) {
// simplified, gets stuff from API:
let contents = api_response;
return(<Element key={i} body={contents} />);
}
}
How can I append elements to the container until it is filled, without re-adding, re-querying the API and re-rendering existing elements on each loop?
I can't see how you are calling your API or under what condition. From my understanding you should do two things.
Keep elements array into the state object and push new elements whenever they arrive.
Use the shouldComponentUpdate instead of componentDidUpdate with the same exactly condition to judge when you have to request more elements from your API.
Eventually draw the state.elements. Whenever you receive a new one you just use the local elements you previously got to redraw the component instead of making all API calls all over again
Because this.setState() triggers a re-render, like dimitrisk said, you should put logic in shouldComponentUpdate to only render once you have everything ready to go to the DOM.
You should look into Dynamic Children in React. It helps React do reconciliation when re-rendering http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
http://revelry.co/dynamic-child-components-with-react/
Related
I know there are a bunch of answers out there for this question, but I feel like none of them were quite what I was looking for. My problem is that I have this application that gets a list of stores within a desired radius. When I click submit, I populate an array with those stores information. What I want to do is be able to store some of that information into an array in my react state. My main issue is that I would like to set state when that information is populated, but this causes an infinite loop because setState calls the render method again. Hopefully this explains my issue a little better, there is some code to go with this below as well. Any help is appreciated, thanks!
stores.js
Class stores extends React.Component{
constructor(props){
this.state = {
StoreInfo: [],
Zip: "",
Radius: ""
}
}
RenderTable(zip, radius){
.... some stuff to generate stores within this location
storeInfo = []
for(var i = 0; i < storeArray.length; i++){
storeInfo.push(storeArray.info);
}
this.setState({StoreInfo: storeInfo});
... generate html to be rendered
}
render(){
var renderPage = (
<div>
{this.RenderTable(this.state.Zip, this.state.Radius)}
</div>
);
return renderPage;
}
This is obviously just an example of what my code does, and is not the exact file since the original is thousands of lines long.
Don't do it on render(). Instead, you can do it using the hook componentDidMount() inside your class, like this:
componentDidMount() {
if(this.state.StoreInfo.length === 0) {
//set your state here
}
}
Inside your if statement, you can verify the length of some data to prevent it to enter in the render loop, don't need to be the length of some information array inside your state, but something that flags to your code that you already loaded up the information. You can also create a bool for this and set it to true (or false) when you load your data.
I have a <BlogPost> component which could've been a Stateless Function Component, but turned out as a Class Stateful Component because of the following:
The blogPost items that it renders (receiving as props) have images embedded in their html marked content which I parse using the marked library and render as a blog post with images in between its paragraphs, h1, h2, h3, etc.
The fact is that I need to preload those images before rendering the post content to my client. I think it's a UX disaster if you start reading a paragraph and all of a sudden it moves down 400px because the image that was being loaded has been mounted to the DOM during the time you were reading it.
So I prefer to hold on by rendering a <Spinner/> until my images are ready. That's why the <BlogPost> is a class component with the following code:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.state={
pending: true,
imagesToLoad: 0,
imagesLoaded: 0
};
}
preloadImages(blogPostMedia) {
this.setState({
pending: true,
imagesToLoad: 0,
imagesLoaded: 0
});
... some more code ...
// Get images urls and create <img> elements to force browser download
// Set pending to false, and imagesToLoad will be = imagedLoaded
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props !== nextProps) {
this.preloadImages(nextProps.singleBlogPost.media);
}
}
componentDidMount() {
this.preloadImages(this.props.singleBlogPost.media);
}
render() {
return(
this.state.pending ?
<Spinner/>
: (this.state.imagesLoaded < this.state.imagesToLoad) ?
<Spinner/>
: <BlogPostStyledDiv dangerouslySetInnerHTML={getParsedMarkdown(this.props.singleBlogPost.content)}/>
);
}
}
export default BlogPost;
At first I was calling the preloadImages() only inside the componentDidMount() method. And that works flawlessly for the first post I render with it.
But as soon as I would click on the next post link; since my <BlogPost>component is already mounted, componentDidMount() doesn't get called again and all the subsequent posts I would render by clicking on links (this is a Single Page App) wouldn't benefit from the preloadImages() feature.
So I needed a way to reset the state and preload the images of the new blogPost received as props inside an update cycle, since the <BlogPost> component it's already mounted.
I decided to call the same preloadImages() function from inside the UNSAFE_componentWillReceiveProps() method. Basically it is reseting my state to initial conditions, so a <Spinner/> shows up right away, and the blog post only renders when all the images have been loaded.
It's working as intended, but since the name of the method contains the word "UNSAFE", I'm curious if there's a better way to do it. Even though I think I'm not doing anything "unsafe" inside of it. My component is still respectful to its props and doesn't change them in anyway. It just been reset to its initial behavior.
RECAP: What I need is a way to reset my already mounted component to its initial state and call the preloadImages() method (inside an update cycle) so it will behave as it was freshly mounted. Is there a better way or what I did is just fine? Thanks.
I would stop using componentWillReceiveProps()(resource). If you don't want the jarring effect, one way you can avoid it is to load the information from <BlogPost/>'s parent, and only once the information is loaded, to pass it into <BlogPost/> as a prop.
But anyway, you can use keys to reset a component back to its original state by recreating it from scratch (resource).
componentWillReceiveProps is deprecated, it's supposed to be replaced with either getDerivedStateFromProps or componentDidUpdate, depending on the case.
Since preloadImages is asynchronous side effect, it should be called in both componentDidMount and componentDidUpdate:
componentDidMount() {
this.preloadImages(this.props.singleBlogPost.media);
}
componentDidUpdate() {
this.preloadImages(this.props.singleBlogPost.media);
}
So let's say there is acomponent which displays 2 child components: a document list and the selected document. By default the selected document component is not rendered, only when a document is selected from the list. And i also want this whole thing work when a new document is selected from the list.
There is a state which holds the document content and responsible for the selected document rendering, so i thought i'm going to set it to null in the method which handles the list item selection in order to unmount the previously created child component. Like this (excerpts from the parent class):
handleResultListItemClick(docname) {
if (this.state.sectioncontainer != null) this.setState({sectioncontainer: null},()=>{console.log("muhoo");});
var selected_doc = this.state.resultlist.filter((doc) => {
return docname === doc.properties.title;
});
this.setState({sectioncontainer: selected_doc[0].content.sections},()=>{console.log("boohoo");});
}
...
render() {
return (
...
{this.state.sectioncontainer != null && <SectionContainer listOfSections={this.state.sectioncontainer}/>}
);
}
The only problem is that state handling is not fast enough (or something) in react, because putting the state nullification and its new value setting in the same method results in no change in ReactDOM.
With the above code, the component will be created when the parent component first rendered, but after selecting a new doc in the list results in no change.
How should i implement this in way which works and also elegant?
I found this: ReactDOM.unmountComponentAtNode(container) in the official react docs. Is this the only way? If yes, how could i get this container 'name'?
Edit:
Based on the answers and thinking the problem a bit more through, i have to explain more of the context.
As kingdaro explained, i understand why there is no need to unmount a child component on a basic level, but maybe my problem is bit more sophisticated. So why did i want to unmount the child?
The documents consist of several subsections, hence the document object which is passed to the child component is an array of objects. And the document is generated dynamically based on this array the following way (excerpt from the SectionContainer class which is responsible to display the document):
buildSectionContainer() {
return this.props.listOfSections.map((section, index) =>
{
if (section.type === 'editor') return (
<QuillEditor
key={index}
id={section.id}
modules={modules}
defaultValue={section.content}
placeholder={section.placeholder}
/>
);
else if (section.type === 'text') return (
<div key={index}>{section.value}</div>
);
}
);
}
render() {
return (
<div>
{this.buildSectionContainer()}
</div>
);
}
The SectionContainer gets the array of objects and generate the document from it according to the type of these sections. The problem is that these sections are not updated when a different doc is selected in the parent component. I see change only when a bigger length array is passed to the child component. Like the firstly selected doc had an array of 2 elements, and then the newly selected doc had 3 elements array of sections and this third section is added to the previously existing 2, but the first 2 sections remained as they were.
And that’s why i though it’s better to unmount the child component and create a new one.
Surely it can happen that i miss something fundamental here again. Maybe related to how react handles lists. I just dont know what.
Edit2:
Ok, figured out that there is a problem with how i use the QuillEditor component. I just dont know what. :) The document updates, only the content of QuillEditors doesnt.
The reason your current solution doesn't actually do anything is because React's state updates are batched, such that, when setState is called a bunch of times in one go, React "combines" the result of all of them. It's not as much of a problem with being "not fast enough" as it is React performing only the work that is necessary.
// this...
this.setState({ message: 'hello', secret: 123 })
this.setState({ message: 'world' })
// ...becomes this
this.setState({ message: 'world', secret: 123 })
This behavior doesn't really have much to do with the problem at hand, though. As long as your UI is a direct translation of state -> view, the UI should simply update in accordance to the state.
class Example extends React.Component {
state = {
documentList: [], // assuming this comes from the server
document: null,
}
// consider making this function accept a document object instead,
// then you could leave out the .find(...) call
handleDocumentSelection = documentName => {
const document = this.state.documentList.find(doc => doc.name === documentName)
this.setState({ document })
}
render() {
const { document } = this.state
return (
<div>
<DocumentList
documents={this.state.documentList}
onDocumentSelection={this.handleDocumentSelection}
/>
{/*
consider having this component accept the entire document
to make it a little cleaner
*/}
{document && <DocumentViewer document={document.content.sections} />}
</div>
)
}
}
Initially componentWillMount() will run a fetch() to an API endpoint & then save the javascript object to the redux store.
Now my problem is that when it comes to rendering the next set of components it re-renders all of them (meaning there's a little flash on the screen because of the rendering).
So essentially onScroll past a certain point it will run the same fetch() api call & grab a new list of javascript objects. It then grabs the data from the redux store & loops through it appending each new postComponent to the layout state.
handleScroll (event: Object) {
const offset = event.nativeEvent.contentOffset.y;
const screenHeight = Dimensions.get('window').height;
// Just for dev purposes until I find a proper way of determining half way down screen
if(offset >= screenHeight/2){
console.log("Halfway past...");
this.props.FeedActions.fetchFeed(this.props.feed.nextUrl, true);
}
}
render() {
var feed = this.props.feed;
if (!_.has(feed, 'posts')) {
return <ActivityIndicatorIOS />;
}
// Append more posts to state
for (var i = 0; i < _.size(feed.posts); i++) {
this.state.postComponents.push(
<PostComponent post={ feed.posts[i] } key={ "post_"+feed.posts[i].postId+Math.random() }/>
);
}
return (
<ScrollView key={Math.random()} onScroll={this.handleScroll.bind(this)}>
{ this.state.postComponents }
</ScrollView>
)
}
};
Is there a way around this? I thought react wouldn't re-render components that are already render, only the ones that are changed? But I guess in this case my components are all dynamic so that means they will be re-rendered.
The problem is in how you're creating your keys. What you want is a key that uniquely identifies that particular node, consistently, and doesn't change every render. Since you use Math.random() as part of your key, it changes the key every render, so react rebuilds that node. Try using postId without the random number trailing it.
Let's say I have a list of 1000 items. And I rendering it with React, like this:
class Parent extends React.Component {
render() {
// this.state.list is a list of 1000 items
return <List list={this.state.list} />;
}
}
class List extends React.Component {
render() {
// here we're looping through this.props.list and creating 1000 new Items
var list = this.props.list.map(item => {
return <Item key={item.key} item={item} />;
});
return <div>{list}</div>;
}
}
class Item extends React.Component {
shouldComponentUpdate() {
// here I comparing old state/props with new
}
render() {
// some rendering here...
}
}
With a relatively long list map() takes about 10-20ms and I can notice a small lag in the interface.
Can I prevent recreation of 1000 React objects every time when I only need to update one?
You can do it by using any state management library, so that your Parent doesn't keep track of this.state.list => your List only re-renders when new Item is added. And the individual Item will re-render when they are updated.
Lets say you use redux.
Your code will become something like this:
// Parent.js
class Parent extends React.Component {
render() {
return <List />;
}
}
// List.js
class List extends React.Component {
render() {
var list = this.props.list.map(item => {
return <Item key={item.key} uniqueKey={item.key} />;
});
return <div>{list}</div>;
}
}
const mapStateToProps = (state) => ({
list: getList(state)
});
export default connect(mapStateToProps)(List);
// Item.js
class Item extends React.Component {
shouldComponentUpdate() {
}
render() {
}
}
const mapStateToProps = (state, ownProps) => ({
item: getItemByKey(ownProps.uniqueKey)
});
export default connect(mapStateToProps)(Item);
Of course, you have to implement the reducer and the two selectors getList and getItemByKey.
With this, you List re-render will be trigger if new elements added, or if you change item.key (which you shouldn't)
EDIT:
My inital suggestions only addressed possible efficiency improvements to rendered
lists and did not address the question about limiting the re-rendering
of components as a result of the list changing.
See #xiaofan2406's answer for a clean solution to the original question.
Libraries that help make rendering long lists more efficient and easy:
React Infinite
React-Virtualized
When you change your data, react default operation is to render all children components, and creat virtual dom to judge which component is need to be rerender.
So, if we can let react know there is only one component need to be rerender. It can save times.
You can use shouldComponentsUpdate in your list component.
If in this function return false, react will not create vitual dom to judge.
I assume your data like this [{name: 'name_1'}, {name: 'name_2'}]
class Item extends React.Component {
// you need judge if props and state have been changed, if not
// execute return false;
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.name === this.props.name) return false;
return true;
}
render() {
return (
<li>{this.props.name}</li>
)
}
}
As react just render what have been changed component. So if you just change one item's data, others will not do render.
There are a few things you can do:
When you build, make sure you are setting NODE_ENV to production. e.g. NODE_ENV=production npm run build or similar. ReactJS performs a lot of safety checks when NODE_ENV is not set to production, such as PropType checks. Switching these off should give you a >2x performance improvement for React rendering, and is vital for your production build (though leave it off during development - those safety checks help prevent bugs!). You may find this is good enough for the number of items you need to support.
If the elements are in a scrollable panel, and you can only see a few of them, you can set things up only to render the visible subset of elements. This is easiest when the items have fixed height. The basic approach is to add firstRendered/lastRendered props to your List state (that's first inclusive and last exclusive of course). In List.render, render a filler blank div (or tr if applicable) of the correct height (firstRendered * itemHeight), then your rendered range of items [firstRendered, lastRendered), then another filler div with the remaining height ((totalItems - lastRendered) * itemHeight). Make sure you give your fillers and items fixed keys. You then just need to handle onScroll on the scrollable div, and work out what the correct range to render is (generally you want to render a decent overlap off the top and bottom, also you want to only trigger a setState to change the range when you get near to the edge of it). A crazier alternative is to render and implement your own scrollbar (which is what Facebook's own FixedDataTable does I think - https://facebook.github.io/fixed-data-table/). There are lots of examples of this general approach here https://react.rocks/tag/InfiniteScroll
Use a sideways loading approach using a state management library. For larger apps this is essential anyway. Rather than passing the Items' state down from the top, have each Item retrieve its own state, either from 'global' state (as in classical Flux), or via React context (as in modern Flux implementations, MobX, etc.). That way, when an item changes, only that item needs to re-render.
One way to avoid looping through the component list every render would be to do it outside of render function and save it to a variable.
class Item extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.item != nextProps.item;
}
render() {
return <li>{this.props.item}</li>;
}
}
class List extends React.Component {
constructor(props) {
super(props);
this.items = [];
this.update = this.update.bind(this);
}
componentWillMount() {
this.props.items.forEach((item, index) => { this.items[index] = <Item key={index} item={item} /> });
}
update(index) {
this.items[index] = <Item key={index} item={'item changed'} />
this.forceUpdate();
}
render() {
return <div>
<button onClick={() => { this.update(199); }}>Update</button>
<ul>{this.items}</ul>
</div>
}
}
There is a way to do this, but I don't think the React team would call it Kosher so to speak. Basically, instead of the parent having state, it has lists of refs. Your components have their own state. Your components are created once in the parent and stored in a parent's ref property which holds an array of all those components. So the components are never recreated on each rerender, and instead are persisted. You also would need a list of refs that attach to a function in each component to allow the parent to call individual components (in hooks you can use imperativehandle to do this).
Now, when the user does something that would cause the data to change for a specific component in that list, you would find that component's ref in the list of attached functions and call the function on it. The component could then update and rerender itself based off that function call from its parent, without other components being affected/recreated/rerendered.
I believe this is called imperative programming rather than declarative, and React doesn't like it. I personally have used this technique in my own projects for similar reasons to you, and it worked for me.