Can i use the document object in React? - javascript

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>
)
}

Related

Render only components with changes

I have an array with thousands of strings and is passed to a component:
Main component:
const array = ['name1', 'name2', 'name3'];
const [names, setNames] = useState(array);
const onClick = (index) => {
setNames(names.map((name, i) => {
if (i === index) {
return 'name changed';
}
};
};
return (
<ul>
{array.map((name, index) => (
<li key={index}>
<ShowName name={name} key={index} onClick={() => onClick(index)} />
</li>
)}
</ul>
);
ShowName component:
let a = 0;
export default function ShowName({ name, onClick }) {
a += 1;
console.log(a);
return (
<button type="button" onClick={onClick}>{name}</button>
);
}
There's also a button which changes a name randomly. But whenever the button is pressed, all the ShowName components are rerendering. I've been trying to use useCallback and useMemo, but the components are still rerendering x times (x is the length of the array).
const ShowNameHoc = ({ name }) => {
return <ShowName name={name} />
};
return (
<div>
{array.map((name, index) => <ShowNameHoc name={name} key={index} />)}
</div>
);
What should I do if I only want to rerender the component with a change?
You have a misunderstanding in the concepts here. The default is for React to call render on all children, regardless of whether the props changed or not.
After that happened, React will compare that new Virtual DOM to the current Virtual DOM and then only update those parts of the real DOM that changed.
That's why the code in a render method should be quick to execute.
This behavior can be changed by using features like useMemo, PureComponents or shouldComponentUpdate.
References:
https://reactjs.org/docs/rendering-elements.html (Bottom):
Even though we create an element describing the whole UI tree on every tick, only the text node whose contents have changed gets updated by React DOM.
https://reactjs.org/docs/optimizing-performance.html#avoid-reconciliation
Even though React only updates the changed DOM nodes, re-rendering still takes some time. In many cases it’s not a problem, but if the slowdown is noticeable, you can speed all of this up by overriding the lifecycle function shouldComponentUpdate, which is triggered before the re-rendering process starts.
...
In most cases, instead of writing shouldComponentUpdate() by hand, you can inherit from React.PureComponent. It is equivalent to implementing shouldComponentUpdate() with a shallow comparison of current and previous props and state.
Also, read this for some more background info: https://dev.to/teo_garcia/understanding-rendering-in-react-i5i
Some more detail:
Rendering in the broader sense in React means this (simplified):
Update existing component instances with the new props where feasible (this is where the key for lists is important) or create a new instance.
Calling render / the function representing the component if shouldComponentUpdate returns true
Syncing the changes to the real DOM
This gives you these optimization possibilities:
Ensure you are reusing instances instead of creating new ones, e.g. by using a proper key when rendering lists. Why? New instances always result in the old DOM node to be removed from the real DOM and a new one to be added. Even when unchanged. Reusing an instance will only update the real DOM if necessary. Please note: This has no effect on whether or not render is being called on your component.
Make sure your render method doesn't do heavy lifting or if it does, memoize those results
Use PureComponents or shouldComponentUpdate to prevent the call to render altogether in scenarios where props didn't change
Answering your specific question:
To actually prevent your ShowName component from being rendered - into the Virtual DOM - if their props changed, you need to perform the following changes:
Use React.memo on your ShowName component:
function ShowName({ name, onClick }) {
return (
<button type="button" onClick={onClick}>{name}</button>
);
}
export default memo(ShowName);
Make sure the props are actually unchanged by not passing a new callback to onClick on each render of the parent. onClick={() => onClick(index)} creates a new anonymous function every time the parent is being rendered.
It's a bit tricky to prevent that, because you want to pass the index to this onClick function. A simple solution is to create another component that is passed the onClick with the parameter and the index and then internally uses useCallback to construct a stable callback. This only makes sense though, when rendering your ShowName is an expensive operation.
That is happening because you are not using the key prop on <ShowName/> component.
https://reactjs.org/docs/lists-and-keys.html
it could look something like this
return (
<div>
{array.map(name => <ShowName key={name} name={name} />)}
</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.

react gatsby: adding css class dynamically not working in prod build

I need to add a css class dynamically on a div. For this I've set the css class like this:-
const Container = ({ divClass }) => {
const [cssClass, setcssClass] = useState(divClass)
return (
<div id="containerDiv" className={cssClass} ></div>
{/* <div id="containerDiv" className={divClass} ></div> even this doesn't work*/}
)
}
This works as expected in development build, but doesn't work in prod build. Even without using state if I set class to divClass it's not working in prod build.
It only works after I set state using setcssClass, like this-
const Container = ({ divClass }) => {
const [cssClass, setcssClass] = useState(divClass)
return (
<div id="containerDiv" className={cssClass} onClick={() => setcssClass('testtt')}></div>
)
}
Can anypone please explain this discrepancy in development and production build?
Your code is not working on your first try because at the moment you are defining const [cssClass, setcssClass] = useState(divClass), your <div> and it's class (divClass or cssClass) is not defined yet.
The solution is to use a useEffect hook with empty deps ([]) since it's a function that is triggered once the DOM tree is loaded (and your <div> in it).
I wouldn't recommend the approach of the other answer (besides being effective) because it impacts directly the DOM and that's the reason why you are using React and not jQuery (you creating a virtual DOM to manipulate). You can achieve the same result by using a React-based approach with useRef hook.
const Container = ({ divClass }) => {
const containerDivRef = useRef(null);
useEffect(() => {
containerDivRef.current.classList.add(divClass)
}, []);
return (
<div ref={containerDivRef}></div>
)
}
Your <div> information is stored inside the containerDivRef.current exactly in the same way that using document.querySelector but without affecting the DOM.
The discrepancy that you mention between develop and build is caused by the webpack's assets treatment and Gatsby's configuration. Since with a gatsby develop (runtime) allows you to use browser's APIs (such as window, document, other global or DOM objects) in the gatsby build (buildtime) the code is processed in the server-side, where those variables are not allowed (even created), that's why you usually should wait for them. You can check for further details in the Overview of the Gatsby Build Process documentation.
const Container = ({ divClass }) => {
useEffect(() => {
const containerDiv = document.querySelector("#containerDiv");
containerDiv.classList.add(divClass);
}, []);
return (
<div id="containerDiv"></div>
)
}
Have you tried manipulating the dom manually inside useEffect?
I know this might not be the most ideal solution but maybe it solves the problem. lol

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);

React, how to access the DOM element in my render function from the same component

I'm wondering what's the best practice for accessing the DOM elements inside my render function from the same component. Note that I will be rendering this component multiple times on a page.
e.g.
var TodoItem = React.createClass({
...
render:function(){
function oneSecLater(){
setTimeout(function(){
//select the current className? this doesn't work, but it gives you an idea what I want.
document.getElementsByClassName('name').style.backgroundColor = "red";
)}, 1000);
}
return(
<div className='name'>{this.oneSecLater}</div>
)
})
You can use ReactDOM.findDOMNode(this) to access the underlying DOM node. But accessing the DOM node and manipulating like you do is against the React style of programming. It's better to use a state variable and called the setState method to re-render the DOM.
Here, no need to use setTimeout. There are lifecycle methods for component, of which componentDidMount is called after the render. So, you can get the reference to your div in the method.
var TodoItem = React.createClass({
...
componentDidMount: function () {
if(this.myDiv) {
this.myDiv.style.backgroundColor = "red";
}
}
render:function(){
return(
<div className='name' ref = {c => this.myDiv = c}></div>
);
});
You can make use of ref callback to access the dom element in react, which is what React Docs recommend to follow
and do that in the componentDidMount lifecycle function as refs won't be accessible before the DOM is created
var TodoItem = React.createClass({
...
componentDidMount() {
setTimeout(function(){
this.myDiv.style.backgroundColor = "red";
)}, 1000);
}
render:function(){
return(
<div className='name' ref={(ele) => this.myDiv = ele}></div>
)
})
DOCS
You should avoid accessing DOM element because the whole point of react is putting abstraction over dom. React keeps representation of DOM in memory which is referred as VirtualDom. Working with VirtualDom will make unit testing your application is easier.If you really know what you are doing, here is how to do it:
componentDidMount(){
const name=this.name.current.style() //current will return the actual DOM
element
}
name=React.createRef() //create a ref object
<div ref={this.name} className="anything" /> //your classname does not need to be named as "name". You access the element via the "ref" attribute not the classname.
In ComponentDidMount, when your component is mounted its style change will be applied.
Just came across this after trying to do form validation before opening a stripe checkout modal with React 14.
I would like to note that you're not actually accessing a DOM Element with references. You're simply accessing the React Component Object. Shown here:
The top one is calling ref.ticketForm, the bottom is showing document.getElementById('ticketform').
The reason I needed to do this was the following:
<Button color="success" size="lg" type="button" onClick={(e) => {
const ticketForm = document.getElementById('ticketForm');
const isValid = ticketForm.reportValidity();
if (!isValid) e.stopPropagation();
}}>Buy Tickets</Button>
reportValidity() is a DOM Element method: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity
Referencing this issue, this person showed this method being used on a reference object, which naturally isn't correct: https://github.com/azmenak/react-stripe-checkout/issues/121#issuecomment-431635855
Hopefully this clears up that DOM Elements do not explicitly equal React Components. If you need to manipulate the DOM in any way, do it in a react way first. This is an edge case where I would rather rely on form validation for a dynamic form than manually doing very heavy custom validation.
here is my solution:
To get a computedCss of an specific element, you need to add a ref attribute to the elemenet first.
enter image description here
render(){
<div>
<Row className="chartline2">
<Col className="gutter-row" span={24}>
<div className="gutter-box lineChartWrap" id="lineChartWrap" ref="lineChartWrap">
<LineChart data={lineChartData} options={lineChartOptions} width="100%" height="300"/>
</div>
</Col>
</Row>
</div>
}
And then, in the componentDidUpdate() function, get your element's css by using window.getComputedStyle(this.refs.lineChartWrap).XXX
enter image description here
componentDidUpdate(){
console.log("------- get width ---");
let ele = document.querySelector("#lineCharWrap");
console.log(this.refs.lineChartWrap);
console.log(window.getComputedStyle(this.refs.lineChartWrap).width);
}

Categories

Resources