setState start to work recursively in React.js - javascript

I start to work with react.js and create a component Box, state = {pageNumber: 1, dataForTable:''}. I insert two components in it - Pagination and Table. When click to Pagination it give number of page to Box. Box state change then it render and then Pagination render too. Then I set ajax to server, get new data for table and then Box render for second time to render Tables.
In which function should I put ajax logic? When i put it in componentDidUpdate setState start to work recursively.
In future will be more components in <Box/ > which will change <Tables />.

From what I understand, this is your setup:
var React = require('react');
var ComponentBox = React.createClass({
render: function() {
return (
<div>
<Table />
<Pagination />
</div>
);
}
});
module.exports = ComponentBox;
Your table component should never handle data, you should "feed" data to your component. So you should have a prop receiving data in your table component.
Pagination component should pass an event to ComponentBox telling ComponentBox to get the data - so ajax should happen in your ComponentBox (read more on flux if you wish)
Here's the suggested solution for you
var React = require('react');
var ComponentBox = React.createClass({
handlePageChange: function(startIndex, size) {
// do your ajax here
// set your data to state which causes re-render
},
render: function() {
return (
<div>
<Table data={this.state.tableData} />
<Pagination onPageChange={this.handlePageChange} />
</div>
);
}
});
module.exports = ComponentBox;
inside your pagination component, remember to pass the information out through props onPageChange =)
Hope this is clear enough for you.

Related

Cannot update a component (`App`) while rendering a different component

