getBoundingClientRect() on two React components and check if they overlap onScroll - javascript

I want to get a ref, more specifically a getBoundingClientRect() on the <Header/> and <Testimonials/> component. I then want to watch for a scroll event and check if the two components ever overlap. Currently, my overlap variable never flips to true even if what appears on the page is that the two components are overlaping.
const [isIntersecting, setIsIntersecting] = useState(false)
const header = useRef(null)
const testimonials = useRef(null)
const scrollHandler = _ => {
let headerRect = header.current.getBoundingClientRect();
let testiRect = testimonials.current.getBoundingClientRect();
let overlap = !(headerRect.right < testiRect.left ||
headerRect.left > testiRect.right ||
headerRect.bottom < testiRect.top ||
headerRect.top > testiRect.bottom)
console.log(overlap) // never flips to true
};
useEffect(() => {
window.addEventListener("scroll", scrollHandler, true);
return () => {
window.removeEventListener("scroll", scrollHandler, true);
};
}, []);
const App = () => {
return (
<div className="App">
<Header />
<LandingPage />
<div style={{ height: '100vh', backgroundColor: 'black', color: 'white' }}>
</div>
<AboutPage />
<TestimonialsPage />
<Footer />
</div>
);
}

First: Components can't receive directly a ref prop, unless you are wrapping the Component itself in a React.forwardRef wrapper:
const Component = React.forwardRef((props, ref) => (
<button ref={ref}>
{props.children}
</button>
));
// Inside your Parent Component:
const ref = useRef();
<Component ref={ref}>Click me!</Component>;
Second: you can also pass a ref down to a child as a standard prop, but you can't call that prop ref since that's a special reserved word just like the key prop:
const Component= (props) => (
<button ref={props.myRef}>
{props.children}
</button>
);
// Inside your Parent Component
const ref = useRef();
<Component myRef={ref}>Click me!</Component>;
This works perfectly fine, and if it's a your personal project you
might work like this with no issues, the only downside is that you
have to use custom prop name for those refs, so the code gets harder to
read and to mantain, especially if it's a shared repo.
Third: Now that you learnt how to gain access to the DOM node of a child Component from its parent, you must know that even if usually it's safe to perform manipulations on those nodes inside a useEffect ( or a componentDidMount ) since they are executed once the DOM has rendered, to be 100% sure you will have access to the right DOM node it's always better using a callback as a ref like this:
const handleRef = (node) => {
if (node) //do something with node
};
<Component ref={handleRef}/>
Basically your function hanldeRef will be called by React during
DOM node render by passing the node itself as its first parameter,
this way you can perform a safe check on the node, and be sure it's
100% valorized when you are going to perform your DOM manipulation.
Concerning your specific question about how to access the getBoundingClientRect of a child Component DOM node, I made a working example with both the approaches:
https://stackblitz.com/edit/react-pqujuz

You'll need to define each of your components as Forwarding Refs, eg
const Header = forwardRef<HTMLElement>((_, ref) => (
<header ref={ref}>
<h1>I am the header</h1>
</header>
));
You can then pass a HTMLElement ref to your components to refer to later
const headerRef = useRef<HTMLElement>(null);
const scrollHandler = () => {
console.log("header position", headerRef.current?.getBoundingClientRect());
};
useEffect(() => {
window.addEventListener("scroll", scrollHandler);
return () => {
window.removeEventListener("scroll", scrollHandler);
};
}, []);
return (
<Header ref={headerRef} />
);
I'm using TypeScript examples since it's easier to translate back down to JS than it is to go up to TS

Related

Problems using useRef / useImperativeHandle in mapping components

