React - get element.getBoundingClientRect() after window resize - reactjs

I have a class that needs to get the size of a DOM element. It works well, but when I resize the window it doesn't update until I change the state in my app, forcing a rerender. I've tried adding this.forceUpdate to a 'resize' event listener in componentDidMount(), but it didn't work. Perhaps I did something wrong? Ideally I'd like to avoid using this.forceUpdate for perfomance implications anyway. Any work arounds for this? Thanks in advance!
My code:
class MyComponent extends React.Component {
state = { x: 0, y: 0 }
refCallback = (element) => {
if (!element) {
return
}
const { x, y } = element.getBoundingClientRect()
this.setState({ x, y })
}
render() {
console.log('STATE:', this.state) // Outputs the correct x and y values.
return (
<div ref={this.refCallback}>
<button>Hello world</button>
</div>
)
}
}

If you want to measure some element in your component whenever the window resizes, it's going to look something like this:
class MyComponent extends React.Component {
state = {
x: 0,
y: 0,
};
element = React.createRef();
onWindowResize = () => {
if (this.element.current) {
const {x, y} = this.element.current.getBoundingClientRect();
this.setState({x, y}, () => {
console.log(this.state);
});
}
};
componentDidMount() {
window.addEventListener('resize', this.onWindowResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.onWindowResize);
}
render() {
return (
<div ref={this.element}>
<button>Hello, World</button>
</div>
);
}
}
The trick here is that your ref callback is only called once, when the element is initially added to the DOM. If you want to update state whenever you resize the window, you're going to need a 'resize' event handler.

That happening because:
From the React documentation:
Adding a Ref to a DOM Element
React supports a special attribute that you can attach to any component. The ref attribute takes a callback function, and the callback will be executed immediately after the component is mounted or unmounted.
React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts.
So, that's why when you refresh you get the value. To overcome the problem you can do something like this:
import React from "react";
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.state = {
x: 0,
y: 0
};
}
updateDimensions = () => {
if (this.myRef.current) {
const {x, y} = this.myRef.current.getBoundingClientRect();
this.setState({ x, y });
}
};
componentDidMount() {
window.addEventListener("resize", this.updateDimensions);
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateDimensions);
}
render() {
console.log("STATE:", this.state); // Outputs the correct x and y values.
return (
<div ref={this.myRef}>
<button>Hello world</button>
</div>
);
}
}
export default MyComponent;
Hope this works for you.

Related

ReadOnly state when I need to change it

I have the following code and I really need to be able to change the state however I am having issues when I try and do the following.
export default class Mediaplayer extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: [],
station: null,
playButton: false,
muteButton: false,
};
}
render() {
const { station, playButton, muteButton } = this.state;
const handleMClick = (e) => {
// Event("Play Button", "Listner Hit Play", "PLAY_BUTTON");
console.log("clicking the play and pause button");
this.setState({ playButton: !playButton });
playButton
? document.getElementById("player").play()
: document.getElementById("player").pause();
};
return (
<i onClick={handleMClick}>
{playButton ? <PlayCircle size={60} /> : <PauseCircle size={60} />}
</i>
);
}
}
I am getting this state is ReadOnly.
setState() only takes effect after the whole eventHandler is
finished, this is called state batching.
Your this.setState({playButton:!playButton}) only run after handleMClick() is finished.
In other words, playButton === true will not available within your handleMClick() function.
On solution could be to put this:
playButton ? document.getElementById("player").play() : document.getElementById("player").pause()
Inside a componentDidUpdate() so it will take effect in the next render after your state is updated.
Direct dom manipulation is not a recommended way of doing things in react because you can always change dom element state according to your react component state or props.
I see your component is called media player but it doesn't have the #player inside it? Perhaps you could reconsider how you arranging the dom element.
Also try to use a functional component instead of class component. I will give an answer with a functional component.
MediaPlayer Component
import { useState } from 'react';
const MediaPlayer = props => {
const [play, setPlay] = useState(false);
const togglePlay = () => {
setPlay( !play );
}
return (
<i onClick={togglePlay}>
{!play ?
<PlayCircle size={60}/>
:
<PauseCircle size={60}/>}</i>
}
);
}

