Creating an Observer component in React - Passing props to the children - javascript

I'm looking to build an intersection observer component in a personal Gatsby project. The reason for it is the animation and trigger is the same in different areas of the site. What I have so far:
// Observer.js
import React, { useEffect, useRef } from "react";
import { useAnimation } from "framer-motion";
const Observer = ({ children }) => {
const controls = useAnimation();
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
controls.start({
opacity: 1,
transition: { duration: 0.75, delay: 0.75 },
});
}
},
{
root: null,
rootMargin: "0px",
threshold: 0.1,
}
);
if (ref.current) {
observer.observe(ref.current);
}
}, [ref]);
return <div ref={ref}>{children}</div>;
};
export default Observer;
What I am looking to do is access controls in the children of the Observer.js component.
// ChildComponent.js
const ChildComponent= (props) => {
return (
<Observer>
<div>
<motion.div
initial={{ opacity: 0 }}
animate={props.controls}
>
<h2>This will animate on scroll</h2>
</motion.div>
</div>
</Observer>
);
};
export default ChildComponent;
I have seen answers such as - Pass props from layout to children in Gatsby - but I get the error props.children is not a function when attempting this.
Is what I am looking to do possible, or is there a better way of achieving this?

Related

Intersection Observer in Reactjs

Can someone help me how can I do this in Reactjs?
Can I iterate one IntersectionObserver for multiple child in reactjs
const faders = document.querySelectorAll('.fade-in');
const appearOptions = {
threshold: 1
};
const appearOnScroll = new IntersectionObserver( function(entries, appearOnScroll){
entries.forEach(entry => {
if(!entry.isIntersecting){
return;
}else{
entry.target.classList.add('appear')
appearOnScroll.unobserve(entry.target)
}
})
}, appearOptions);
faders.forEach(fader =>{
appearOnScroll.observe(fader)
})
useIntersectionObserver.js
Use this custom hook. It creates an IntersectionObserver instance and saves it in a useRef hook. It tracks the elements that are being observed in a state. Whenever the state changes, it unobserves all elements and then reobserves the elements that remain in the state.
The advantages of creating a custom hook is that you can reuse the hook and implement it on multiple occasions.
import { useRef, useState, useEffect } from 'react';
const useIntersectionObserver = ({ root = null, rootMargin = '0px', threshold = 0 }) => {
const [entries, setEntries] = useState([]);
const [observedNodes, setObservedNodes] = useState([]);
const observer = useRef(null);
useEffect(() => {
if (observer.current) {
observer.current.disconnect();
}
observer.current = new IntersectionObserver(entries => setEntries(entries), {
root,
rootMargin,
threshold
});
const { current: currentObserver } = observer;
for (const node of observedNodes) {
currentObserver.observe(node);
}
return () => currentObserver.disconnect();
}, [observedNodes, root, rootMargin, threshold]);
return [entries, setObservedNodes];
};
export default useIntersectionObserver;
app.js
Use the hook where you need to observe your elements. Create references to the elements that you need to observe and pass them to the hook after the first render.
The entries state will contain an array of IntersectionObserverEntry objects. Loop over it whenever the entries state changes and assert your logic, like adding a class.
import { useRef, useEffect } from 'react';
import useIntersectionObserver from './useIntersectionObserver';
function App() {
const targets = useRef(new Set());
const [entries, setObservedNodes] = useIntersectionObserver({
threshold: 1
});
useEffect(() => {
setObservedNodes(() => ([...targets.current]));
}, [setObservedNodes]);
useEffect(() => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('appear');
setObservedNodes(observedNodes =>
observedNodes.filter(node => node !== entry.target)
);
}
}
}, [entries, setObservedNodes]);
return (
<>
<div className="fade-in" ref={element => targets.current.add(element)}></div>
<div className="fade-in" ref={element => targets.current.add(element)}></div>
</>
)
}

el.getBoundingClientRect is not a function in react odometer js with react visibility sensor?

I am trying to use react-odometer js with react-visibility-sensor in next js. Here I am getting one err like the image? How can I get rid of this error, Experts please help.
Here is my code https://codesandbox.io/s/summer-dream-ysi00
import { useState, useEffect } from "react";
import dynamic from "next/dynamic";
import "odometer/themes/odometer-theme-default.css";
const Odometer = dynamic(import("react-odometerjs"), {
ssr: false,
loading: () => 0
});
import VisibilitySensor from "react-visibility-sensor";
export default function IndexPage() {
const [odometerValue, setOdometerValue] = useState(0);
const [view, setView] = useState(false);
const onVisibilityChange = (isVisible) => {
if (isVisible) {
setView(true);
}
};
useEffect(() => {
setTimeout(() => {
setOdometerValue(500);
}, 10);
}, []);
return (
<VisibilitySensor onChange={onVisibilityChange} offset={8} delayedCall>
<Odometer
value={view ? odometerValue : 0}
format="(,ddd)"
theme="default"
/>
</VisibilitySensor>
);
}
wrap Odometer in a div, like so
<VisibilitySensor onChange={onVisibilityChange} offset={8} delayedCall>
<div>
<Odometer
value={view ? odometerValue : 0}
format="(,ddd)"
theme="default"
/>
</div>
</VisibilitySensor>;
you should see '500' rendered.
https://codesandbox.io/s/wonderful-fast-y3k4s?file=/pages/index.js