There are a bunch of similar questions on so, but I can't see one that matches my conundrum.
I have a react component (a radial knob control - kinda like a slider).
I want to achieve two outcomes:
Twiddle the knob and pass the knob value up to the parent for further actions.
Receive a target knob value from the parent and update the knob accordingly.
All without going into an endless loop!
I have pulled my hair out - but have a working solution that seems to violate react principles.
I have knob.js as a react component that wraps around the third party knob component and I have app.js as the parent.
In knob.js, we have:
export default class MyKnob extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
size: props.size || 100,
radius: (props.value/2).toString(),
fontSize: (props.size * .2)
}
if (props.value){
console.log("setting value prop", props.value)
this.state.value = props.value
} else {
this.state.value = 25 // any old default value
}
}
To handle updates from the parent (app.js) I have this in knob.js:
// this is required to allow changes at the parent to update the knob
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
this.setState({value: this.props.value})
}
console.log("updating knob from parent", value)
}
and then to pass changes in knob value back to the parent, I have:
handleOnChange = (e)=>{
//this.setState({value: e}) <--used to be required until line below inserted.
this.props.handleChangePan(e)
}
This also works but triggers a warning:
Cannot update a component (App) while rendering a different component (Knob)
render(){
return (
<Styles font-size={this.state.fontSize}>
<Knob size={this.state.size}
angleOffset={220}
angleRange={280}
steps={10}
min={0}
max={100}
value={this.state.value}
ref={this.ref}
onChange={value => this.handleOnChange(value)}
>
...
Now over to app.js:
function App() {
const [panLevel, setPanLevel] = useState(50);
// called by the child knob component. works -- but creates the warning
function handleChangePan(e){
setPanLevel(e)
}
// helper function for testing
function changePan(e){
if (panLevel + 10>100){
setPanLevel(0)
} else {
setPanLevel(panLevel+10)
}
}
return (
<div className="App">
....
<div className='mixer'>
<div key={1} className='vStrip'>
<Knob size={150} value={panLevel} handleChangePan = {(e) => handleChangePan(e)}/>
</div>
<button onClick={(e) => changePan(e)}>CLICK ME TO INCREMENT BY 10</button>
...
</div>
So - it works -- but I am violating react principles -- I haven't found another way to keep the external "knob value" and the internal "knob value" in sync.
Just to mess with my head further, if I remove the bubbling to parent in 'handleOnChange' - which presumably then triggers a change in prop-->state cascading back down - I not only have a lack of sync with the parent -- but I also need to reinstate the setState below, in order to get the knob to work via twiddling (mouse etc.._)! This creates another warning:
Update during an existing state transition...
So stuck. Advice requested and gratefully received. Apols for the long post.
handleOnChange = (e)=>{
//this.setState({value: e})
**this.props.handleChangePan(e)**
}
It has been suggested on another post, that one should wrap the setState into a useEffect - but I can't figure out how to do that - let alone whether it's the right approach.
The error message will be displayed if parent (App) states are set while rendering children (Knob).
In your case, while App is rendering, Knob'sonChange() is triggered when loaded, which then calls this.handleOnChange() and then this.props.handleChangePan() having App'ssetPanLevel().
To fix using useEffect():
In knob.js, you can store panLevel as state first just like in App, instead of direct calling this.props.handleChangePan() to call App'ssetPanLevel().
Then, use useEffect(_=>props.handleChangePan(panLevel),[panLevel]) to call App'ssetPanLevel() via useEffect().
Your knob.js will look like this:
function Knob(props){
let [panLevel, setPanLevel] = useState(50);
useEffect(_=>{
props.handleChangePan(panLevel);
}, [panLevel]);
return *** Knob that does not call props.handleChangePan(), but call setPanLevel() instead ***;
}
setState() called inside useEffect() will be effective after the render is done.
In short, you cannot call parent'ssetState() outside useEffect() while in first rendering, or the error message will come up.

How to redirect to another component without using a button

I'm using a library to render a Gantt Chart. This Chart provides an js event to handle a button click. I need to route or render another react component in that event. So... how can i render o route to my component in that event? I'm using react router and meteor.
render() {
gantt.config.buttons_left=["dhx_save_btn","dhx_cancel_btn","dhx_delete_btn"];
gantt.config.buttons_right = ["go_task_btn"];
gantt.locale.labels["go_task_btn"] = 'VER';
gantt.attachEvent("onLightboxButton", function(button_id, node, e){
if(button_id == "go_task_btn"){
var idTask = gantt.getState().lightbox;
//HERE I WAN TO REDIRECT OR RENDER ANOTHER
// REACT COMPONENTE
}
});
return (
<div>
<Toolbar
zoom={this.state.currentZoom}
onZoomChange={this.handleZoomChange}
/>
<div className="gantt-container">
<Gantt
tasks={data}
zoom={this.state.currentZoom}
onTaskUpdated={this.logTaskUpdate}
onLinkUpdated={this.logLinkUpdate}
/>
</div>
{/*<MessageArea messages={this.state.messages}/> */}
</div>
); }}export default App;
You can do this by making a function outside render, put all your code their and call this function in the return.

React.js: How do you change the position of a Component item

How do you change the position of a Component item in React?
Unless I've misunderstood it, React orders list items by key, which is represented in the DOM by data-reactid, but I don't know how to modify the key of components on the page.
i.e. How do you grab the component, change it's key, and then fire a render so that the reordered list renders in the order you've set?
e.g. in the following code example, when the Click me link is clicked, the first list item would be swapped with the last list item.
Ideally, this functionality would allow you to dynamically reorder/relocate any component on the page without changing the order of components in the render method.
Here is a link to the repo where the full project is located: https://github.com/bengrunfeld/gae-react-flux-todos
var TodoBox = React.createClass({
render: function(){
return (
<div className="todo-container">
<h4>GAE React Flux Todos</h4>
<TodoList data={this.state.data} />
</div>
)
}
});
var TodoList = React.createClass({
changePosition: function(e){
// Change position of list item (e.g. to top/certain position/end of list)
},
render:function(){
var todoNodes = this.props.data.map(function(todo) {
return (
<Todo key={todo.id} id={todo.id}>
{todo.todoText}
</Todo>
);
});
return (
<form className="todoList">
{todoNodes}
<a onClick={this.changePosition}>Click me</a>
</form>
)
}
});
var Todo = React.createClass({
render:function(){
return (
<div className="todoItem">
<input type="text" className={this.props.id} onChange={this.checkInput} defaultValue={this.props.children} ref="todoItem"/>
</div>
)
}
});
The key prop is not used to order the element, but to reconciliate it between different render calls. Elements with the same key will not be re-rendered but rather diffed against each other in order to update the DOM optimally. See Reconciliation
If you want to reorder elements, you need to change their position in your JSX or in the element array you pass as children in your render method (todoNodes).
In your case, you could make a copy of this.props.data in the TodoList component state, then update that copy in your changePosition method with something like this.setState({data: reorderedData}). A good place to make that copy would be in getInitialState.
The render method of your TodoList would then be called again, and you would map over your newly reordered this.state.data to create an array of Todo elements ordered to your liking.
However, be aware that props in getInitialState is an anti-pattern. Since your data lives in the state of your TodoBox component, a way to avoid this would be to have your TodoList component call this.props.onReorder(reorderedData) in its changePosition method. Your TodoBox component could then pass an event handler to its TodoList child, and update its state with the new data whenever this handler is called.
var TodoBox = React.createClass({
handleReorder: function(reorderedData) {
this.setState({data: reorderedData});
},
render: function(){
return (
<div className="todo-container">
<h4>GAE React Flux Todos</h4>
<TodoList data={this.state.data} onReorder={this.handleReorder} />
</div>
)
}
});
var TodoList = React.createClass({
changePosition: function(e){
// Change position of list item (e.g. to top/certain position/end of list)
// Create a copy of this.props.data and reorder it, then call
// this.props.onReorder to signal to the parent component that
// the data has been reordered
this.props.onReorder(reorderedData);
},
render:function() {
var todoNodes = this.props.data.map(function(todo) {
return (
<Todo key={todo.id} id={todo.id}>
{todo.todoText}
</Todo>
);
});
return (
<form className="todoList">
{todoNodes}
<a onClick={this.changePosition}>Click me</a>
</form>
)
}
});
Keys are used for something else, not for sorting. React uses keys to optimize its internal Virtual DOM operations. It means you tell React that "no matter the order of these siblings, the individual sibling is still identified by this key". That's how React knows whether it should prepend, insert, delete, or append new siblings by reusing the old, without throwing stuff away unnecessarily.
As for your sorting question: To change the order of the siblings, just sort the JavaScript array this.props.data.

React selectively not passing new props to rendered object

I'm working on a multi-stage form that gets some intermediate data via AJAX based on this guide. I'm having an odd issue where React isn't passing new props to a component.
// MyForm.js.jsx
var MyForm = React.createClass({
render: function() {
switch(this.state.stage) {
case 1:
return <InitialFields
nextStage={this.nextStage}
save={this.save}
/>
case 2:
return <ChoiceFields
title="Choose first thing:"
field="first_id"
options={this.state.firstChoices}
nextStage={this.nextStage}
save={this.save}
/>
case 3:
return <ChoiceFields
title="Choose second thing:"
field="second_id"
options={this.state.secondChoices}
nextStage={this.nextStage}
save={this.save}
/>
}
}
// etc ...
});
ChoiceFields.js.jsx:
var ChoiceFields = React.createClass({
render: function() {
console.log(this.state);
var options = this.setOptions();
return (
<div className="choiceFields">
<h1>{this.props.title}</h1>
<SearchBar onChange={this.onSearch} />
<div className="btn-group">{options}</div>
<NextButton next={this.saveAndContinue} text="Set Default Values" />
</div>
);
},
setOptions: function() {
var buttons = this.state.options;
return buttons.map(function(choice) {
return (
<ChoiceButton key={choice.id} title={choice.name}
description={choice.description} id={choice.id}
makeSelection={this.selectButton} selected={choice.selected}
/>
);
}.bind(this));
}
});
When the state advances from 1 to 2, it renders the ChoiceFields without issue. When the state advances from 2 to 3, it renders the new title, but the options prop remains unchanged despite giving it a different object.
Is there some way to force React to update the prop, or otherwise rerender the ChoiceFields object?
--UPDATE--
I was copying this.props.options into this.state.options, and using state to keep track of whether or not an option was selected. Per #superfell's recommendation, I kept the object array in props and calculated which one was selected in the render method. This fixed it the issue.
Based on the comments, you are copying the props to state in your ChoiceFields component in getInitialState. getInitialState doesn't get called again when the props are updated, and so you're left looking at stale state. You can add a componentWillReceiveProps function to ChoiceFields that can update state from the new props. Or you can refactor to not copy props to state at all, as that is a specific anti-pattern called out by React.
Another option available to you is to give your ChoiceField variants different keys, so React will know they're different instances and they'll each get the full component lifecycle when you swap between them on subsequent renders:
case 2:
return <ChoiceFields
key="first"
title="Choose first thing:"
field="first_id"
options={this.state.firstChoices}
nextStage={this.nextStage}
save={this.save}
/>
case 3:
return <ChoiceFields
key="second"
title="Choose second thing:"
field="second_id"
options={this.state.secondChoices}
nextStage={this.nextStage}
save={this.save}
/>
React.js and Dynamic Children - Why the Keys are Important has a good explaination of what's happening and links to the relevant docs.

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