How to update state just after rendering

I have the following component:
import React from 'react';
import './styles.css';
import ToolTip from '../../Common/components/ToolTip/ToolTip';
export default class RouteTitleTooltipComponent extends React.Component {
constructor(props) {
super(props);
this.titleParagraphRef = React.createRef();
this._tooltipTimer = null;
this.state = { shouldPopupBeEnabled: false, isTooltipShown: false };
this._showTooltip = this._showTooltip.bind(this);
this._hideTooltip = this._hideTooltip.bind(this);
}
componentDidMount() {
const { scrollWidth, clientWidth } = this.titleParagraphRef.current;
const shouldPopupBeEnabled = scrollWidth > clientWidth;
this.setState({ shouldPopupBeEnabled });
}
_showTooltip() {
this._tooltipTimer = setTimeout(
() => {
this.setState({ isTooltipShown: true });
}, 1000,
);
}
_hideTooltip() {
clearTimeout(this._tooltipTimer);
this.setState({ isTooltipShown: false });
}
render() {
const { shouldPopupBeEnabled, isTooltipShown } = this.state;
const { message } = this.props;
return (
<ToolTip
message="Tooltip!!"
popoverOpen={shouldPopupBeEnabled && isTooltipShown}
>
<div
ref={this.titleParagraphRef}
onMouseOver={this._showTooltip}
>
{message}
</div>
</ToolTip>
);
}
}
This basically renders a floating tooltip over a div element if the message inside of it is bigger than the container. To do that, I use scrollWidth and clientWidth of the div element using a React reference. To detect those values I use componentDidMount, but this only works in full renders of the component. That is, if I have the component visible and reload the page, both values are equal to 0 and it does not work.
In addition, if I change the message, it does not work either because the component is already mounted.
So what I want is to change the state right after the component is mounted or updated so that the react reference is rendered and clientWidth and scrollWidth are not 0.
I have tried replace componentDidUpdate instead of componentDidMount but it's not a good practica to use setState inside componentDidUpdate.
Any solution?
First you should know that componentDidMount will execute only once. Therefor you can go for componentDidUpdate but don't forget to put a condition as it will render in a loop.
componentDidUpdate(prevProps,prevState) {
const shouldPopupBeEnabled = scrollWidth > clientWidth;
if (shouldPopupBeEnabled !== this.state.shouldPopupBeEnabled ) {
this.setState({shouldPopupBeEnabled });
}
}
Or you can go for functional components and use useEffect which will only render again if state changes.
useEffect(() => {
console.log('mounted');
}, [shouldPopupBeEnabled]) // It will re render id `shouldPopupBeEnabled` changes

How to get the DOM node from a Class Component ref with the React.createRef() API