Property in Component State not updating quick enough

I'm trying to display text when I hover over an icon, but if you hover the icons quickly it'll get stuck on displaying the text instead of displaying the icon (default state)
ex: https://giphy.com/gifs/UsS4JcRJGV5qfCI5VI
Skills Component:
import React, { useState } from 'react';
import { UserIcon } from './AboutBtnStyling';
import IconText from '../../../IconText';
const AboutBtn = () => {
const [hover, setHover] = useState(false);
const onHover = () => {
setHover(true)
}
const onLeave = () => {
setHover(false)
}
return (
<div onMouseEnter={onHover} onMouseLeave={onLeave} role="button">
{hover ? <IconText text="ABOUT" /> : <UserIcon /> }
</div>
)
}
export default AboutBtn;
Then I hoped converting it to a class component would help, bc of stale closure problem associate with useState hook
import React, { Component } from 'react';
import { SkillIcon } from './SkillsBtnStyling';
import IconText from '../../../IconText';
class SkillsBtn extends Component {
constructor(props) {
super(props);
this. state = { hover: false }
}
onHover = () => {
this.setState({ hover: true })
}
onLeave = () => {
this.setState({ hover: false })
}
render() {
return (
<div onMouseEnter={this.onHover} onMouseLeave={this.onLeave} role="button">
{this.state.hover ? <IconText text="SKILLS" /> : <SkillIcon /> }
</div>
)
}
}
export default SkillsBtn;
Would greatly appreciate any insight! I really want to solve this problem, instead of resorting to achieving this effect using CSS
An important aspect of useState is that it is asynchronous. I believe this is causing your code to act a bit buggy. I would add more decisiveness to your setState calls and set it (true/false) based on mouse position rather than toggle.
import React, { useState } from 'react';
import { SkillsButton } from './SkillsBtnElements'
const SkillsBtn = () => {
const [hover, setHover] = useState(false);
const onHover = () => {
setHover(!hover)
}
return (
<div onMouseEnter={() => setHover(true)} onMouseLeave={() =>
setHover(false)} role="button" tabIndex='-3' >
{ hover ? "SKILLS" : <SkillsButton /> }
</div>
)
}
export default SkillsBtn;

Working with react-spring and styled-components

