componentDidUpdate() doesn't seem to get called after actual re-rendering occurs - javascript

The basic code flow is like this.
item.js
export default class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
modalIsOpen: false,
logs: []
};
this._fetchLogs = this._fetchLogs.bind(this);
this._fetchAnotherLogs = this._fetchAnotherLogs.bind(this);
}
render() {
let tableBody;
if (this.state.logs.length > 0) {
let body = [];
this.state.logs.forEach((log) => {
body.push(
<tr key={`log${log.id}`}>
<td>{log.operator_name} / {log.operator_code}</td>
</tr>
);
});
tableBody = (
<table className="logs">
<thead>
<tr>
<th className="operator">{i18next.t('operator')}</th>
</tr>
</thead>
<tbody>{body}</tbody>
</table>
);
}
return (
<div>
<ul className="tabs">
<li onClick={this._fetchLogs}>Logs</li>
<li onClick={this._fetchAnotherLogs}>Another Logs</li>
</ul>
<div className="modalContent">
{tableBody}
</div>
</div>
);
}
componentDidUpdate() {
console.log('didUpdate', $('.ReactModal__Content').height(), $(window).height());
}
_fetchLogs() {
$.ajax({
context: this,
url: URL.DEVICE_LOGS(this.props.id),
success(resp) {
const logs = resp.data;
this.setState({logs});
},
error(resp) {
console.log(resp);
},
});
}
_fetchAnotherLogs() {
$.ajax({
context: this,
url: URL.DEVICE_LOGS_ANOTHER(this.props.id),
success(resp) {
const logs = resp.data;
this.setState({logs});
},
error(resp) {
console.log(resp);
},
});
}
};
So when the <li> element is clicked, it calls _fetchLogs(), which sets the logs state to some array from the server, and inside render(), it sets the tableBody variable and fills <div className="modalContent">.
What I want to achieve is that by using jQuery after the state changes and the component re-renders the table, measure the height of the table and changes some styles accordingly.
But what componentDidUpdate() logs out is the height before all the children(say, the <table>) are re-rendered.
For example, if I say the default height (without <table> content) is 170px, the after-height (with <table> content of logs) is 340px, and the after-height (with <table> content of another logs) is 500px, when I first click <li> it says the height is 170px although the result height is 340px actually. And if I click another <li> to fetch another logs, now it says the height is 340px, which is also wrong because it should be 500px.
So I'm thinking that componentDidUpdate() is called only after the state changes, not actual re-rendering (due to the state change) finishes. Is there something that I'm doing wrong or is there another options should I take into account?
Edit 1
Following the Matthew Herbst's answer, which I thought is reasonable enough to be the right answer, I've made couple of changes but it didn't really change.
<div className="modalContent" ref={c => this._modalContent = c}>
{tableBody}
</div>
And
componentDidUpdate() {
console.log('didUpdate', this._modalContent.offsetHeight);
}
(I also tried to wrap it with jQuery but it didn't change...)
I tried to log out the sequence of rendering and componentDidUpdate() by adding console.log() right before the render()'s return (though it cannot be achieved this way, I know), it clearly shows that the array of objects log comes before the didUpdate log.

So, what's happening here is that you're not actually accessing the component in componentDidMount, you're accessing the DOM, which may not have been updated yet even though your component has rendered (remember, React has to render all the components that may have updated and then does a single DOM diff).
You are correct about using componentDidUpdate, but you are just accessing the wrong thing. What you needs are refs.
In your render, add a ref:
return (
<div>
<ul className="tabs">
<li onClick={this._fetchLogs}>Logs</li>
<li onClick={this._fetchAnotherLogs}>Another Logs</li>
</ul>
<div
className="modalContent"
ref={(myTableRef) => this.myTableRef = myTableRef}
>
{tableBody}
</div>
</div>
);
Then in your componentDidUpdate, you can do:
componentDidUpdate() {
console.log('didUpdate', $(this.myTableRef).height(), $(window).height());
}
That should give you the correct height for the table model.

Related

REACT connect 4 game - attempt to reset grid over setState but results in "undefined"

I am trying to create a Connect of 4 game in React as an exercise.
If i want to reset the grid or for displaying player points, a reset of my grid is required rather than simply reloading the entire page.
In this case, dealing with my grid via state is a logical step, but after several attempts and variations, I'm unfortunately lost at the moment
In this variation below, this.state.grid always returns undefined on reset (console.log right after render method begins).
I see that the problem is most likely because in the gridHtml function I am already passing the grid to the state via setState.
If I call this.gridHTML() directly on the reset button, my grid completely disappears.
I am very grateful for any help at this point
import React from 'react';
class Grid extends React.Component {
constructor(props) {
super(props);
this.state = {
player: "red",
isGameOver: false,
gamestarts: false
};
this.findLastEmptyColl = this.findLastEmptyColl.bind(this);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onClick = this.onClick.bind(this);
this.checkForWinner = this.checkForWinner.bind(this);
this.gridHtml = this.gridHtml.bind(this);
this.reset = this.reset.bind(this);
};
/*left out MouseEnter, leave, click and win logic , those work fine and to keep it short */
gridHtml() {
let rows = Array(6).fill(0), cols = Array(7).fill(0);
let grid = rows.map((el, i) => {
return (
<div key={i} className="row">
{cols.map((value, index) => {
return (
<div key={index}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onClick={this.onClick}
className="col empty"
data-col={index}
data-row={i}>
</div>
);
})}
</div>
);
});
this.setState({
grid: grid
});
}
componentDidMount() {
this.gridHtml();
}
componentWillMount() {
this.gridHtml();
}
reset() {
this.setState({
grid: this.gridHtml(),
isGameOver: false,
gamestarts: false
})
}
render() {
console.log(this.state.grid);
return (
<>
{!this.state.gamestarts && <h4>Connect 4 - Player {this.state.player} begins!</h4>}
{this.state.gamestarts && <h4>Player {this.state.player} </h4>}
{(this.state.isGameOver && !this.state.gamestarts) && <h4>Player {this.state.player} has won</h4>}
<div id="board">
{this.state.grid}
</div>
<div>
<button style={{margin: "30px"}} onClick={() => {this.reset()}}>Reset</button>
</div>
</>
)
}
}
export default Grid;
Update:
I see that my understanding of React doesn't seem to be properly adjusted yet; in my reset() function, due to the asynchronicity of react, I assume that the dynamic assignment via setState of my grid should actually render automatically?
Again, the problem: when I currently press my reset button, the grid is re-created but the moves, red and yellow, are still on the grid as they were; last I thought of writing a function that instead of creating a new grid removes all CSS classes and data properties related to it - but that would make the whole point of doing something like this with React absurd.
To make it even clearer:
if I extend my reset() function with a setTimeout around setState, right after overwriting my grid, it works?! I can understand why but this right now feels like a hack and I don't want to leave it like this, because this is supposed to be the core competence of React? Hope it helps to understand better
reset () {
this.setState({grid: 'some text ... loading '});
setTimeout(() =>{
this.setState({
grid: this.gridHtml(),
isGameOver: false,
gamestarts: true,
player: "red"
});
}, 1000);
}
Hope somebody can explain?
Many thanks
Your gridHtml() function doesn't return anything so grid is being set to undefined. Try adding a return grid; statement to the end.

Toggle prop passed on one of many targets in ReactJS

Just starting off with ReactJS and have a project where I am showing an accordion of issues and including a details area that is hidden on the start.
There is a button in the accordion bar that should pass a prop to the child element to hide or show them. I have refs on the button and on the details child compoment and added a function to call the function and pass the ref of the details area. I am just not sure how to dynamically change the class hidden on one of many areas and not all of them.
Not sure if putting a class on each element and then learning how to toggle the particular child's class is better or changing the prop to the child.
I can get to the change function but am drawing a blank from there and all the googling shows how to do one element with a grand change of state but I need individual elements.
Here is what I have so far.
Parent
...
<AccordionItem key={item.id} className={iconClass} title={`${item.area}`} expanded={item === 1}>
{
item.issues.map(issue => {
let trim = (issue.issue.length>21) ? `${issue.issue.substring(0,22)}...`: issue.issue;
return (
<div className="issue-bar container-fluid">
<div className="row issue-bar-row">
<span className="issue-title"><img src={CriticalRed} alt="Critical"/> {trim}</span>
<span className="btns">
<button className="btn btn-details" onClick={() => this.showDetail(`details-${issue.id}`)}>Details</button>
</span>
</div>
<IssuesDetails ref={`details-${issue.id}`} issue={issue} shouldHide={true} />
</div>
)
})
}
<div>
</div>
</AccordionItem>
...
Child
export default class IssuesDetails extends Component{
render(){
let issueDetails = classNames( 'issue-details', { hidden: this.props.shouldHide } )
return(
<div className={issueDetails}>
<div className="issues-details-title">
<h3>{this.props.issue.issue}</h3>
</div>
<div className="issues-details-details">
{this.props.issue.details}
</div>
<div className="issues-details-gallery">
<ImageGallery source={this.props.issue.photos} showPlayButton={false} useBrowserFullscreen={false} />
</div>
<button className="btn btn-success">Resolve</button>
</div>
)
}
}
Thanks for any help you provide or places you can send me!
If i'm understanding correctly, you need to be able to swap out shouldHide={true} in certain circumstances. To do this, you'll want your parent component to have a state object which indicates whether they should be hidden or not.
Exactly what this state object looks like depends on what sort of data you're working with. If the issues is a single array, then perhaps the state could be an array of booleans indicating whether each issue is expanded or not. I suspect you may have a more nested data structure, but i can't tell exactly since some of the code was omitted.
So assuming you have an array, it might look like this (i've omitted some things from the render method for brevity):
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
hidden: (new Array(props.issues.length)).fill(false),
};
}
showDetail(index) {
let newHidden = this.state.hidden.slice();
newHidden[index] = true;
this.setState({
hidden: newHidden
});
}
render() {
return (
<AccordionItem>
{this.props.issues.map((issue, index) => {
<div>
<button onClick={() => this.showDetail(index))}/>
<IssuesDetails issue={issue} shouldHide={this.state.hidden[index]}/>
</div>
})}
</AccordionItem>
);
}
}
Take a look at these:
https://codepen.io/JanickFischr/pen/xWEZOG
style={{display: this.props.display}}
I think it will help with your problem. If you need more information, please just ask.

