React - Identify an element render (paint) completion - javascript

In angular, we can write a directive to identify whether any element is rendered or not.
In react, we can use *useEffect* for the component mount.
But in reality, the element is not available in the dom. How to identify whether the dom completes the render. I need to calculate the position and height of an element. But cannot identify the render completes. Always gets zero for the height.
In a component, I am looping an element.
For example,
array.map(item=> <div>Show data</div>)
In this above, I want to do some action when the show data has been displayed in the dom.
If we have 10k records of <div>Show data</div>, then I need to wait for those elements to render completely then need to do some actions for them. But only after completion.

Check out useLayoutEffect hook to access the DOM after rendering: https://beta.reactjs.org/reference/react/useLayoutEffect
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);

you can use useRef to get html element when it will be rendered in DOM, after first render you can get width/height (elementRef.current?.offsetHeight).

This is how I do it, using a ref:
const [divHeight, setDivHeight] = useState(0);
const handleRect = useCallback(node => {
if (node !== null) {
setDivHeight(Math.round(node.getBoundingClientRect().height));
}
}, []);
// ...
return (
<div ref={handleRect}>
.....
</div>
)

Related

Plotly - Dash #app.callback does not re-render Dash elements inside custom component unless re-render is triggered somehow

Inside usage.py I try to update graph-tooltip:
#app.callback(
Output("grid-value", "children"),
Input("live-graph", "hoverData"),
)
def display_hover(hoverData):
if hoverData is None:
return no_update
# demo only shows the first point, but other points may also be available
pt = hoverData["points"][0]
x = pt["x"]
y = pt["y"]
children = f"{x}:{y}"
return children
Both the live-graph and grid-value elements are inside custom Dash component (dash-react-flow, but with a new node type that can contain anything passed to it from Dash).
Now if I over a the live-graph, it gets updated only once I click, so it seems not to be re-rendering properly.
I implemented the node:
function AnyContentNode({ data }) {
return (
<div className="any-content-node" id={data.id} >
<div>
{
data.elements?.map((node, index) => {
return window.elements[data.elementId][node];
})
}
</div>
{
data.handles?.map((node, index) => {
return <Handle key={index} type={node.direction} position={node.position} id={node.id} />
})
}
</div>
);
}
where window.elements contains the node that is not being updated properly. I suspect that storing elements inside window.elements might be the cause of this, but I did not find any other way to pass those elements into the custom node without having to change the source code for ReactFlow.
So far it works for non-dynamic elements, but currently I got stuck when I have to update dynamic element. It gets occasionally updated, but that is not very good user-experience.
Is there a way to force re-render/or somehow fix this? I am quite new to React, so I don't have a good knowledge of the default hooks etc. I tried to see whether componentDidUpdate gets triggered and it does (so many times, every second).

Can i use the document object in React?

