I'm creating a component and I need to get it's parent <div> width and height. I'm using Hooks, so all my components are functions. I've read some examples using classes, but this won't apply to my component.
So I have this component:
export default function PlantationMap(props) {
<div className="stage-canvas">
<Stage
width={window.innerWidth * 0.5}
height={window.innerHeight * 0.5}
onWheel={handleWheel}
scaleX={stage.stageScale}
scaleY={stage.stageScale}
x={stage.stageX}
y={stage.stageY}
draggable
/ >
</div>
}
How could I get the <div> height and width to use in <Stage width={} height={} />?
Thank you very much in advance
Edit: I tried using the useRef() hook, like this:
const div = useRef();
return (
<div ref={div}>
...
</div>
)
But I can't access the div.current object
I think useCallback is what you want to use so you can get the width and height when it changes.
const [height, setHeight] = useState(null);
const [width, setWidth] = useState(null);
const div = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
setWidth(node.getBoundingClientRect().width);
}
}, []);
return (
<div ref={div}>
...
</div>
)
Declare a reference using useRef hook and then read current.offsetHeight and current.offsetWidth properties.
Here is the code:
import React, { useEffect, useRef } from 'react';
const PlantationMap = (props) => {
const stageCanvasRef = useRef(null);
// useEffect will run on stageCanvasRef value assignment
useEffect( () => {
// The 'current' property contains info of the reference:
// align, title, ... , width, height, etc.
if(stageCanvasRef.current){
let height = stageCanvasRef.current.offsetHeight;
let width = stageCanvasRef.current.offsetWidth;
}
}, [stageCanvasRef]);
return(
<div className = "stage-canvas" ref = {stageCanvasRef}>
<Stage
width={window.innerWidth * 0.5}
height={window.innerHeight * 0.5}
onWheel={handleWheel}
scaleX={stage.stageScale}
scaleY={stage.stageScale}
x={stage.stageX}
y={stage.stageY}
draggable
/ >
</div>);
}
export default PlantationMap;
You can make use of the built-in ResizeObserver:
export default function PlantationMap(props) {
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
// Depending on the layout, you may need to swap inlineSize with blockSize
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize
setWidth(event[0].contentBoxSize[0].inlineSize);
setHeight(event[0].contentBoxSize[0].blockSize);
});
resizeObserver.observe(document.getElementById("div1"));
});
return (
<div id="div1" className="stage-canvas">
<Stage
width={width * 0.5}
height={height * 0.5}
onWheel={handleWheel}
scaleX={stage.stageScale}
scaleY={stage.stageScale}
x={stage.stageX}
y={stage.stageY}
draggable
/ >
</div>
);
}
I think ResizeObserver is the way to go as mentioned in the answer from Dan.
I just wouldn't use the document.getElementById. Either use useMeasure from react-use or create everything on your own.
There are two scenarios:
Component contains the container that you'd like to observe
Component is a child component and doesn't have the container reference
To 1 - Reference directly accessible
In this case, you can create the reference with useRef in the component and use it at resizeObserver.observe(demoRef.current).
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
const DisplaySize = ({ width, height }) => (
<div className="centered">
<h1>
{width.toFixed(0)}x{height.toFixed(0)}
</h1>
</div>
);
const Demo = () => {
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const demoRef = useRef();
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
// Depending on the layout, you may need to swap inlineSize with blockSize
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize
setWidth(event[0].contentBoxSize[0].inlineSize);
setHeight(event[0].contentBoxSize[0].blockSize);
});
if (demoRef) {
resizeObserver.observe(demoRef.current);
}
}, [demoRef]);
return (
<div ref={demoRef} className="App">
<DisplaySize width={width} height={height} />
</div>
);
}; //);
export default function App() {
return <Demo />;
}
To 2 - Reference of container not directly accessible:
This case is probably happening more often and requires slightly more code.
You need to pass the reference from the parent to the child component with React.forwardRef.
Demo code can be found below or in the following Codesandbox
Some words to the code:
In the parent component you create a reference with const containerRef = useRef() and use it at the main container with <div ref={containerRef}/>. Under the hood it will do something like ref => containerRef.current=ref
Next, pass the reference to the Demo component.
Why not use React.createRef?
That would work too but it would recreate the reference on every render of your App. Please have a look here for an explanation of the difference between useRef and createRef.
In short, use useRef with functional components and use createRef with class-based components.
const {useEffect, useRef, useState} = React;
const DisplaySize = ({ width, height }) => (
<div className="centered">
<h1>
{width.toFixed(0)}x{height.toFixed(0)}
</h1>
</div>
);
const Demo = React.forwardRef((props, ref) => {
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
// Depending on the layout, you may need to swap inlineSize with blockSize
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize
setWidth(event[0].contentBoxSize[0].inlineSize);
setHeight(event[0].contentBoxSize[0].blockSize);
});
if (ref && ref.current) {
resizeObserver.observe(ref.current);
}
}, [ref]);
return <DisplaySize width={width} height={height} />;
});
function App() {
const containerRef = useRef();
return (
<div ref={containerRef} className="App">
<Demo ref={containerRef} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
/* apply a natural box layout model to all elements, but allowing components to change */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html,
body {
margin: 0;
padding: 0;
}
.App {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: sans-serif;
text-align: center;
border: 4px solid red;
}
.centered {
display: flex; /* establish flex container */
flex-direction: column; /* make main axis vertical */
justify-content: center; /* center items vertically, in this case */
align-items: center; /* center items horizontally, in this case */
height: 100%;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Library React-use
There are also some useful hooks in React-use that could help here.
useWindowSize and useSize look pretty similar but after looking at the source code the first one relies on the window.onresize event and requires less code to implement.
useSize will add an iframe below the current component (z-index: -1) to track the size with resize event and requires more code. It also adds a little debounce with setTimeout.
So use useWindowSize if you just need the width/height to do some calculations on the first render and useSize if you'd like to show that the size changed.
useWindowSize
If you just need to get the window size useWindowSize is the way to go.
They're doing it with onresize event with document.addEventlistener('resize', resizeHandler) and checking innerWidth / innerHeight
Codesandbox Demo
useMeasure
To track an element size, useMeasure can be used. It is using ResizeObserver under the hood, so it's like the code above where ref is the reference you'd like to track:
The first element returned by useMeasure is the setRef method.
So you can do the following in your component:
const [setRef, { width, height }] = useMeasure();
useEffect(() => {
setRef(ref.current)
}, [])
Please have a look at the following Codesandbox.
useSize
If you want to track the size of a component useSize could be used as mentioned in the docs.
Codesandbox Demo useSize
to my knowledge if it is concerned with style can only be registered by:
<Stage style={{width:window.innerWidth * 0.5,height:width:window.innerWidth * 0.5}} />
Related
I'm working with styled-components in atomic system.
I have my <Atom title={title} onClick={onClick}/>
And I have molecule that extends that atom, by just adding some functionality to it without wrapping element:
const Molecule = ({}) => {
const [title, setTitle] = useState('Base title')
const onClick = () => {
// some actions
}
useEffect(()=>{
setTitle('New title')
},[conditions changed])
return(
<Atom title={title} onClick={onClick}/>
)
}
I have base styling in <Atom/> but I would like to add some more to it in <Molecule/>. Is it possible to do it without creating extra wrapper?
It is possible, but the question is if it's worthy the effort - the most anticipated way would be to do it as the documentation says - to wrap the styled component and extend the styles (but this is what you want to avoid). So either:
you could assign a className to Atom, so you can adjust/overwrite the styles with CSS
pass the extraStyles props to Atom and then pass to the styled component and just use inside after the previous, default styles to overwrite them
or either pass some extraStyles as CSSProperties object and just use them as inline styling.
https://codesandbox.io/s/withered-leftpad-znip6b?file=/src/App.js:64-545
/* styles.css */
.extraClass {
color: green;
}
const AtomStyled = styled.div`
font-size: 17px;
color: blue;
font-weight: 600;
`;
const Atom = ({ children, className, extraStyles }) => {
return (
<AtomStyled style={extraStyles} className={className}>
{children}
</AtomStyled>
);
};
const Molecule = () => {
return (
<Atom className={'extraClass'} extraStyles={{ fontSize: 27 }}>
Atom
</Atom>
);
};
I have an Accordion component which his children can change his height dynamically (by API response), I try this code but not working because the height changes only if I close and re-open the accordion. The useEffect not triggering when children DOM change. Can anyone help me? Thanks
export const VerticalAccordion = (props) => {
const accordionContainerRef = useRef<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState<number | undefined>(0);
const [animationClass, setAnimationClass] = useState<'animated'>();
const [overflow, setOverflow] = useState<'visible' | 'hidden'>('visible');
const [isOpen, setIsOpen] = useState<boolean>(true);
const {title, children} = props;
useEffect(() =>{
if(accordionContainerRef.current){
const height = isOpen ? accordionContainerRef.current.scrollHeight: 0;
setContentHeight(height);
if(isOpen){
// delay need for animation
setTimeout(() => setOverflow('visible'),700);
return;
}
return setOverflow('hidden')
}
}, [isOpen, accordionContainerRef, children]);
const onAccordionClick = () => {
setAnimationClass('animated');
setIsOpen(prevState => !prevState)
};
return (
<div className={'accordion'}>
<div className={`header`}>
<div className={`header-title`}>{title}</div>
<MyIcon onClick={() => onAccordionClick()}
customClass={`header-arrow`}
path={menuDown}
size={20}/>
</div>
<div ref={accordionContainerRef}
style={{ height: `${contentHeight}px`, overflow}}
className={`data` + (animationClass ? ` data--${animationClass}` : '')}>
{children}
</div>
</div>
)
}
Even though the working around what I have found is not a robust one, but it is working completely fine for me. If someone stumbles upon this same issue might find this useful.
const [activeState, setActiveState] = useState("");
const [activeHeight, setActiveHeight] = useState("0px");
const contentRef = useRef(null)
const toogleActive = () => {
setActiveState(activeState === "" ? "active" : "");
setActiveHeight(activeState === "active" ? "0px" :`${contentRef.current.scrollHeight + 100}px`)
}
return (
<div className={styles.accordion_section}>
<button className={styles.accordion} onClick={toogleActive}>
<p className={styles.accordion_title}>{title}</p>
</button>
<div ref={contentRef}
style={{ maxHeight: `${activeHeight}` }}
className={styles.accordion_content}>
<div>
{content}
</div>
</div>
</div>
)
I have hardcoded some extra space so that while the dynamic response is accepted the Child DOM is shown. In the CSS module file, I have kept the overflow as auto, earlier it was hidden.
.accordion_content {
background-color: white;
overflow: auto;
max-height: max-content;
}
As a result, the Child DOM is appearing dynamically and the user can scroll inside the Accordion if the Child DOM needs larger space.
If you want to use state hooks it is better to use context, so that the hook will be shared between the child and parent. This link might give you an idea of how does context works.
Here is how I am solving a similar problem. I needed an accordion that
Could be opened/closed depending on external variable
Could be opened/closed by clicking on the accordion title
Would resize when the contents of the accordion changes height
Here's the accordion in its parent component. Also, the useState hook for opening/closing the accordion from outside the accordion (as necessary).
const [isOpen, setOpen] = useState(false)
// some code...
<Accordion title={"Settings"} setOpen={setOpen} open={isOpen}>
{children}
</Accordion>
Here's the accordion component. I pass the setOpen hook from the parent to the accordion. I use useEffect to update the accordion's properties when either open or the children change.
import React, { useEffect, useState, useRef } from "react"
import styled from "styled-components"
const AccordionTitle = styled.span`
cursor: pointer;
`
const AccordionContent = styled.div`
height: ${({ height }) => height}px;
opacity: ${({ height }) => (height > 0 ? 1 : 0)};
overflow: hidden;
transition: 0.5s;
`
export const Accordion = ({ title, setOpen, open, children }) => {
const content = useRef(null)
const [height, setHeight] = useState(0)
const [direction, setDirection] = useState("right")
useEffect(() => {
if (open) {
setHeight(content.current.scrollHeight)
setDirection("down")
} else {
setHeight(0)
setDirection("right")
}
}, [open, children])
return (
<>
<h3>
<AccordionTitle onClick={(e) => setOpen((prev) => !prev)}>
{title}
<i className={`arrow-accordion ${direction}`}></i>
</AccordionTitle>
</h3>
<AccordionContent height={height} ref={content}>
{children}
</AccordionContent>
</>
)
}
I have the following Redux Reducer to handle the offset of an infinite scroll component:
const offset = handleActions(
{
[questionListTypes.ON_QUESTIONS_SCROLL]: state => state + QuestionsLoadChunkTotal,
[combineActions(questionListTypes.RESET_QUESTIONS_OFFSET)]: () => {
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
return 0;
},
},
0,
);
When the offset of the component resets I want to scroll the HTML element to the top. I have added the following line in the reducer to handle this:
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
This doesn't feel right to me to put it here because it has nothing to do with my state. Is there a better way to handle this situation?
You may use a Redux middleware, which purpose is to handle side effects.
It receives every action that goes through and enables us to have any side effect.
const scrollReseter = store => next => action => {
next(action);
if (action.type === combineActions(questionListTypes.RESET_QUESTIONS_OFFSET)) {
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
}
}
See https://redux.js.org/advanced/middleware/
You can use a ref to get a reference to the DOM element and use an effect to manipulate that element when a certain value in the state changes.
Here is an example using local state:
const App = () => {
//this would be data that comes from state
// maybe with useSelector or with connect
const [scrollToTop, setScrollToTop] = React.useState(0);
//create a ref to the element you want to scroll
const scrollRef = React.useRef();
//this would be an action that would set scrollToTop with a new
// value
const goToTop = () => setScrollToTop((val) => val + 1);
//this is an effect that runs every time scrollToTop changes
// it will run on mount as well so when scrollToTop is 0 it
// does nothing
React.useEffect(() => {
if (scrollToTop) {
scrollRef.current.scrollTop = 0;
}
}, [scrollToTop]);
return (
<div
ref={scrollRef}
style={{ maxHeight: '250px', overflow: 'scroll' }}
>
{[...new Array(10)].map((_, key) => (
<h1
key={key}
onClick={goToTop}
style={{ cursor: 'pointer' }}
>
click to scroll to top
</h1>
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is one of the first times I am actually using React Hooks properly in a project so bear with me if I am not quite there.
In the component below, my aim is to display the <HelperTooltip> on load and when the scrolling div (not the window) scrolls I want to hide after it scrolls X amount of pixels.
My thought process is to create a useRef object on the scrolling <div/> element, which then I can add an event listens with a callback function which then can toggle the state to hide the <HelperTooltip>
I have created a Codesandbox below to try and demonstrate what I am trying to do. As you can see in the demo the node.addEventListener('click') is working fine, however when I try and call the node.addEventListener('scroll') it is not firing.
I'm not sure if I taking the wrong approach or not, any help will greatly be appreciated. In the codesandbox demo it is the react image that I trying to hide on scroll, not the <HelperTooltip>
CodeSandbox link: https://codesandbox.io/s/zxj322ln24
import React, { useRef, useCallback, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = props => {
const [isLogoActive, toggleLogo] = useState(true);
const scrollElementRef = useCallback(node => {
node.addEventListener("click", event => {
console.log("clicked", event);
});
/*
I want to add the scroll event listener
here and the set the state isLogoActive to
false like the event listener above but the 'scroll' event
is firing --- see below on line 21
*/
// node.addEventListener("scroll", event => {
// console.log("scrolled", event);
// toggle log
// });
});
return (
<div className="scrolling-container">
<div ref={scrollElementRef} className="scrolling-element">
<p>top</p>
{isLogoActive && (
<div className="element-to-hide-after-scroll">
<img
style={{ width: "100px", height: "100px" }}
src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
/>
</div>
)}
<p>bottom</p>
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
An easier approach for this particular use case might be to use the onScroll prop and use the scrollTop property from the event target to figure out if you should hide the image or not.
Example
const { useState } = React;
const App = props => {
const [isLogoActive, setLogoActive] = useState(true);
const onScroll = e => {
setLogoActive(e.target.scrollTop < 100);
};
return (
<div onScroll={onScroll} style={{ height: 300, overflowY: "scroll" }}>
<p style={{ marginBottom: 200 }}>top</p>
<img
style={{
width: 100,
height: 100,
visibility: isLogoActive ? "visible" : "hidden"
}}
src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
/>
<p style={{ marginTop: 200 }}>bottom</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Here is the correct way to bind the addEventListener on div using useRef()
import React, { useState, useEffect, useCallback, useRef } from 'react';
function ScrollingWrapper(props) {
const [hasScrolledDiv, setScrolled] = useState(false);
const scrollContainer = useRef(null);
const onScroll = useCallback((event) => {
if(event.target.scrollTop > 125){
setScrolled(true);
} else if(event.target.scrollTop < 125) {
setScrolled(false);
}
}, []);
useEffect(() => {
scrollContainerWrapper.current.addEventListener('scroll', onScroll);
return () => scrollContainerWrapper.current.removeEventListener('scroll', onScroll);
},[]);
return (
<div ref={scrollContainerWrapper}>
{props.children}
</div>
);
}
export default ScrollingWrapper;
Depending on your use case, it's usually also good to throttle scroll event listeners, so they don't run on every pixel change.
const App = props => {
const [isLogoActive, setLogoActive] = useState(true);
const onScroll = useMemo(() => {
const throttled = throttle(e => setLogoActive(e.target.scrollTop < 100), 300);
return e => {
e.persist();
return throttled(e);
};
}, []);
return (
<div onScroll={onScroll}>
<img
style={{ visibility: isLogoActive ? 'visible' : 'hidden' }}
src="https://arcweb.co/wp-content/uploads/2016/10/react-logo-1000-transparent-768x768.png"
/>
</div>
);
};
The throttle function is available in lodash.
In your example, the scroll is not triggered on the scrolling-element but on the scrolling-container so that's where you want to put your ref : https://codesandbox.io/s/ko4vm93moo :)
But as Throlle said, you could also use the onScroll prop !
I'm trying to migrate my app from old one to the new React-trancition-group API, but it's not so easy in case of using the manual <Transition> mode for transition creation of the particular React component.
My animation logic is:
we have an array of the components, each child of him comes one by one in the <TransitionGroup> API by onClick action. Where every new income component smoothly replace and hide previous one, which is already present in <Transition> API.
I almost finish unleash this tangle in react-trancition-group .v2, but one thing is still not solved - the component, that already been in <Transition> API does not disappear after the new one is overlayed him, which was automatically happen in react-trancition-group .v1 instead. So now they all just stack together...
So, maybe you can look on my code and luckly say where is my problem located...
I'll be grateful for any help. Thanks for your time
My code:
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import { TweenMax, Power1 } from 'gsap'
import { Transition } from 'react-transition-group'
class Slider extends Component {
_onEntering = () => {
const { id, getStateResult, isCurrentRow, removeAfterBorder, calcHeight } = this.props
const el = findDOMNode(this)
const height = calcHeight(el)
TweenMax.set(el.parentNode, {
height: `${height}px`
})
TweenMax.fromTo(
el,
0.5,
{
y: -120,
position: 'absolute',
width: `${100}%`,
zIndex: 0 + id
},
{
y: 0,
width: `${100}%`,
zIndex: 0 + id,
ease: Power1.easeIn
}
)
}
_onEntered = () => {
const { activeButton, removeAfterBorder, getCurrentOutcome } = this.props
findDOMNode(this)
}
_onExiting = () => {
const el = findDOMNode(this)
TweenMax.to(el, 2, {
onComplete: () => {
el.className = ''
}
})
}
_onExited = () => {
const { getStateResult } = this.props
getStateResult(true)
}
render() {
const { children } = this.props
return (
<Transition
in={true}
key={id}
timeout={2000}
onEntering={() => this._onEntering()}
onEntered={() => this._onEntered()}
onExiting={() => this._onExiting()}
onExited={() => this._onExited()}
unmountOnExit
>
{children}
</Transition> || null
)
}
}
export default Slider
```
So, you problem is very typically. As you written here: the component, that already been in <Transition> API does not disappear after the new one is overlayed him - it's happen because you does not changing the status flag in for Transition Component. You set always true for him, it's not a right.
By the way, you need to understand what is you trying to do in your code. There is a big difference between methods <Transition></Transition> and <TransitionGroup><CSSTransition></CSSTransition><TransitionGroup>. You need to use pure <Transition></Transition> API only for very rare cases, when you need explicitly manipulate animation scenes.
As I see in you code you trying to replace one component by the other and in this case you need to use the second method that I have provided above.
So, try this:
import { TransitionGroup, CSSTransition } from 'react-transition-group'
...some code
return (
<TransitionGroup>
<CSSTransition
key={id}
timeout={2000}
onEntering={() => this._onEntering()}
onEntered={() => this._onEntered()}
onExiting={() => this._onExiting()}
onExited={() => this._onExited()}
unmountOnExit
>
{children}
</CSSTransition> || null
</TransitionGroup>
)
It should help you and start to render Compenents by normal overlaying each other.