Only display item hovered on react after map

I am really surprised this isn't working as I anticipated.
I want to show a username when the the person visiting the site hovers over the users's message. Below is the code I have so far. The problem is that when I hover over the message every username in the array that I have mapped over appears. How can I change this so that it is only one username?
I guess I need to change the state to each user's message in a separate component, however, I wouldn't have thought this should be neccessary.
class App extends Component {
state = {
showUsername: false
};
showUsername = () => {
this.setState({
showUsername: true
});
}
hideUsername = () => {
this.setState({
showUsername: false
});
}
render() {
let messageArr = this.props.messages.map(msg => {
let name = `${msg.firstName} ${msg.lastName}`;
return(
<div key={msg.id}>
{this.state.showUsername && (
<span>{name}</span>
)}
<li><span onMouseEnter={this.showUsername} onMouseLeave{this.hideUsername}>{msg.message}</span></li>
</div>
);
});
return (
<ul>
{messageArr}
</ul>
);
}
}
I guess I need to change the state to each user's message in a separate component, however, I wouldn't have thought this should be necessary.
yes.Its necessary.
As of now,there is one common state showUsername is used to show user's name which is why you getting this effect.
showUsername = (oneUserMessage) => {
oneUserMessage.showUsername = true; // updating via reference
}
hideUsername = (oneUserMessage) => {
oneUserMessage.showUsername = false;// updating via reference
}
//return in render
return (
<div key={msg.id}>
{msg.showUsername && (
<span>{name}</span>
)}
<li><span
onMouseEnter={()=>{this.showUsername(msg)}}
onMouseLeave={()=>{this.hideUsername(msg)}}>
{msg.message}</span></li>
</div>
);
Your components are behaving like this, because this.state.showUsername is a state inside your component.
So, whichever div is hovered, it will set the state to true, and all of your others will receive this.state.showUsername as true either.
This makes all of them visible.
What you should do is, write a new component having the div and the logic inside of it. So when you map it down, you each of these divs will have its own states.
Another thing, you can do the map like this:
const messageArr = this.props.messages.map((msg) => <div key={msg.id}>
<li>
<span onMouseEnter={this.showUsername} onMouseLeave{this.hideUsername}>{msg.message}</span>
</li>
</div>);
So no need for the return statement :)