For an example if we made a App component and we needed to create an element each time a button was clicked:
function App() {
const handleClick = () => {
// Code here
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
</div>
)
}
Is it ok if i used document.createElement("div") and document.getElementById("app").append() in that case?
function App() {
const handleClick = () => {
let div = document.createElement("div")
div.innerHTML = "Hi!"
document.getElementById("app").append(div)
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
</div>
)
}
It's fine to use the document object for certain things in React code, but not in this case. That's not how you'd add an element to your #app div. Instead, you'd use state for it. When switching to an MVC/MVVM/whatever tool like React, you need to stop thinking in terms of modifying the DOM and start thinking in terms of component states.
In your case, for instance, you'd either want a boolean state member telling you whether to render that Hi! div, or perhaps an array state member of messages you might display.
Here's an example of the former:
const { useState } = React;
const App = () => {
// The state information
const [showHi, setShowHi] = useState(false);
const handleClick = () => {
// Set the state to true on button click
setShowHi(true);
};
return <div>
{/* Check the state and conditionally render */}
{showHi && <div>Hi!</div>}
<button onClick={handleClick}>Click here</button>
</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
I suggest working through the tutorial on the React website for a solid introduction to React.
Can you?
You can, but this goes against the idea of React in the first place and is advised against. Updating the DOM this way can cost you performance and even introduce bugs in your code.
The point of React is to handle updating the DOM in a performant way that is cross-browser compatible. In fact, behind the scenes, React is going to create the <div> element and place it in the DOM, but it is going to do so in a less costly and better-managed way by using a virtual representation of the DOM and everything in it. This is not as expensive as directly building, destroying and rebuilding elements on the page, because, for one, not everything on the page needs to be changed each time a user interaction that changes something of the page happens. React will react to parts of the page that have changed and keep parts that have not changed improving the performance on your web app. (See
Reactive Updates), This is one of the reasons the React library was built.
React keeps its own internal registry of elements it renders on the page, in the virtual DOM. It updates and tracks them all through their lifecycle on the page. Because of this, it knows which elements to replace, keep or tear down during user interaction. So creating an element by using the document object directly circumvents the creation (and registration) of a representation of the element in the virtual DOM making React unaware of the element - leaving the lifecycle handling of the element to the browser.
The React way
Because of the above, anything that has to do with the UI; including rendering, updating and destroying is best left to React.
The way to build (thanks to JSX and this is an improvement to #yanir's answer) your element is by simply writing out the element where you need it (or storing it in a variable first and using embedded JSX). The innerHTML attribute can be an embedded expression that is computed in the div element. Don't worry, this operation won't cost as much as using the document object to create elements directly. We'll just need a form of state to track how many times the user has clicked and create a plain JavaScript object then use the map() method to create as many <div>s as needed.
function App() {
const [items, setItems] = useState([]);
const handleClick = () => {
let obj = { value: "Hi!" };
setItems([...items, obj]);
};
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
{items.map((item, index) => {
return <div key={index}>{item.value}</div>;
})}
</div>
)
}
If you need to interact with the DOM directly, through the document object for example, as #Kaushik suggested, you can track the element through the use of hooks like useRef(), to get a reference to the object, useId() for unique stable Ids across server and client, useLayoutEffect() to hook into the browser after DOM updates but before it paints and so on. That being said, there are some APIs and attributes that you may need to access from the document object like events, title, URL, and several other attributes. In as much, as these do not mutate the UI, you can use these when needed, while the UI operations are left to React to handle.
Yes, you can. But the React way to do this is to keep track of this in state. You can keep count of the number of divs you want to render, and update that. State update will trigger a rerender, and your view will be updated.
Below I am using a state variable count and initializing an Array using the Array() constructor of size equal to count.
function App() {
const [count,setCount] = 0;
const handleClick = () => {
setCount(count => count+1);
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
{Array(count).map((x) => <div>{/**YOUR DIV CONTENT **/}<div>)}
</div>
)
}
If you need to access the DOM element, the recommended approach is to use the useRef, useImperativeHandle, useLayoutEffect, or useid hooks, as these allow React to be aware of when you are using the DOM elements, and allows for future React updates to not break your existing behavior. In your example though, I would argue that you do not need access to the DOM element, and that you should instead can let React handle the rendering of the element via declarative JSX.
You don't you document.getElementById in reactjs
All point of react is to use jsx (dynamic HTML)
What you can to it's to create an array that you append item to this array each click and use map to render the new item each time :
function App() {
const [items,setItems] = useState([])
const handleClick = () => {
// Code here
setItems(prev => {
let arr = []
//change obj each time to your need.
let obj = {label : "test" , value : "test"}
arr.push(obj)
setItems(arr)
})
}
return (
<div id="app">
<button onClick={handleClick}>Click here</button>
{items.map((item,index) =>{
return <p key={index}>{item.label}</p>
})
</div>
)
}

Use GraphQL data in gatsby-browser?

I have an app with some route ID's (basically a bunch of sections in a long SPA) that I have defined manually. I fetch these in gatsby-browser.js and use them in conjunction with shouldUpdateScroll, checking if the route ID exist, and in that case, scroll to the position of the route/section.
Example:
export const shouldUpdateScroll = ({ routerProps: { location } }) => {
const container = document.querySelector('.site')
const { pathname } = location
const projectRoutes = [`project1`, `project2`]
if (projectRoutes.indexOf(pathname) !== -1) {
const target = document.getElementById(pathname)
container.scrollTop = target.offsetTop;
}
return false
}
This works well for my usecase.
Now I want to add something similar for a page where the content is dynamically created (fetched from Sanity). From what I understand I cannot use GraphQL in gatsby-browser.js, so what is the best way to get the ID's from Sanity to gatsby-browser.js so I can use them to identify their scroll positions?
If there's some other better way to achieve the same result I'm open to that of course.
I think that you are over complexing the issue. You don't need the gatsby-browser.js to achieve it.
First of all, because you are accessing directly to the DOM objects (using document.getElementById) and you are creating precisely a virtual DOM with React to avoid pointing the real DOM. Attacking directly the real DOM (like jQuery does) has a huge performance impact in your applications and may cause some issues since in the SSR (Server-Side Rendering) the element may not be created yet.
You are hardcoding a logic part (the ids) on a file that is not intended to do so.
I think you can achieve exactly the same result using a simple function using a few hooks.
You can get the same information as document.getElementById using useRef hook and scrolling to that position once needed.
const YourComponent= (props) => {
const sectionOne = useRef(null);
const sectionTwo = useRef(null);
useEffect(()=>{
if(typeof window !== `undefined`){
console.log("sectionOne data ",sectionOne.current)
console.log("sectionTwo data ",sectionTwo.current)
if(sectionOne) window.scrollTo( 0, 1000 ); // insert logic and coordinates
}
}, [])
return (
<>
<section ref={sectionOne}>Section 1</section>
<section ref={sectionTwo}>Section 2</section>
</>
);
}
You can isolate that function into a separate file in order to receive some parameters and return some others to achieve what you want. Basically, the snippet above creates a reference for each section and, once the DOM tree is loaded (useEffect with empty deps, []) do some stuff based on your logic.
Your document.getElementById is replaced for sectionOne.current (note the .current), initially set as null to avoid unmounting or cache issues when re-hidration occurs.

Dynamically replace elements using jsx

I'm having an array data.info that is being updated over time and I'm trying to replace placeholder rendered elements with another. So by default app.js looks like this
return (
<Fragment>
{data.info.map((index) => {
return <Typography key={index} variant="h6" className={classes.title}>Demo</Typography>
})}
</Fragment>
)
Also I have a hook with async function to subscribed to data.info.length.
useEffect(
() => {
if (!initialRender.current) {
if (data.info.length!==0) {
for (let i = data.info.length-iScrollAmount+1 ; i < data.info.length+1; i++) {
firstAsync(i)
}
}
} else {
initialRender.current = false
}
},
[data.info.length]
)
async function firstAsync(id) {
let promise = new Promise(() => {
setTimeout(() => console.log(document.getElementById(id)), 500)
});
}
With document.getElementById() and id I can get to every element that was rendered and change it. And here goes the problems.
I'm using material-ui so I can't get to <Typography/> because it is transformed into <h6/>. Probably that is not a problem since I need to replace contents, so I can find parent element and remove all children. Is that way correct?
After I delete children how do I add content using jsx? What I mean is that in async function I'll get an array that I want to use in new element <NewCard/> to dynamically put into <Fragment/>. Yet I did not find any example how to do that.
It is not a good practice to change DOM Nodes directly in React, and you need to let React do the rendering for you and you just tell react what to do.
in your case you need to define a React State for your data and set your state inside your firstAsync function and then use your state to render whatever html element or React component which you want
React does not encourage the practice of manipulating the HTML DOM nodes directly.
Basically you need to see 2 things.
State which is a special variable whose value is retained on subsequent refresh. Change in reference in this variable will trigger component and its children a refresh/re-render.
Props which is passed to every Component and is read only. Changing in props causes refresh of component by default.
In your example, based on data.info you want to render Typography component.
Solution
First thing is your map function is incorrect. First parameter of map function is item of list and second is index. If you are not sure if info will always be present in data, you may want to have a null check as well.
{(data.info || []).map((info, index) => {
return <Typography key={index} variant="h6" className={classes.title}>{info.text}</Typography>
})}
You should be passing info from map to Typography component. Or use info value in content of Typography as shown above.
Update data.info and Typography will update automatically. For this, please make sure, data.info is a component state and not a plain variable. Something like
const [data, setData] = React.useState({});
And when you have value of data (assuming from API), then
setData(responseApi);

How to find the DOM element of a functional children in React?

Apparently this is not possible, but since I'm not a React expert I'd like to get a confirmation.
The usual way of getting the DOM element of a child is with:
render () {
const child = React.Children.only(this.props.children);
const cloned = React.cloneElement(child, {
ref: this.someRef
});
return cloned;
}
The problem here is that according to the docs:
You may not use the ref attribute on function components because they
don’t have instances
Another option to find the DOM element could be using React.findDOMNode() but according to a comment from this issue when trying to React.findDOMNode(child):
If you want to find one of your children's DOM nodes, you have to add
a ref first (using React.cloneElement) and then use that ref.
And we're back to the previous problem.
So, is it possible to find the DOM element of a functional component?
As suggested in this question, this may be one of few acceptable cases for calling stateless component function directly:
function withRef(SFC) {
return React.forwardRef((props, ref) => SFC({ref, ...props}));
}
...
render () {
const child = React.Children.only(this.props.children);
const isFunctional = typeof child === 'function' &&
(!child.prototype || !child.prototype.isReactComponent);
if (isFunctional) {
const Child = withRef(child.type);
return <Child {...child.props} ref={this.someRef}/>;
} else {
...
}
}
This won't work if functional component's child cannot accept a ref, too.
In this form it's inefficient because a child will be re-rendered. Depending on what's the purpose, there may be better ways to achieve this. A component could be designed to receive a component to render as a prop instead of a child. Or if DOM element is needed for specific task like setting up DOM event listener, this may be done like in this case .

Categories

Resources