I have a dashboard with different components. Everything is working with a separate start-button on each component, now I need to have a common start-button, and for accessing the children's subfunctions from a parent, I understand that in React you should use the useRef.(but its perhaps not correct, but I'm struggling to see another way). I would like to have the flexibility to choose which component to start from this "overall start-button"
I have a component list that i map through shown below.
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
/>
)
This works fine, but I would like, as mentioned, to access a function called something like "buttonclick" in each of the children, so I tested this with a pressure-gauge component
The function "exposed" via the forwardRef and the useImparativeHandle
const ShowRadialGauge = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
buttonclick() {
setStart(!start);
},
}));
)
then in my dashboard I changed to :
const gaugepressure = useRef();
return(
<div>
<Button onClick={() => gaugepressure.current.buttonclick()}>
Start processing
</Button>
<ShowRadialGauge ref={gaugepressure} />
<div>
)
This works fine if I use the useRef from the dashboard and instead of mapping over the components, I add them manually.
I understand the useRef is not a props, but its almost what I want. I want to do something like this:
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
**ref={item.ref}**
/>
)
where the ref could be a part of my component array (as below) or a separate array.
export const ComponentsList = [
{
name: "Radial gauge",
text: "showradialgauge",
component: ShowRadialGauge,
ref: "gaugepressure",
},
{
name: "Heatmap",
text: "heatmap",
component: Heatmap,
ref: "heatmapstart",
},
]
Anyone have any suggestions, or perhaps do it another way?
You are on the right track with a React ref in the parent to attach to a single child component. If you are mapping to multiple children though you'll need an array of React refs, one for each mapped child, and in the button handler in the parent you will iterate the array of refs to call the exposed imperative handle from each.
Example:
Parent
// Ref to hold all the component refs
const gaugesRef = React.useRef([]);
// set the ref's current value to be an array of mapped refs
// new refs to be created as needed
gaugesRef.current = componentsList.map(
(_, i) => gaugesRef.current[i] ?? React.createRef()
);
const toggleAll = () => {
// Iterate the array of refs and invoke the exposed handle
gaugesRef.current.forEach((gauge) => gauge.current.toggleStart());
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Gauges
</button>
{componentsList.map(({ name, component: Component, ...props }, i) => (
<Component
key={name}
ref={gaugesRef.current[i]}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = React.forwardRef(({ name }, ref) => {
const [start, setStart] = React.useState(false);
const toggleStart = () => setStart((start) => !start);
React.useImperativeHandle(ref, () => ({
toggleStart
}));
return (....);
});
The more correct/React way to accomplish this however is to lift the state up to the parent component and pass the state and handlers down to these components.
Parent
const [gaugeStarts, setGaugeStarts] = React.useState(
componentsList.map(() => false)
);
const toggleAll = () => {
setGaugeStarts((gaugeStarts) => gaugeStarts.map((start) => !start));
};
const toggleStart = (index) => {
setGaugeStarts((gaugeStarts) =>
gaugeStarts.map((start, i) => (i === index ? !start : start))
);
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Guages
</button>
{componentsList.map(({ name, component: Component, ...props },, i) => (
<Component
key={name}
start={gaugeStarts[i]}
toggleStart={() => toggleStart(i)}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = ({ name, start, toggleStart }) => {
return (
<>
...
<button type="button" onClick={toggleStart}>
Toggle Start
</button>
</>
);
};
#Drew Reese
Thx Drew,
you are off course correct. I'm new to React, and I'm trying to wrap my head around this "state handling".
I tested your suggestion, but as you say, its not very "React'ish", so I lifted the state from the children up to the parent.
In the parent:
const [componentstate, setComponentstate] = useState([
{ id:1, name: "pressuregauge", start: false},
{ id:2, name: "motormap", start: false },
{ id:3, name: "heatmapstart", start: false},
]);
then in the component ShowRadialGauge, I did like this:
const ShowRadialGauge = ({ props, componentstate })
and if we need to keep the button in each component, I have the id in the componentstate object that is desctructured, so I can send that back.
.
First of all, why do you need refs to handle click when you can access it via onClick. The most common use case for refs in React is to reference a DOM element or store value that is persist between renders
My suggestion are these
First, try to make it simple by passing a function and then trigger it via onClick
Second if you really want to learn how to use imperativeHandle you can reference this video https://www.youtube.com/watch?v=zpEyAOkytkU.

react - Add a component after main component has loaded

I have a functional react component like this:
function ComponentA(){
function AddComponentB(){
return <><ComponentB /></>
}
useEffect(() => {
AddComponentB();
}, []);
return
<>
<div id="parent"></div>
</>
}
Now I have understood that everything under useEffect is loaded once the ComponentA is loaded. I want to add the ComponentB to div with id parent. Please help me understand how do I specify where to add the component.
P.S. I know I can do it by document.getElementById("parent").append(ComponentB) but I am looking for other ways.
Try using conditional rendering, like below :
export default function ComponentA() {
const [renderComponentB, setRenderComponentB] = useState(false)
useEffect(() => {
setRenderComponentB(true);
}, []);
return(<div id="parent">
{renderComponentB && <ComponentB/>}
</div>)
}
No, you do not manipulate the DOM directly when using React.
You need to have a "flag" that dictates if you want to render the extra component or not.
Something like
function ComponentA(){
const [renderComponentB, setRenderComponentB] = useState(false);
useEffect(() => {
setRenderComponentB(true);
}, []);
return (
<>
<div id="parent">
{renderComponentB && <ComponentB/>}
</div>
</>
);
}
Although i am not sure why you want to delay the ComponentB for just one rendering cycle.
As far as the getBoundingClientRect of ComponentA, there is no such thing, as that depends on what the component actually renders in the DOM. ComponentA in it self is not part of the DOM.
In your specific case, though you could add a ref to the #parent element and use that for the getBoundingClientRect, since it is your "container" element of the ComponentA
function ComponentA(){
const [renderComponentB, setRenderComponentB] = useState(false);
const parentRef = useRef();
useEffect(() => {
setRenderComponentB(true);
}, []);
useLayoutEffect(() => {
const rect = parentRef.current.getBoundingClientRect();
// do what you want with the rect here
// if you want to apply values to the ComponentB
// add them to a state variable and use those when
// rendering the ComponentB
}, [])
return (
<>
<div id="parent" ref={parentRef}>
{renderComponentB && <ComponentB/>}
</div>
</>
);
}
You should call method that returns component in render.
You do not need useEffect as i understood from your answer.
function ComponentA(){
function AddComponentB(){
return <><ComponentB /></>
}
return
<>
<div id="parent">
{AddComponentB()}
</div>
</>
}

How to get element height and width from ReactNode?

I have a dynamic component in which I pass in children as prop.
So the props look something like:
interface Props {
...some props
children: React.ReactNode
}
export default Layout({...some props, children}: Props) {...}
I need to access the size of the children elements (height and width), in the Layout component. Note that the children are from completely different components and are non-related.
I can use the Layout component as follow:
<Layout ...some props>
<Child1 /> // I need to know the height and width of this child
<Child2 /> // as well as this child
<Child3 /> // and this child.
</Layout>
How can I do so dynamically? Do I somehow have to convert ReactNode to HTMLDivElement? Note that there is no way I can pass in an array of refs as a prop into Layout. Because that the pages which use Layout are dynamically generated.
Since many doesn't really understand what I meant by dynamically generated. It means that the pages which are using the Layout component can pass in x amount of children. The amount of children is unknown but never 0.
You can achieve this by using React.Children to dynamically build up a list of references before rendering the children. If you have access to the children element references, you can follow the below approach. If you don't then you can follow the bit at the bottom.
You have access to the children element references
If the children components pass up their element reference, you can use React.Children to loop through each child and get each element reference. Then use this to perform calculations before the children components are rendered.
i.e. This is a very simple example on how to retrieve the references and use them.
interface LayoutWrapperProps {
onMount: () => void;
}
const LayoutWrapper: React.FC<LayoutWrapperProps> = ({ onMount, children }) => {
React.useEffect(() => {
onMount();
}, [onMount]);
return <>{children}</>;
};
const Layout: React.FC = ({ children }) => {
const references = React.useRef<HTMLElement[]>([]);
React.useEffect(() => {
references.current = [];
});
function getReference(ref: HTMLElement) {
references.current = references.current.filter(Boolean).concat(ref);
}
function getHeights() {
const heights = references.current.map((ref) =>
ref?.getBoundingClientRect()
);
console.log(heights);
}
const clonedChildren = React.Children.map(children, (child) => {
return React.cloneElement(child as any, {
ref: getReference
});
});
return <LayoutWrapper onMount={getHeights}>{clonedChildren}</LayoutWrapper>;
};
If you don't have access to the children element references
If the children components aren't passing up an element as the reference, you'll have to wrap the dynamic children components in a component so we can get an element reference. i.e.
const WrappedComponent = React.forwardRef((props, ref) => {
return (
<div ref={ref}>
{props.children}
</div>
)
});
When rendering the children components, then the code above that gets the references will work:
<Layout>
<WrappedComponent>
<Child1 />
</WrappedComponent>
</Layout>
Since we don't know how your children is built, here is what I can propose you :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const el1Ref = React.useRef();
const el2Ref = React.useRef();
const [childrenValues, setChildrenValues] = React.useState([]);
React.useEffect(() => {
setChildrenValues([
el1Ref.current.getBoundingClientRect(),
el2Ref.current.getBoundingClientRect()
]);
}, []);
return (
<Parent childrenVals={childrenValues}>
<span ref={el1Ref}>
<Child value="Hello" />
</span>
<span ref={el2Ref}>
<Child value="<div>Hello<br />World</div>" />
</span>
</Parent>
);
};
const Parent = ({ children, childrenVals }) => {
React.useEffect(() => {
console.log('children values from parent = ', childrenVals);
});
return <>{children}</>;
};
const Child = ({ value }) => {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
};
render(<App />, document.getElementById('root'));
And here is the repro on Stackblitz.
The idea is to manipulate how your children is built.

Using querySelectorAll on a React component's children

I am trying to render a list of elements lazily by initially rendering only what's in view and then replacing placeholder elements with the real thing as you scroll down using an IntersectionObserver. This keeps the list's length from changing as I add new elements and is much cheaper to render as I'm only listing empty divs when the page loads. Like a poor man's virtualization.
The issue:
The parent element adds its children to the IO as so:
useEffect(() => {
if (!observer.current) return
const els = [...document.querySelectorAll(`.list > :nth-child(n + 10})`)]
els.forEach(el => observer.current.observe(el))
}, [list])
els does not always find elements as React renders its elements as it sees fit. The problem is that I don't know how I could do this using ref. Using context I may be able to do this but I'd imagine there would be constant rerendering of the entire list.
I hope this example can help you understand how to use Refs in your case. I recommend you to read React Docs about Refs.
Please let me know if you have any doubt.
const MyItem = React.forwardRef((props, ref) => {
return (<div ref={ref}>{'ITEM ' + props.index}</div>);
});
const MyList = () => {
const [list, setList] = React.useState([]);
const refsMap = React.useRef(new Map());
const onClickHandler = React.useCallback(() => {
setList((previousList) => {
const key = previousList.length;
return ([
...previousList,
<MyItem key={key} index={key} ref={(ref) => {
if (ref) {
refsMap.current.set(key, ref);
} else {
refsMap.current.delete(key);
}
console.log(refsMap.current.get(key));
}} />
]);
});
}, []);
return (
<div>
<div>
<button type={'button'} onClick={onClickHandler}>{'ADD'}</button>
</div>
<div>
{list}
</div>
</div>
);
};
function App() {
return (
<MyList />
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="app"></div>

How to access a DOM element in React? What is the equilvalent of document.getElementById() in React

How do I select certain bars in react.js?
This is my code:
var Progressbar = React.createClass({
getInitialState: function () {
return { completed: this.props.completed };
},
addPrecent: function (value) {
this.props.completed += value;
this.setState({ completed: this.props.completed });
},
render: function () {
var completed = this.props.completed;
if (completed < 0) { completed = 0 };
return (...);
}
I want to use this React component:
var App = React.createClass({
getInitialState: function () {
return { baction: 'Progress1' };
},
handleChange: function (e) {
var value = e.target.value;
console.log(value);
this.setState({ baction: value });
},
handleClick10: function (e) {
console.log('You clicked: ', this.state.baction);
document.getElementById(this.state.baction).addPrecent(10);
},
render: function () {
return (
<div class="center">Progress Bars Demo
<Progressbar completed={25} id="Progress1" />
<h2 class="center"></h2>
<Progressbar completed={50} id="Progress2" />
<h2 class="center"></h2>
<Progressbar completed={75} id="Progress3" />
<h2 class="center"></h2>
<span>
<select name='selectbar' id='selectbar' value={this.state.baction} onChange={this.handleChange}>
<option value="Progress1">#Progress1</option>
<option value="Progress2">#Progress2</option>
<option value="Progress3">#Progress3</option>
</select>
<input type="button" onClick={this.handleClick10} value="+10" />
<button>+25</button>
<button>-10</button>
<button>-25</button>
</span>
</div>
)
}
});
I want to execute the handleClick10 function and perform the operation for my selected progressbar.
But the result I get is:
You clicked: Progress1
TypeError: document.getElementById(...) is null
How do I select the certain Element in react.js?
You can do that by specifying the ref
EDIT: In react v16.8.0 with function component, you can define a ref with useRef. Note that when you specify a ref on a function component, you need to use React.forwardRef on it to forward the ref to the DOM element of use useImperativeHandle to to expose certain functions from within the function component
Ex:
const Child1 = React.forwardRef((props, ref) => {
return <div ref={ref}>Child1</div>
});
const Child2 = React.forwardRef((props, ref) => {
const handleClick= () =>{};
useImperativeHandle(ref,() => ({
handleClick
}))
return <div>Child2</div>
});
const App = () => {
const child1 = useRef(null);
const child2 = useRef(null);
return (
<>
<Child1 ref={child1} />
<Child1 ref={child1} />
</>
)
}
EDIT:
In React 16.3+, use React.createRef() to create your ref:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
In order to access the element, use:
const node = this.myRef.current;
DOC for using React.createRef()
EDIT
However facebook advises against it because string refs have some issues, are considered legacy, and are likely to be removed in one of the future releases.
From the docs:
Legacy API: String Refs
If you worked with React before, you might be
familiar with an older API where the ref attribute is a string, like
"textInput", and the DOM node is accessed as this.refs.textInput. We
advise against it because string refs have some issues, are considered
legacy, and are likely to be removed in one of the future releases. If
you're currently using this.refs.textInput to access refs, we
recommend the callback pattern instead.
A recommended way for React 16.2 and earlier is to use the callback pattern:
<Progressbar completed={25} id="Progress1" ref={(input) => {this.Progress[0] = input }}/>
<h2 class="center"></h2>
<Progressbar completed={50} id="Progress2" ref={(input) => {this.Progress[1] = input }}/>
<h2 class="center"></h2>
<Progressbar completed={75} id="Progress3" ref={(input) => {this.Progress[2] = input }}/>
DOC for using callback
Even older versions of react defined refs using string like below
<Progressbar completed={25} id="Progress1" ref="Progress1"/>
<h2 class="center"></h2>
<Progressbar completed={50} id="Progress2" ref="Progress2"/>
<h2 class="center"></h2>
<Progressbar completed={75} id="Progress3" ref="Progress3"/>
In order to get the element just do
var object = this.refs.Progress1;
Remember to use this inside an arrow function block like:
print = () => {
var object = this.refs.Progress1;
}
and so on...
For getting the element in react you need to use ref and inside the function you can use the ReactDOM.findDOMNode method.
But what I like to do more is to call the ref right inside the event
<input type="text" ref={ref => this.myTextInput = ref} />
This is some good link to help you figure out.
With newer versions of React you can use and manipulate the DOM via hooks like this:
import React, { useEffect, useRef } from "react";
const MyComponent = () => {
const myContainer = useRef(null);
useEffect(() => {
console.log("myContainer..", myContainer.current);
});
return (
<>
<h1>Ref with react</h1>
<div ref={myContainer}>I can use the DOM with react ref</div>
</>
);
};
export default MyComponent;
Whenever you want to access your DOM element just use myContainer.current
You can replace
document.getElementById(this.state.baction).addPrecent(10);
with
this.refs[this.state.baction].addPrecent(10);
<Progressbar completed={25} ref="Progress1" id="Progress1"/>
Disclaimer: While the top answer is probably a better solution, as a beginner it's a lot to take in when all you want is something very simple. This is intended as a more direct answer to your original question "How can I select certain elements in React"
I think the confusion in your question is because you have React components which you are being passed the id "Progress1", "Progress2" etc. I believe this is not setting the html attribute 'id', but the React component property. e.g.
class ProgressBar extends React.Component {
constructor(props) {
super(props)
this.state = {
id: this.props.id <--- ID set from <ProgressBar id="Progress1"/>
}
}
}
As mentioned in some of the answers above you absolutely can use document.querySelector inside of your React app, but you have to be clear that it is selecting the html output of your components' render methods. So assuming your render output looks like this:
render () {
const id = this.state.id
return (<div id={"progress-bar-" + id}></div>)
}
Then you can elsewhere do a normal javascript querySelector call like this:
let element = document.querySelector('#progress-bar-Progress1')
You have to follow two different ways to do it in Class and Functional components.
For class components
<input type="text" ref={ref => this.myTextInput = ref} />
Look at the above code. Use "ref" attribute to refer to the relevant element. Then you will be able to refer to that element using that reference. In this example, I can use "this.myTextInput" to refer to the above input element.
For functional components
const textInput = useRef(null)
Use the "useRef" hook and set that variable name as the value of the "ref" attribute of the element you want to refer to (like below).
<input type="text" ref={textInput} />
An example for this on functional components.
import React, {useRef} from 'react'
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input type="text" ref={textInput} />
</div>
);
}
Want to learn more? Here you go
Since React uses JSX code to create an HTML we cannot refer dom using regulation methods like documment.querySelector or getElementById.
Instead we can use React ref system to access and manipulate Dom as shown in below example:
constructor(props){
super(props);
this.imageRef = React.createRef(); // create react ref
}
componentDidMount(){
**console.log(this.imageRef)** // acessing the attributes of img tag when dom loads
}
render = (props) => {
const {urls,description} = this.props.image;
return (
<img
**ref = {this.imageRef} // assign the ref of img tag here**
src = {urls.regular}
alt = {description}
/>
);
}
}
In my case, I wasn't able to use ref because elements were somewhere between many child components and I have to access them by class and id instead of ref. So, trying with useEffect hook didn't work as it can't find the element:
useEffect(() => {
const el1 = document.querySelector('.el1')
const el2 = document.querySelector('.el2')
}, [])
The element is undefined because when it is mounted the children components also doesn't mounted before this parent component.
So, what I did is to use timeout:
useEffect(() => {
const timer = setTimeout(() => {
const el1 = document.querySelector('.el1')
const el2 = document.querySelector('.el2')
},500)
return () => {
clearTimeout(timer)
}
}, [])
Now, it worked fine. It found the DOM and I was able to manipulate with them. Hope, this helps someone!
The equivalent of document.getElementById() in React is document.querySelector().

Categories

Resources