React CSSTransitionGroup does not work even with key being set

I have been trying since yesterday to make an animation to my image carousel. As far as I understand, you wrap the content to be animated with the CSSTransitionGroup and make sure it stays in the dom and also specify a unique key to each child of the transition group. I believe I have followed all this yet I see no transition.
One thing worth to mention, While I was trying to get this working I suspected if something could be wrong with the key, so I tried setting the key with a random string. The key would change every-time the state changes, and for some unknown reason I could see the animation. Can someone explain this to me.
I am not sure where I am going wrong, whether the version of transition group or in setting the key to children, No clue !
Below is the code replicating my problem.
var CSSTransitionGroup = React.addons.CSSTransitionGroup
class Images extends React.Component {
constructor(props){
super(props);
this.state = {
showComponent: false,
}
}
componentWillReceiveProps(nextProps){
if (this.props.name === nextProps.showComponentName){
this.setState({
showComponent: true,
})
} else {
this.setState({
showComponent: false,
})
}
}
render() {
if (this.state.showComponent){
return (
<img src={this.props.url} />
)
} else {
return null;
}
}
}
class TransitionExample extends React.Component {
constructor (props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state= {
showComponentName: null,
}
}
onClick(button) {
this.setState({
showComponentName: button.currentTarget.textContent,
})
}
render() {
var imageData = [
"http://lorempixel.com/output/technics-q-c-640-480-9.jpg",
"http://lorempixel.com/output/food-q-c-640-480-8.jpg",
"http://lorempixel.com/output/city-q-c-640-480-9.jpg",
"http://lorempixel.com/output/animals-q-c-640-480-3.jpg"
];
var images = [];
for (var i in imageData) {
i = parseInt(i, 10);
images.push(
<Images url={imageData[i]} showComponentName={this.state.showComponentName} name={imageData[i]} key={imageData[i]} />
);
}
return (
<div>
<div>
<button onClick={this.onClick}>{imageData[0]}</button>
<button onClick={this.onClick}>{imageData[1]}</button>
<button onClick={this.onClick}>{imageData[2]}</button>
<button onClick={this.onClick}>{imageData[3]}</button>
</div>
<div className="transitions">
<CSSTransitionGroup
transitionName="viewphoto"
transitionEnterTimeout={2000}
transitionLeaveTimeout={2000}
transitionAppearTimeout={2000}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}>
{images}
</CSSTransitionGroup>
</div>
</div>
);
}
}
ReactDOM.render(
<TransitionExample />,
document.getElementById('container')
);
I am also providing the link to the example on jsfiddle
The problem with your code is that images is always an array of elements that don't mount/unmount. The correct approach for this is to change the child. For example, if you substitute the return of the render method of your fiddle with this:
return (
<div>
<div>
<button onClick={this.onClick}>{imageData[0]}</button>
<button onClick={this.onClick}>{imageData[1]}</button>
<button onClick={this.onClick}>{imageData[2]}</button>
<button onClick={this.onClick}>{imageData[3]}</button>
</div>
<div className="transitions">
<CSSTransitionGroup
transitionName="viewphoto"
transitionEnterTimeout={2000}
transitionLeaveTimeout={2000}
transitionAppearTimeout={2000}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}>
<img src={this.state.showComponentName} key={this.state.showComponentName}/>
</CSSTransitionGroup>
</div>
</div>
);
The animation works! Using a simple img instead of your Images component and giving it the image url (this only works when you have clicked a button, showComponentName should be initialized to show the first image). You could also use a custom component of course, but the point here is that the children elements of CSSTransitionGroup must be changed if you want the animation to trigger because otherwise you are always rendering the same four Images components no matter whether they return the img or not. You might want to check out react-css-transition-replace since it usually works better when it comes to replacing.