I have these two components:
import { findDOMNode } from 'react-dom';
class Items extends Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.selectedItemRef = React.createRef();
}
componentDidMount() {
if (this.props.selectedItem) {
this.scrollToItem();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.scrollToItem();
}
}
scrollToItem() {
const itemsRef = this.ref.current;
const itemRef = findDOMNode(this.selectedItemRef.current);
// Do scroll stuff here
}
render() {
return (
<div ref={this.ref}>
{this.props.items.map((item, index) => {
const itemProps = {
onClick: () => this.props.setSelectedItem(item.id)
};
if (item.id === this.props.selectedItem) {
itemProps.ref = this.selectedItemRef;
}
return <Item {...itemProps} />;
})}
</div>
);
}
}
Items.propTypes = {
items: PropTypes.array,
selectedItem: PropTypes.number,
setSelectedItem: PropTypes.func
};
and
class Item extends Component {
render() {
return (
<div onClick={() => this.props.onClick()}>item</div>
);
}
}
Item.propTypes = {
onClick: PropTypes.func
};
What is the proper way to get the DOM node of this.selectedItemRef in Items::scrollToItem()?
The React docs discourage the use of findDOMNode(), but is there any other way? Should I create the ref in Item instead? If so, how do I access the ref in Items::componentDidMount()?
Thanks
I think what you want is current e.g. this.selectedItemRef.current
It's documented on an example on this page:
https://reactjs.org/docs/refs-and-the-dom.html
And just to be safe I also tried it out on a js fiddle and it works as expected! https://jsfiddle.net/n5u2wwjg/195724/
If you want to get the DOM node for a React Component I think the preferred way of dealing with this is to get the child component to do the heavy lifting. So if you want to call focus on an input inside a component, for example, you’d get the component to set up the ref and call the method on the component, eg
this.myComponentRef.focusInput()
and then the componentRef would have a method called focusInput that then calls focus on the input.
If you don't want to do this then you can hack around using findDOMNode and I suppose that's why it's discouraged!
(Edited because I realized after answering you already knew about current and wanted to know about react components. Super sorry about that!)

Do react render props cause remounting of the child components?

I was just wondering if people know if using the "render props" pattern causes excessive mounting/unmounting of the child component.
For example, adapting from the react docs (https://reactjs.org/docs/render-props.html):
<Mouse>
{mouse => (
<ShowMousePosition mouse={mouse}/>
)}
</Mouse>
class ShowMousePosition extends React.Component {
componentDidMount(){
console.log('mounting!')
}
render () {
const {mouse} = this.props
return (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)
}
}
I know the react docs say:
Using a render prop can negate the advantage that comes from using React.PureComponent if you create the function inside a render method. This is because the shallow prop comparison will always return false for new props, and each render in this case will generate a new value for the render prop.
But, will "mounting!" be called over and over as the user moves the mouse around?
Thanks!
I went ahead and tried to answer my own question using a fiddle. It appears that "mounting!" is not called over and over again:
https://jsfiddle.net/69z2wepo/186690/
Here is the code:
class Hello extends React.Component {
render() {
return <Mouse>
{mouse => (
<ShowMousePosition mouse={mouse}/>
)}
</Mouse>
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: 800, width: 800 }} onMouseMove={this.handleMouseMove}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.children(this.state)}
</div>
);
}
}
class ShowMousePosition extends React.Component {
componentDidMount(){
console.log('mountin!')
}
render () {
const {mouse} = this.props
return (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)
}
}
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
componentDidMount is only called once but componentDidUpdate will be called multiple times along with your render function every time your state/props is changed.

React. Debouncing function that implements setState method

i am developing a simple hoc component that passes viewport dimensions to its children. On window resize, I initiate handleResize method to pass new window dimensions into child component. I want to use debounce func from lodash to minimize number of times that handleResize method is called(ref).
import React from 'react'
import debounce from 'lodash/debounce'
const getDimensions = (Component) => {
return class GetDimensions extends React.Component {
constructor () {
super()
this.state = {
viewport: {
x: window.innerWidth,
y: window.innerHeight
}
}
}
handleResize = () => {
this.setState(() => ({viewport: {x: window.innerWidth, y: window.innerHeight}}))
}
componentDidMount = () => {
if (window) window.addEventListener('resize', debounce(this.handleResize, 400))
}
componentWillUnmount = () => {
if (window) window.removeEventListener('resize', this.handleResize)
}
render () {
return (
<Component
{...this.props}
viewport={this.state.viewport}
/>
)
}
}
}
export default getDimensions
It works as expected but i keep getting the warning that:
does anyone knows what is going on?
please let me know
keep in mind you are not removing the event. if (window) window.addEventListener('resize', debounce(this.handleResize, 400)) will mutate the function and return a wrapped function, the removal of the event just passes the original this.handleResize, which won't be found.
you need to this.handleResize = debounce(this.handleResize, 400) in the constructor.
tl;dr: component will unmount but event will continue firing.

Resources