Not sure what I'm doing wrong but my component wrapped in setTimeout is not being rendered to the DOM:
const ContentMain = Component({
getInitialState() {
return {rendered: false};
},
componentDidMount() {
this.setState({rendered: true});
},
render(){
var company = this.props.company;
return (
<div id="ft-content">
{this.state.rendered && setTimeout(() => <Content company={company}/>,3000)}
</div>
)
}
})
I'd bet this isn't working because the render method needs all of its input to be consumed at the same time and it can't render other components in retrospect, there's a certain flow to React. I'd suggest to separate the timeout from render method anyway for logic's sake, and do it in componentDidMount like this:
const ContentMain = Component({
getInitialState() {
return {rendered: false};
},
componentDidMount() {
setTimeout(() => {
this.setState({rendered: true});
}, 3000);
},
render(){
if (!this.state.rendered) {
return null;
}
var company = this.props.company;
return (
<div id="ft-content">
<Content company={company}/>
</div>
)
}
})
Changing the state triggers the render method.
On a side note - even if your original approach worked, you'd see the component flicker for 3 seconds every time it got rendered after the initial load. Guessing you wouldn't want that :)
Related
Suppose you had a React App component with a child component that was slow to mount (in my case a 3rd party document editor). The App has to re-render whenever the document changes, and the two child components (the header (which simply displays title, id), the document editor (slow to initially render when initializing a document)) should be able to re-render and repaint the dom as soon as they finish updating. However, the document editor, because it is a slow to initialize, prevents the header from repainting until it is complete. Below is an illustration, where the FastChild will not be repainted until the SlowChild returns.
export default function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<FastChild count={count} />
<button onClick={() => setCount(count+1)}>increment</button>
<SlowChild count={count}/>
</div>
);
}
function FastChild({ count }) {
return (
<h1>{count}</h1>
)
}
function SlowChild({ count }) {
return veryLongList.map(()=> (
<div>
{`count ${count}`}
</div>
))
}
Unfortunately, I cannot make changes the document editor (which would be the SlowChild component in this example).
One solution is to keep a copy of the prop in state, and use shouldComponentUpdate to delay the slow component from rendering using setImmediate
class DelayedSlowChild extends React.Component {
constructor(props) {
super(props)
this.state = {
localCount: props.count,
}
}
shouldComponentUpdate = (nextProps, nextState) => {
if (nextProps.count !== this.props.count) {
setImmediate(() => this.setState({ localCount: nextProps.count }))
return false;
} else if (this.state.localCount !== nextState.localCount) {
return true;
}
return false
}
render() {
veryLongList.map(i => (
<div>
{`${i} ${this.props.count}`}
</div>
))
}
}
I am making a simple React site which fetches some data from an API (this API) and then display it on the page. The endpoint I am using is the skyblock/auctions endpoint. What this returns is a list of objects, which I want to get the first one of and then pass it to a child component. The parent can successfully get the data, however when I pass it to the child and console.log it, it returns null. The only reason I can think of for why its doing this is because the parent component hasn't finished fetching the data yet, but I am not sure how to make it render only after its finished.
Here is the code for the parent component AuctionViewer:
class AuctionViewer extends Component {
state = { data: null}
loadData = async () => {
let url = "https://api.hypixel.net/skyblock/auctions?key=INSERT_KET_HERE"
let response = await fetch(url);
let json = await response.json();
this.setState({data: json.auctions[0]}, function () {
console.log(this.state.data)
});
}
componentDidMount() {
this.loadData();
setInterval(this.loadData, 60 * 1000);
}
render() {
return (<Auction data={this.state.data} />);
}
}
And here is the child component Auction:
class Auction extends Component {
state = {
loading: true,
item: null,
price: null,
startPrice: null,
time: null,
};
loadData() {
let data = this.props.data;
console.log(data);
let end = new Date(data.end - Date.now());
let timeLeft = end.getHours() + ":" + end.getMinutes() + ":" + end.getSeconds();
this.setState({loading: false, item: data.item_name, price: data.highest_bid_amount, startPrice: data.starting_bid, time: timeLeft, timestamp: end});
};
componentDidMount() {
this.loadData();
}
render() {
return (
<div className="gridBox">
{this.state.loading ? (
<p>Loading...</p>
) : (
<div>
<p>Item: {this.state.item}</p>
<p>Top Bid: {this.state.price}</p>
<p>Start Bid: {this.state.startPrice}</p>
<p>Time Left: {this.state.time}</p>
</div>
)}
<button onClick={this.loadData}>Refresh</button>
</div>
);
}
}
The only reason I can think of for why its doing this is because the parent component hasn't finished fetching the data yet...
That's right.
...but I am not sure how to make it render only after its finished.
You have two options:
(I don't think you want this one.) Move the ajax call into the parent of the parent component, and only render the parent component when you have the data.
Have the parent component render a "loading" state of some kind until the data is available
#2 looks something like this:
render() {
const { data } = this.state;
return (data ? <Auction data={data} /> : <em>Loading...</em>);
}
...but probably with something more attractive than just a <em>Loading...</em>. :-)
The simplest was is with some conditional rendering while AuctionViewer is "fetching" the data. The initial state is null and this is passed to Auction, but you can conditionally render other UI or null while waiting.
render() {
const { data } = this.state;
return data ? (
<Auction data={data} />
) : <div>Loading...</div>;
}
Or return null to indicate to react that nothing should be rendered.
render() {
const { data } = this.state;
return data ? (
<Auction data={data} />
) : null;
}
I have this array, that I want to iterate. I need to delay it by a couple of seconds before the next.
{this.props.things.map((thing, index) => {
return (
<div key={index}>{thing.content}</div>
// Delay 1 second here
)
})}
The initial state of this array is always more than one. For UI purposes I want them to load in one by one in to the DOM.
The render function of react is synchronous. Also javascript map is synchronous. So using timers is not the right solution here.
You can however, in your component state, keep track of items that have been rendered and update that state using javascript timers:
For an example implementation check out this fiddle:
React.createClass({
getInitialState() {
return {
renderedThings: [],
itemsRendered: 0
}
},
render() {
// Render only the items in the renderedThings array
return (
<div>{
this.state.renderedThings.map((thing, index) => (
<div key={index}>{thing.content}</div>
))
}</div>
)
},
componentDidMount() {
this.scheduleNextUpdate()
},
scheduleNextUpdate() {
this.timer = setTimeout(this.updateRenderedThings, 1000)
},
updateRenderedThings() {
const itemsRendered = this.state.itemsRendered
const updatedState = {
renderedThings: this.state.renderedThings.concat(this.props.things[this.state.itemsRendered]),
itemsRendered: itemsRendered+1
}
this.setState(updatedState)
if (updatedState.itemsRendered < this.props.things.length) {
this.scheduleNextUpdate()
}
},
componentWillUnmount() {
clearTimeout(this.timer)
}
})
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}/>
I have this array, that I want to iterate. I need to delay it by a couple of seconds before the next.
{this.props.things.map((thing, index) => {
return (
<div key={index}>{thing.content}</div>
// Delay 1 second here
)
})}
The initial state of this array is always more than one. For UI purposes I want them to load in one by one in to the DOM.
The render function of react is synchronous. Also javascript map is synchronous. So using timers is not the right solution here.
You can however, in your component state, keep track of items that have been rendered and update that state using javascript timers:
For an example implementation check out this fiddle:
React.createClass({
getInitialState() {
return {
renderedThings: [],
itemsRendered: 0
}
},
render() {
// Render only the items in the renderedThings array
return (
<div>{
this.state.renderedThings.map((thing, index) => (
<div key={index}>{thing.content}</div>
))
}</div>
)
},
componentDidMount() {
this.scheduleNextUpdate()
},
scheduleNextUpdate() {
this.timer = setTimeout(this.updateRenderedThings, 1000)
},
updateRenderedThings() {
const itemsRendered = this.state.itemsRendered
const updatedState = {
renderedThings: this.state.renderedThings.concat(this.props.things[this.state.itemsRendered]),
itemsRendered: itemsRendered+1
}
this.setState(updatedState)
if (updatedState.itemsRendered < this.props.things.length) {
this.scheduleNextUpdate()
}
},
componentWillUnmount() {
clearTimeout(this.timer)
}
})