Updating a React list without re-rendering said list - javascript

I'm trying to figure out how to render out a set of divs, without re-rendering the entire list as a new set is added.
So I've got a stateful component. Inside said stateful component, I've got a function that A, gets a list of post id's, and B, makes a request to each of those post id's and pushes the results to an array. Like so:
getArticles = () => {
axios.get(`${api}/topstories.json`)
.then(items => {
let articles = items.data;
let init = articles.slice(0,50);
init.forEach(item => {
axios.get(`${post}/${item}.json`)
.then(article => {
this.setState({ articles: [...this.state.articles, article.data]});
});
})
});
}
Then, I've got a second function that takes this information and outputs it to a list of posts. Like so:
mapArticles = () => {
let articles = this.state.articles.map((item, i) => {
let time = moment.unix(item.time).fromNow();
return(
<section className="article" key={i}>
<Link className="article--link" to={`/posts/${item.id}`}/>
<div className="article--score">
<FontAwesomeIcon icon="angle-up"/>
<p>{item.score}</p>
<FontAwesomeIcon icon="angle-down"/>
</div>
<div className="article--content">
<div className="article--title">
<h1>{item.title}</h1>
</div>
<div className="article--meta">
{item.by} posted {time}. {item.descendants ? `${item.descendants} comments.` : null}
</div>
</div>
<div className="article--external">
<a href={item.link} target="_blank">
<FontAwesomeIcon icon="external-link-alt"/>
</a>
</div>
</section>
)
});
return articles;
}
I then use {this.mapArticles()} inside the render function to return the appropriate information.
However, whenever the app loads in a new piece of data, it re-renders the entire list, causing a ton of jank. I.e., when the first request finishes, it renders the first div. When the second request finishes, it re-renders the first div and renders the second. When the third request finishes, it re-renders the first and second, and renders the third.
Is there a way to have React recognize that the div with that key already exists, and should be ignored when the state changes and the function runs again?

A technique that I use to only render the part that are new is to keep a cache map of already drawn obj, so in the render method I only render the new incoming elements.
Here is an example:
Take a look at https://codesandbox.io/s/wq2vq09pr7
In this code you can see that the List has an cache array and the render method
only draw new arrays
class RealTimeList extends React.Component {
constructor(props) {
super(props);
this.cache = [];
}
renderRow(message, key) {
return <div key={key}>Mesage:{key}</div>;
}
renderMessages = () => {
//let newMessages=this,props.newMessage
let newElement = this.renderRow(this.props.message, this.cache.length);
this.cache.push(newElement);
return [...this.cache];
};
render() {
return (
<div>
<div> Smart List</div>
<div className="listcontainer">{this.renderMessages()}</div>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = { message: "hi" };
}
start = () => {
if (this.interval) return;
this.interval = setInterval(this.generateMessage, 200);
};
stop = () => {
clearTimeout(this.interval);
this.interval = null;
};
generateMessage = () => {
var d = new Date();
var n = d.getMilliseconds();
this.setState({ title: n });
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={this.start}> Start</button>
<button onClick={this.stop}> Stop</button>
<RealTimeList message={this.state.message} />
</div>
);
}
}