I can't get react-spring to work. I'm fairly new to this so I have no idea what is going wrong. I'm trying to make navbar appear from top to bottom to 40vh, but it doesn't appear to be recognizing the props passed. I used create-react-app and react-spring 8.0.27
App.js:
const App = () => {
const [open, setOpen] = useState(false);
const navprops = useSpring({
from: {height: "0"},
to: {height: "40vh"}
})
return (
<Fragment>
{open ? <Navbar style={navprops}/> : null}
</Fragment>
Navbar.js:
const NavBar = styled(animated.nav)`
width: 100%;
`;
const Navbar = (props) => {
return (
<NavBar style={props.style}>
</NavBar>
);
};
This is basically the code. There are more style props but I guess it's irrelevant to functionality.
animated and useSpring are imported in both files for testing. Thank you for your help.
Here is my solution,
Demo Link
Navbar.js
import React from "react";
import styled from "styled-components";
import { animated } from "react-spring";
const NavBar = styled(animated.nav)`
width: 100%;
background: red;
`;
export default (props) => {
return <NavBar style={props.style}></NavBar>;
};
App.js
import React, { useState } from "react";
import { useTransition, config } from "react-spring";
import Navbar from "./Navbar";
export default function App() {
const [open, setOpen] = useState(false);
// const navprops = useSpring({
// from: { height: "0" },
// to: { height: "40vh" },
// config: config.wobbly
// });
const transitions = useTransition(open, null, {
initial: { height: "0px" }, //Not required
from: { height: "0px" },
enter: { height: "40vh" },
leave: { height: "0px" },
config: config.wobbly //More configs here https://www.react-spring.io/docs/hooks/api
});
return (
<div className="App">
{transitions.map(
({ item, key, props }) => item && <Navbar key={key} style={props} />
)}
<br />
<br />
<button onClick={() => setOpen(!open)}>Toggle Navbar</button>
</div>
);
}
I do not think useSpring will work on unmounted component. You were trying to animate an unmounted component.
According to documentation, useTransition can be used to animate mounting of unmounted components.
The syntax is little complicated, but they have made the syntax simpler in version 9(release candidate) of react-spring Link Here

Toggle Class based on scroll React JS

I'm using bootstrap 4 nav bar and would like to change the background color after ig 400px down scroll down. I was looking at the react docs and found a onScroll but couldn't find that much info on it. So far I have...
I don't know if I'm using the right event listener or how to set the height etc.
And I'm not really setting inline styles...
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = { scrollBackground: 'nav-bg' };
this.handleScroll = this.handleScroll.bind(this);
}
handleScroll(){
this.setState ({
scrollBackground: !this.state.scrollBackground
})
}
render() {
const scrollBg = this.scrollBackground ? 'nav-bg scrolling' : 'nav-bg';
return (
<div>
<Navbar inverse toggleable className={this.state.scrollBackground}
onScroll={this.handleScroll}>
...
</Navbar>
</div>
);
}
}
export default App;
For those of you who are reading this question after 2020, I've taken #glennreyes answer and rewritten it using React Hooks:
const [scroll, setScroll] = useState(0)
useEffect(() => {
document.addEventListener("scroll", () => {
const scrollCheck = window.scrollY < 100
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
})
})
Bear in mind that, useState has an array of two elements, firstly the state object and secondly the function that updates it.
Along the lines, useEffect helps us replace componentDidmount, the function written currently does not do any clean ups for brevity purposes.
If you find it essential to clean up, you can just return a function inside the useEffect.
You can read comprehensively here.
UPDATE:
If you guys felt like making it modular and even do the clean up, you can do something like this:
Create a custom hook as below;
import { useState, useEffect } from "react"
export const useScrollHandler = () => {
// setting initial value to true
const [scroll, setScroll] = useState(1)
// running on mount
useEffect(() => {
const onScroll = () => {
const scrollCheck = window.scrollY < 10
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
}
// setting the event handler from web API
document.addEventListener("scroll", onScroll)
// cleaning up from the web API
return () => {
document.removeEventListener("scroll", onScroll)
}
}, [scroll, setScroll])
return scroll
}
Call it inside any component that you find suitable:
const component = () => {
// calling our custom hook
const scroll = useScrollHandler()
....... rest of your code
}
One way to add a scroll listener is to use the componentDidMount() lifecycle method. Following example should give you an idea:
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
state = {
isTop: true,
};
componentDidMount() {
document.addEventListener('scroll', () => {
const isTop = window.scrollY < 100;
if (isTop !== this.state.isTop) {
this.setState({ isTop })
}
});
}
render() {
return (
<div style={{ height: '200vh' }}>
<h2 style={{ position: 'fixed', top: 0 }}>Scroll {this.state.isTop ? 'down' : 'up'}!</h2>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This changes the Text from "Scroll down" to "Scroll up" when your scrollY position is at 100 and above.
Edit: Should avoid the overkill of updating the state on each scroll. Only update it when the boolean value changes.
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > specify_height_you_want_to_change_after_here);
});
}, []);
Then you can change your class or anything according to scroll.
<nav className={scroll ? "bg-black" : "bg-white"}>...</nav>
It's Better
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isTop: true
};
this.onScroll = this.onScroll.bind(this);
}
componentDidMount() {
document.addEventListener('scroll', () => {
const isTop = window.scrollY < 100;
if (isTop !== this.state.isTop) {
this.onScroll(isTop);
}
});
}
onScroll(isTop) {
this.setState({ isTop });
}
render() {
return (
<div style={{ height: '200vh' }}>
<h2 style={{ position: 'fixed', top: 0 }}>Scroll {this.state.isTop ? 'down' : 'up'}!</h2>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This is yet another take / my take on hooks approach for on scroll displaying and hiding of a random page element.
I have been very much inspired from: Dan Abramov's post here.
You can check a full working example, in this CodeSandbox demo.
The following is the code for the useScroll custom hook:
import React, { useState, useEffect } from "react";
export const useScroll = callback => {
const [scrollDirection, setScrollDirection] = useState(true);
const handleScroll = () => {
const direction = (() => {
// if scroll is at top or at bottom return null,
// so that it would be possible to catch and enforce a special behaviour in such a case.
if (
window.pageYOffset === 0 ||
window.innerHeight + Math.ceil(window.pageYOffset) >=
document.body.offsetHeight
)
return null;
// otherwise return the direction of the scroll
return scrollDirection < window.pageYOffset ? "down" : "up";
})();
callback(direction);
setScrollDirection(window.pageYOffset);
};
// adding and cleanning up de event listener
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
};
And this hook will be consumed like this:
useScroll(direction => {
setScrollDirection(direction);
});
A full component using this custom hook:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import CustomElement, { useScroll } from "./element";
import Scrollable from "./scrollable";
function Page() {
const [scrollDirection, setScrollDirection] = useState(null);
useScroll(direction => {
setScrollDirection(direction);
});
return (
<div>
{/* a custom element that implements some scroll direction behaviour */}
{/* "./element" exports useScroll hook and <CustomElement> */}
<CustomElement scrollDirection={scrollDirection} />
{/* just a lorem ipsum long text */}
<Scrollable />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Page />, rootElement);
And lastly the code for CustomElement:
import React, { useState, useEffect } from "react";
export default props => {
const [elementVisible, setElementVisible] = useState(true);
const { scrollDirection } = props;
// when scroll direction changes element visibility adapts, but can do anything we want it to do
// U can use ScrollDirection and implement some page shake effect while scrolling
useEffect(() => {
setElementVisible(
scrollDirection === "down"
? false
: scrollDirection === "up"
? true
: true
);
}, [scrollDirection]);
return (
<div
style={{
background: "#ff0",
padding: "20px",
position: "fixed",
width: "100%",
display: `${elementVisible ? "inherit" : "none"}`
}}
>
element
</div>
);
};
I have changed #PouyaAtaei answer a bit for my use case.
import { useState, useEffect } from "react"
// Added distance parameter to determine how much
// from the top tell return value is updated.
// The name of the hook better reflects intended use.
export const useHasScrolled = (distance = 10) => {
// setting initial value to false
const [scroll, setScroll] = useState(false)
// running on mount
useEffect(() => {
const onScroll = () => {
// Logic is false tell user reaches threshold, then true after.
const scrollCheck = window.scrollY >= distance;
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
}
// setting the event handler from web API
document.addEventListener("scroll", onScroll)
// cleaning up from the web API
return () => {
document.removeEventListener("scroll", onScroll)
}
}, [scroll, setScroll])
return scroll
}
Calling the hook:
const component = () => {
// calling our custom hook and optional distance agument.
const scroll = useHasScrolled(250)
}
These are two hooks - one for direction (up/down/none) and one for the actual position
Use like this:
useScrollPosition(position => {
console.log(position)
})
useScrollDirection(direction => {
console.log(direction)
})
Here are the hooks:
import { useState, useEffect } from "react"
export const SCROLL_DIRECTION_DOWN = "SCROLL_DIRECTION_DOWN"
export const SCROLL_DIRECTION_UP = "SCROLL_DIRECTION_UP"
export const SCROLL_DIRECTION_NONE = "SCROLL_DIRECTION_NONE"
export const useScrollDirection = callback => {
const [lastYPosition, setLastYPosition] = useState(window.pageYOffset)
const [timer, setTimer] = useState(null)
const handleScroll = () => {
if (timer !== null) {
clearTimeout(timer)
}
setTimer(
setTimeout(function () {
callback(SCROLL_DIRECTION_NONE)
}, 150)
)
if (window.pageYOffset === lastYPosition) return SCROLL_DIRECTION_NONE
const direction = (() => {
return lastYPosition < window.pageYOffset
? SCROLL_DIRECTION_DOWN
: SCROLL_DIRECTION_UP
})()
callback(direction)
setLastYPosition(window.pageYOffset)
}
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
})
}
export const useScrollPosition = callback => {
const handleScroll = () => {
callback(window.pageYOffset)
}
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
})
}
how to fix :
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
MenuNews
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > specify_height_you_want_to_change_after_here);
});
}, []);
Approach without scroll event listener
import { useEffect, useState } from "react";
interface Props {
elementId: string;
position: string;
}
const useCheckScrollPosition = ({ elementId, position }: Props) => {
const [isOverScrollPosition, setIsOverScrollPosition] = useState<boolean>(false);
useEffect(() => {
if (
"IntersectionObserver" in window &&
"IntersectionObserverEntry" in window &&
"intersectionRatio" in window.IntersectionObserverEntry.prototype
) {
const observer = new IntersectionObserver((entries) => {
setIsOverScrollPosition(entries[0].boundingClientRect.y < 0);
});
const flagElement = document.createElement("div");
flagElement.id = elementId;
flagElement.className = "scroll-flag";
flagElement.style.top = position;
const container = document.getElementById("__next"); // React div id
const oldFlagElement = document.getElementById(elementId);
if (!oldFlagElement) container?.appendChild(flagElement);
const elementToObserve = oldFlagElement || flagElement;
observer.observe(elementToObserve);
}
}, [elementId, position]);
return isOverScrollPosition;
};
export default useCheckScrollPosition;
and then you can use it like this:
const isOverScrollPosition = useCheckScrollPosition({
elementId: "sticky-header",
position: "10px",
});
isOverScrollPosition is a boolean that will be true if you scroll over position provided value (10px) and false if you scroll below it.
This approach will add a flag div in react root.
Reference: https://css-tricks.com/styling-based-on-scroll-position/

Categories