React "after render" code?

I have an app where I need to set the height of an element (lets say "app-content") dynamically. It takes the height of the "chrome" of the app and subtracts it and then sets the height of the "app-content" to fit 100% within those constraints. This is super simple with vanilla JS, jQuery, or Backbone views, but I'm struggling to figure out what the right process would be for doing this in React?
Below is an example component. I want to be able to set app-content's height to be 100% of the window minus the size of the ActionBar and BalanceBar, but how do I know when everything is rendered and where would I put the calculation stuff in this React Class?
/** #jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass({
render: function () {
return (
<div className="wrapper">
<Sidebar />
<div className="inner-wrapper">
<ActionBar title="Title Here" />
<BalanceBar balance={balance} />
<div className="app-content">
<List items={items} />
</div>
</div>
</div>
);
}
});
module.exports = AppBase;
componentDidMount()
This method is called once after your component is rendered. So your code would look like so.
var AppBase = React.createClass({
componentDidMount: function() {
var $this = $(ReactDOM.findDOMNode(this));
// set el height and width etc.
},
render: function () {
return (
<div className="wrapper">
<Sidebar />
<div className="inner-wrapper">
<ActionBar title="Title Here" />
<BalanceBar balance={balance} />
<div className="app-content">
<List items={items} />
</div>
</div>
</div>
);
}
});
One drawback of using componentDidUpdate, or componentDidMount is that they are actually executed before the dom elements are done being drawn, but after they've been passed from React to the browser's DOM.
Say for example if you needed set node.scrollHeight to the rendered node.scrollTop, then React's DOM elements may not be enough. You need to wait until the elements are done being painted to get their height.
Solution:
Use requestAnimationFrame to ensure that your code is run after the painting of your newly rendered object
scrollElement: function() {
// Store a 'this' ref, and
var _this = this;
// wait for a paint before running scrollHeight dependent code.
window.requestAnimationFrame(function() {
var node = _this.getDOMNode();
if (node !== undefined) {
node.scrollTop = node.scrollHeight;
}
});
},
componentDidMount: function() {
this.scrollElement();
},
// and or
componentDidUpdate: function() {
this.scrollElement();
},
// and or
render: function() {
this.scrollElement()
return [...]
In my experience window.requestAnimationFrame wasn't enough to ensure that the DOM had been fully rendered / reflow-complete from componentDidMount. I have code running that accesses the DOM immediately after a componentDidMount call and using solely window.requestAnimationFrame would result in the element being present in the DOM; however, updates to the element's dimensions aren't reflected yet since a reflow hasn't yet occurred.
The only truly reliable way for this to work was to wrap my method in a setTimeout and a window.requestAnimationFrame to ensure React's current call stack gets cleared before registering for the next frame's render.
function onNextFrame(callback) {
setTimeout(function () {
requestAnimationFrame(callback)
})
}
If I had to speculate on why this is occurring / necessary I could see React batching DOM updates and not actually applying the changes to the DOM until after the current stack is complete.
Ultimately, if you're using DOM measurements in the code you're firing after the React callbacks you'll probably want to use this method.
Just to update a bit this question with the new Hook methods, you can simply use the useEffect hook:
import React, { useEffect } from 'react'
export default function App(props) {
useEffect(() => {
// your post layout code (or 'effect') here.
...
},
// array of variables that can trigger an update if they change. Pass an
// an empty array if you just want to run it once after component mounted.
[])
}
Also if you want to run before the layout paint use the useLayoutEffect hook:
import React, { useLayoutEffect } from 'react'
export default function App(props) {
useLayoutEffect(() => {
// your pre layout code (or 'effect') here.
...
}, [])
}
You can change the state and then do your calculations in the setState callback. According to the React documentation, this is "guaranteed to fire after the update has been applied".
This should be done in componentDidMount or somewhere else in the code (like on a resize event handler) rather than in the constructor.
This is a good alternative to window.requestAnimationFrame and it does not have the issues some users have mentioned here (needing to combine it with setTimeout or call it multiple times). For example:
class AppBase extends React.Component {
state = {
showInProcess: false,
size: null
};
componentDidMount() {
this.setState({ showInProcess: true }, () => {
this.setState({
showInProcess: false,
size: this.calculateSize()
});
});
}
render() {
const appStyle = this.state.showInProcess ? { visibility: 'hidden' } : null;
return (
<div className="wrapper">
...
<div className="app-content" style={appStyle}>
<List items={items} />
</div>
...
</div>
);
}
}
I feel that this solution is dirty, but here we go:
componentDidMount() {
this.componentDidUpdate()
}
componentDidUpdate() {
// A whole lotta functions here, fired after every render.
}
Now I am just going to sit here and wait for the down votes.
React has few lifecycle methods which help in these situations, the lists including but not limited to getInitialState, getDefaultProps, componentWillMount, componentDidMount etc.
In your case and the cases which needs to interact with the DOM elements, you need to wait till the dom is ready, so use componentDidMount as below:
/** #jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass({
componentDidMount: function() {
ReactDOM.findDOMNode(this).height = /* whatever HEIGHT */;
},
render: function () {
return (
<div className="wrapper">
<Sidebar />
<div className="inner-wrapper">
<ActionBar title="Title Here" />
<BalanceBar balance={balance} />
<div className="app-content">
<List items={items} />
</div>
</div>
</div>
);
}
});
module.exports = AppBase;
Also for more information about lifecycle in react you can have look the below link:
https://facebook.github.io/react/docs/state-and-lifecycle.html
I ran into the same problem.
In most scenarios using the hack-ish setTimeout(() => { }, 0) in componentDidMount() worked.
But not in a special case; and I didn't want to use the ReachDOM findDOMNode since the documentation says:
Note: findDOMNode is an escape hatch used to access the underlying DOM
node. In most cases, use of this escape hatch is discouraged because
it pierces the component abstraction.
(Source: findDOMNode)
So in that particular component I had to use the componentDidUpdate() event, so my code ended up being like this:
componentDidMount() {
// feel this a little hacky? check this: http://stackoverflow.com/questions/26556436/react-after-render-code
setTimeout(() => {
window.addEventListener("resize", this.updateDimensions.bind(this));
this.updateDimensions();
}, 0);
}
And then:
componentDidUpdate() {
this.updateDimensions();
}
Finally, in my case, I had to remove the listener created in componentDidMount:
componentWillUnmount() {
window.removeEventListener("resize", this.updateDimensions.bind(this));
}
There is actually a lot simpler and cleaner version than using request animationframe or timeouts. Iam suprised no one brought it up:
the vanilla-js onload handler.
If you can, use component did mount, if not, simply bind a function on the onload hanlder of the jsx component. If you want the function to run every render, also execute it before returning you results in the render function. the code would look like this:
runAfterRender = () =>
{
const myElem = document.getElementById("myElem")
if(myElem)
{
//do important stuff
}
}
render()
{
this.runAfterRender()
return (
<div
onLoad = {this.runAfterRender}
>
//more stuff
</div>
)
}
}
I'm actually having a trouble with similar behaviour, I render a video element in a Component with it's id attribute so when RenderDOM.render() ends it loads a plugin that needs the id to find the placeholder and it fails to find it.
The setTimeout with 0ms inside the componentDidMount() fixed it :)
componentDidMount() {
if (this.props.onDidMount instanceof Function) {
setTimeout(() => {
this.props.onDidMount();
}, 0);
}
}
After render, you can specify the height like below and can specify the height to corresponding react components.
render: function () {
var style1 = {height: '100px'};
var style2 = { height: '100px'};
//window. height actually will get the height of the window.
var hght = $(window).height();
var style3 = {hght - (style1 + style2)} ;
return (
<div className="wrapper">
<Sidebar />
<div className="inner-wrapper">
<ActionBar style={style1} title="Title Here" />
<BalanceBar style={style2} balance={balance} />
<div className="app-content" style={style3}>
<List items={items} />
</div>
</div>
</div>
);`
}
or you can specify the height of the each react component using sass. Specify first 2 react component main div's with fixed width and then the third component main div's height with auto. So based on the third div's content the height will be assigned.
For me, no combination of window.requestAnimationFrame or setTimeout produced consistent results. Sometimes it worked, but not always—or sometimes it would be too late.
I fixed it by looping window.requestAnimationFrame as many times as necessary.
(Typically 0 or 2-3 times)
The key is diff > 0: here we can ensure exactly when the page updates.
// Ensure new image was loaded before scrolling
if (oldH > 0 && images.length > prevState.images.length) {
(function scroll() {
const newH = ref.scrollHeight;
const diff = newH - oldH;
if (diff > 0) {
const newPos = top + diff;
window.scrollTo(0, newPos);
} else {
window.requestAnimationFrame(scroll);
}
}());
}
For me, componentDidUpdate alone or window.requestAnimationFrame alone didn't solve the problem, but the following code worked.
// Worked but not succinct
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.refreshFlag) { // in the setState for which you want to do post-rendering stuffs, set this refreshFlag to true at the same time, to enable this block of code.
window.requestAnimationFrame(() => {
this.setState({
refreshFlag: false // Set the refreshFlag back to false so this only runs once.
});
something = this.scatterChart.current.canvas
.toDataURL("image/png"); // Do something that need to be done after rendering is finished. In my case I retrieved the canvas image.
});
}
}
And later I tested with requestAnimationFrame commented, it still worked perfectly:
// The best solution I found
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.refreshFlag) { // in the setState for which you want to do post-rendering stuffs, set this refreshFlag to true at the same time, to enable this block of code.
// window.requestAnimationFrame(() => {
this.setState({
refreshFlag: false // Set the refreshFlag back to false so this only runs once.
});
something = this.scatterChart.current.canvas
.toDataURL("image/png"); // Do something that need to be done after rendering is finished. In my case I retrieved the canvas image.
// });
}
}
I'm not sure whether it's just a coincidence that the extra setState induced a time delay, so that when retrieving the image, the drawing is already done (I will get the old canvas image if I remove the setState).
Or more possibly, it was because setState is required to be executed after everything is rendered, so it forced the waiting for the rendering to finish.
-- I tend to believe the latter, because in my experience, calling setState consecutively in my code will result in each one triggered only after the last rendering finished.
Lastly, I tested the following code. If this.setState({}); doesn't update the component, but wait till the rendering finishes, this would be the ultimate best solution, I thought. However, it failed. Even when passing an empty {}, setState() still updates the component.
// This one failed!
componentDidUpdate(prevProps, prevState, snapshot) {
// if (this.state.refreshFlag) {
// window.requestAnimationFrame(() => {
this.setState({});
something = this.scatterChart.current.canvas
.toDataURL("image/png");
// });
// }
}
I recommend that you make use of hooks.
They are available from version 16.8.0 onwards.
You can check the behavior of this hook in the official react documentation.
Something like this:
import React, { useEffect } from 'react'
const AppBase = ({ }) => {
useEffect(() => {
// set el height and width etc.
}, [])
return (
<div className="wrapper">
<Sidebar />
<div className="inner-wrapper">
<ActionBar title="Title Here" />
<BalanceBar balance={balance} />
<div className="app-content">
<List items={items} />
</div>
</div>
</div>
);
}
export default AppBase
I had weird situation when i need to print react component which receives big amount of data and paint in on canvas. I've tried all mentioned approaches, non of them worked reliably for me, with requestAnimationFrame inside setTimeout i get empty canvas in 20% of the time, so i did the following:
nRequest = n => range(0,n).reduce(
(acc,val) => () => requestAnimationFrame(acc), () => requestAnimationFrame(this.save)
);
Basically i made a chain of requestAnimationFrame's, not sure is this good idea or not but this works in 100% of the cases for me so far (i'm using 30 as a value for n variable).
I am not going to pretend I know why this particular function works, however window.getComputedStyle works 100% of the time for me whenever I need to access DOM elements with a Ref in a useEffect — I can only presume it will work with componentDidMount as well.
I put it at the top of the code in a useEffect and it appears as if it forces the effect to wait for the elements to be painted before it continues with the next line of code, but without any noticeable delay such as using a setTimeout or an async sleep function. Without this, the Ref element returns as undefined when I try to access it.
const ref = useRef(null);
useEffect(()=>{
window.getComputedStyle(ref.current);
// Next lines of code to get element and do something after getComputedStyle().
});
return(<div ref={ref}></div>);
for functional components you can react-use-call-onnext-render, its a custom hook that allows schedule callback on a later render.
It is used successfully on one of my other projects.
for requiring dimension of a dom element,
see this example,its the third example on react-use-call-onnext-render examples:
let's say we want to get dimensions of a removable DOM element,lets say div that is controlled by showBox state
variable. for that we can use getBoundingClientRect(). however, we want to call this function only after the element
mounted into the dom, so will schedule this call one render after the variable responsible for showing this element
in the dom has changed,and this variable is showBox, so he will be dependency of useCallOnNextRender:
const YourComponent = () => {
const [showBox, setShowBox] = useState(false)
const divRef = useRef()
const callOnNextShowBoxChange = useCallOnNextRender()
return (
<>
<div style={canvasStyle} id="canvas">
<button style={boxStyle} onClick={() => {
setShowBox(!showBox)
callOnNextShowBoxChange(() => console.log(divRef.current.getBoundingClientRect())) //right value
}}>toggle show box
</button>
<div style={{border: "black solid 1px"}} ref={divRef}>
{showBox ? <div style={boxStyle}>box2</div> : null}
</div>
</div>
</>
);
};
After trying all the suggested solutions above with no luck I found one of my elements in the middle had CSS transition, that's why I failed to get correct computed geometry after props changed.
So I had to use onTransitionEnd listener to wait for a moment when to try getting the computed by DOM height of container element.
Hope this will save someone's work day lol.
From the ReactDOM.render() documentation:
If the optional callback is provided, it will be executed after the
component is rendered or updated.
A little bit of update with ES6 classes instead of React.createClass
import React, { Component } from 'react';
class SomeComponent extends Component {
constructor(props) {
super(props);
// this code might be called when there is no element avaliable in `document` yet (eg. initial render)
}
componentDidMount() {
// this code will be always called when component is mounted in browser DOM ('after render')
}
render() {
return (
<div className="component">
Some Content
</div>
);
}
}
Also - check React component lifecycle methods:The Component Lifecycle
Every component have a lot of methods similar to componentDidMount eg.
componentWillUnmount() - component is about to be removed from browser DOM

Categories

Resources