If items arrive at the same time, wait till all items are fetched, then render:
getArticles = () => {
axios.get(`${api}/topstories.json`)
.then(items => {
let articles = items.data;
let init = articles.slice(0, 50);
Promise.all(init.map(item => axios.get(`${post}/${item}.json`)).then(articles => {
this.setState({
articles
});
})
});
}
If you really want to render immediately after an item is fetched, you can introduce a utility component that renders when promise resolves.
class RenderOnResolve extends React.Component {
state = null
componentDidMount() {
this.props.promise.then(data => this.setState(data))
}
render() {
return this.state && this.props.render(this.state);
}
}
// usage:
<RenderOnResolve promise={promise} render={this.articleRenderer}/>

Related

ReactJs - return a sequence of divs

I have a React class that takes a JSON object array and outputs a set of divs representing keys and values. The thing is, each object in the json has around 60 key value pairs inside; in this example i am rendering the divs for the 19th index for each of the objects:
import React, { Component } from "react";
import "./Maps.css";
import df3 from "./data/df3.json"
import sample from "./data/sample.json"
class Maps extends Component {
constructor() {
super();
const data = df3;
this.state = data
}
renderDiv = () => {
var df4 = df3["Devotions"];
return df4.map(v => {
return Object.keys(v).map((host) => {
return (
<div class={host}>
{host} {v[host][19]}
<div class='space' style={{ borderRadius:'19px',
transform:`scale(${v[host][19]},${v[host][19]})`,
opacity:'9%'}} >
</div>
</div>
);
});
});
};
render() {
return <div id="Maps">{this.renderDiv()}</div>;
}
}
export default Maps
what I would like to do is control the rendering, so that the divs for each index appears sequentially on the screen.
return Object.keys(v).map((host) => {
return (
<div class={host}>
{host} {v[host][19]}
<div class='space' style={{ borderRadius:'19px',
transform:`scale(${v[host][19]},${v[host][19]})`,
opacity:'9%'}} >
</div>
</div>
Im not sure if if should just wrap all of sets of divs id like to return in a single div, and just have them connected to a single keyframe, but im not sure if theres a more elegant way to do it.
As always, help is appreciated!
I think this is what you need :
Considering, you have multiple objects, inside each object, there are some data in array, and you want to display all of them in sequence.
renderDiv = () => {
var df4 = df3["Devotions"];
let updatedArray = [];
df4.forEach(v => { //<--- no need of map
Object.keys(v).forEach((hosts) => { //<--- no need of map
updatedArray = [...updatedArray ,
...v[hosts].map((host) => {
return (
<div className={host}>
{host} {host}
<div className='space' style={{ borderRadius:'19px',
transform:`scale(${host},${host})`,
opacity:'9%'}} >
</div>
</div> )
})
]
})
})
return updatedArray;
}

Loop over all instances of component, log each state

I'm building out a simple drum machine application using ReactJS and could use some help understanding how to loop through all instances of a component while outputting each instance's state.
The application UI shows 16 columns of buttons, each containing 4 unique drum rows. There is a "SixteenthNote.js" component which is essentially on column containing each "Drum.js" instance. In the "DrumMachine.js" module, I am outputting "SixteenthNote.js" 16 times to display one full measure of music. When you click on a drum button, that drum's value is pushed into the SixteenthNote' state array. This is all working as intended.
The last part of this is to create a "Play.js" component which, when clicked, will loop through all of the SixteenthNote instances and output each instance's state.
Here is the "DrumMachine.js" module
class DrumMachine extends Component {
constructor(props) {
super(props);
this.buildKit = this.buildColumns.bind(this);
this.buildLabels = this.buildLabels.bind(this);
this.buildAudio = this.buildAudio.bind(this);
this.state = {
placeArray: Array(16).fill(),
drumOptions: [
{type: 'crash', file: crash, title: 'Crash'},
{type: 'kick', file: kick, title: 'Kick'},
{type: 'snare', file: snare, title: 'Snare'},
{type: 'snare-2', file: snare2, title: 'Snare'}
]
}
}
buildLabels() {
const labelList = this.state.drumOptions.map((sound, index) => {
return <SoundLabel title={sound.title} className="drum__label" key={index} />
})
return labelList;
}
buildColumns() {
const buttonList = this.state.placeArray.map((object, index) => {
return <SixteenthNote columnClassName="drum__column" key={index} drumOptions={this.state.drumOptions}/>
});
return buttonList;
}
buildAudio() {
const audioList = this.state.drumOptions.map((audio, index) => {
return <Audio source={audio.file} drum={audio.type} key={index}/>
})
return audioList;
}
render() {
return (
<div>
<div className={this.props.className}>
<div className="label-wrapper">
{this.buildLabels()}
</div>
<div className="drum-wrapper">
{this.buildColumns()}
</div>
</div>
<div className="audio-wrapper">
{this.buildAudio()}
</div>
</div>
)
}
}
Here is "SixteenthNote.js" module
class SixteenthNote extends Component {
constructor(props) {
super(props);
this.buildColumn= this.buildColumn.bind(this);
this.buildDrumOptions = this.buildDrumOptions.bind(this);
this.updateActiveDrumsArray = this.updateActiveDrumsArray.bind(this);
this.state = {
activeDrums: []
}
}
buildDrumOptions() {
return this.props.drumOptions;
}
updateActiveDrumsArray(type) {
let array = this.state.activeDrums;
array.push(type);
this.setState({activeDrums: array});
}
buildColumn() {
const placeArray = this.buildDrumOptions().map((button, index) => {
return <Drum buttonClassName="drum__button" audioClassName="drum__audio" type={button.type} file={button.file} key={index} onClick={() => this.updateActiveDrumsArray(button.type)}/>
})
return placeArray;
}
render() {
return (
<div className={this.props.columnClassName}>
{this.buildColumn()}
</div>
)
}
}
Here is the "Drum.js" module
class Drum extends Component {
constructor(props) {
super(props);
this.clickFunction = this.clickFunction.bind(this);
this.state = {
clicked: false
}
}
drumHit(e) {
document.querySelector(`.audio[data-drum=${this.props.type}]`).play();
this.setState({clicked:true});
}
clickFunction(e) {
this.state.clicked === false ? this.drumHit(e) : this.setState({clicked:false})
}
render() {
const drumType = this.props.type;
const drumFile = this.props.file;
const buttonClasses = `${this.props.buttonClassName} drum-clicked--${this.state.clicked}`
return (
<div onClick={this.props.onClick}>
<button className={buttonClasses} data-type={drumType} onClick={this.clickFunction}></button>
</div>
)
}
}
You will need to contain the information about the activeDrums in your DrumMachine component.
That means:
In your DrumMachine component you create the state activeDrums like you have in your SixteenthNote.js. You will need to put your updateActiveDrumsArray function to your drumMachine component as well.
Then you pass this function to your SixteenthNote component like:
<SixteenthNote columnClassName="drum__column" key={index} drumOptions={this.state.drumOptions} onDrumsClick={this.updateActiveDrumsArray} />
After doing so, you can access that function via props. So, in your SixteenthNote component it should look like:
<Drum buttonClassName="drum__button" audioClassName="drum__audio" type={button.type} file={button.file} key={index} onClick={() => this.props.onDrumsClick(button.type)}/>
(Don't forget to get rid of the unneccessary code.)
With this, you have your activeDrums state in DrumMachine containing all the active drums. This state you can then send to your play component and do the play action there.

Unable to render html divs with React?

I'm trying to generate several divs based off an array - but I'm unable to. I click a button, which is supposed to return the divs via mapping but it's returning anything.
class History extends Component {
constructor(props) {
super(props);
this.state = {
info: ""
};
this.generateDivs = this.generateDivs.bind(this);
}
async getCurrentHistory(address) {
const info = await axios.get(`https://api3.tzscan.io/v2/bakings_history/${address}?number=10000`);
return info.data[2];
}
async getHistory() {
const info = await getCurrentHistory(
"tz1hAYfexyzPGG6RhZZMpDvAHifubsbb6kgn"
);
this.setState({ info });
}
generateDivs() {
const arr = this.state.info;
const listItems = arr.map((cycles) =>
<div class="box-1">
Cycle: {cycles.cycle}
Count: {cycles.count.count_all}
Rewards: {cycles.reward}
</div>
);
return (
<div class="flex-container">
{ listItems }
</div>
)
}
componentWillMount() {
this.getHistory();
}
render() {
return (
<div>
<button onClick={this.generateDivs}>make divs</button>
</div>
);
}
You are not actually rendering the the divs just by invoking the generateDivs function, the JSX it is returning is not being used anywhere.
To get it to work you could do something like -
render() {
return (
<div>
<button onClick={this.showDivs}>make divs</button>
{this.state.isDisplayed && this.generateDivs()}
</div>
);
}
where showDivs would be a function to toggle the state property isDisplayed to true
The main point is that the JSX being returned in the generateDivs function will now be rendered out in the render function. There is many ways to toggle the display, that is just one straight forward way

ReactJs - Add unique Component every time during onClick

I am in the process of learning React and Redux. Currently I am working on a project where I need to append a component on button click.
New Component should be added down the previous component
Previously added component contains the data added and it should not be refreshed while adding a new component.
I tried to search but all the solutions are recommending to use a List and incrementing the count on every click.
This is my requirement diagram:
Update:
I have added my code which I tried in the below JS Fiddle.
While appending the new component, the data modified in the existing component should be retained.
https://jsfiddle.net/np7u6L1w/
constructor(props) {
super(props)
this.state = { addComp: [] }
}
addComp() { // Onclick function for 'Add Component' Button
//this.setState({ addComp: !this.state.addComp })
this.setState({
addComp: [...this.state.addComp, <Stencil />]
});
}
render() {
return (
<div>
<div class="contentLeft"><h2>Workflows:</h2>
<Stencil />
{this.state.addComp.map((data, index) => {
{ data }
})}
</div>
<div class="contentRight" >
<button name="button" onClick={this.addComp.bind(this)} title="Append new component on to the end of the list">Add Component</button>
</div>
<div class="clear"></div>
</div>
)
}
Code is Updated:
You can do something like that
// New state
this.state = {
appendedCompsCount: 0
}
// Outside render()
handleClick = () => {
this.setState({
appendedCompsCount: this.state.appendedCompsCount + 1
})
}
getAppendedComponents = () => {
let appendedComponents = [];
for (let i = 0; i < this.state.appendedCompsCount; i++) {
appendedComponents.push(
<AppendedComponents key={i} />
)
}
return appendedComponents;
}
// In render()
<button onClick={this.handleClick}>Click here</button>
{
this.getAppendedComponents()
}
maybe when added new child, you want animation to work.
this is the best method react-transition-group
example: https://reactcommunity.org/react-transition-group/transition-group

ReactJS: how to append a component to the App component in React

I am trying to append a "Item" component which consists of some array items, in the main "App" Component. But the component is getting replaced with the new array items instead of getting appended. Following is the code snippet:
//the App render function
render: function() {
return (
<div>
{
this.state.productDisplayed.map(function(product, i) {
return (
<Item source = {product.url} prodId = {product.id} key = {product.id} />
)
})
}
</div>
)
}
//The Item render function
render: function(){
return(
<div className = "col-sm-4" >
<img src = {this.props.source} width = "70%" className = "img-responsive"></img>
<div>{this.props.prodId}
</div>
</div>
)
}
"ProductDisplayed" is an array which gets replaced by new items which are then displayed using a "for" loop.
How can i append the items as if I am adding some extra items to the main App component. I am trying to implement infinite scrolling.
In order to append items to your app component, you need to append data to your productDisplayed state array
For this you can do something like
addItem=(item)=>{
var productDisplayed=[...this.state.productDisplayed];
productDisplayed.push(item);
this.setState({productDisplayed});
}
And you can call this function addItem on some event.
You need to append them to the array in your state. I have used the function argument to setState because your nextState is dependent on your previous state.
component ProductList extends React.Component {
constructor() {
super()
this.setState({
productDisplayed: []
});
}
getMoreItems = ( startingId ) => {
Api.getMoreItems(startingId).then(this.addItems);
}
addItems = ( items ) => {
this.setState(( prevState ) => ({
updatedItems: [...prevState.productDisplayed, ...items]
}));
}
render() {
// no changes
// something triggers this.getMoreItems(id)
}
}

Categories