React Scroll Animation - reactjs

I have multiple pages in my react app and I want to set screen view to the page that is most visible when i scroll, just like what happen in tesla.com. This is my code if someone can help;
import React, { useState, useRef, useEffect } from "react";
const Page = ({ children, index }) => {
const ref = useRef(null);
return (
<div ref={ref} style={{ height: "100vh" }}>
{children}
</div>
);
};
const App = () => {
const [activePage, setActivePage] = useState(0);
const pages = [...Array(5)].map((_, index) => (
<Page key={index} index={index}>
Page {index + 1}
</Page>
));
const handleScroll = () => {
const pageRefs = pages.map((page) => page.ref.current);
let maxVisiblePageIndex = 0;
let maxVisiblePageAmount = 0;
pageRefs.forEach((pageRef, index) => {
const { top, bottom } = pageRef.getBoundingClientRect();
const visibleAmount =
top > 0 && bottom < window.innerHeight
? bottom - top
: Math.min(Math.abs(top), Math.abs(bottom));
if (visibleAmount > maxVisiblePageAmount) {
maxVisiblePageIndex = index;
maxVisiblePageAmount = visibleAmount;
}
});
setActivePage(maxVisiblePageIndex);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return <div>{pages}</div>;
};
export default App;
I want it to scroll like it does in Tesla.com

Related

why useRef current value , isn't sharing trough custom hook?

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?
Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

How to scroll into child component in a list from parent in react?

Hello guys I have an issue that may be simple but I'm stuck.
I have a parent that call an endpoint and render a list of child components once the data is received, at the same time in the URL could (or not) exists a parameter with the same name as the "name" property of one of the child components, so if parameter exists I need to scroll the page down until the children component that have the same "name" as id.
Here is part of the code:
const ParentView = () => {
const [wines, setWines] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const params = new URLSearchParams(document.location.search);
const isMx = params.get('lang') ? false : true;
const wineId = params.get('wine');
const ref = createRef();
const scroll = () => ref && ref.current && ref.current.scrollIntoView({ behavior: 'smooth' });
React.useEffect(() => {
retrieveData();
}, []);
React.useEffect(() => {
if (!isEmptyArray(wines) && !loading && wineId) scroll();
}, [wineId, wines, loading]);
function renderWines() {
if (loading) return <Loading />;
if (isEmptyArray(wines) && !loading) return <h2>No items found</h2>;
if (!isEmptyArray(wines) && !loading)
return (
<React.Fragment>
{wines
.filter(p => p.status === 'published')
.map((w, idx) => (
<ChildComponent
wine={w}
isMx={isMx}
idx={idx}
openModal={openModal}
ref={wineId === w.name.toLowerCase() ? ref : null}
/>
))}
</React.Fragment>
);
}
return (
<React.Fragment>
{renderWines()}
</React.Fragment>
);
};
And this is the child component...
import React, { forwardRef } from 'react';
import { Row,} from 'reactstrap';
const WineRow = forwardRef(({ wine, isMx, idx, openModal }, ref) => {
const {
name,
} = wine;
// const ref = React.useRef();
React.useEffect(() => {
// console.log({ ref, shouldScrollTo });
// shouldScrollTo && ref.current.scrollIntoView({ behavior: 'smooth' });
}, []);
return (
<Row id={name} ref={ref}>
...content that is irrelevant for this example
</Row>
);
});
Of course I remove a lot of irrelevant code like retrieveData() function and all the logic to handle the data from api
I've been trying many ways but I can't make it works :(
Well after a headache I just realized that I don't need react to do this 😂
so I just fixit with vanilla js 🤷🏻‍♂️
Parent:
const Public = () => {
const [wines, setWines] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const params = new URLSearchParams(document.location.search);
const isMx = params.get('lang') ? false : true;
const wineId = params.get('wine');
React.useEffect(() => {
retrieveData();
}, []);
React.useEffect(() => {
if (!isEmptyArray(wines) && !loading && wineId) scroll(wineId);
}, [wineId, wines, loading]);
const scroll = wineId => document.getElementById(wineId).scrollIntoView({ behavior: 'smooth' });
const retrieveData = async () => {
....logic to handle data
};
function renderWines() {
if (loading) return <Loading />;
if (isEmptyArray(wines) && !loading) return <h2>No items found</h2>;
if (!isEmptyArray(wines) && !loading)
return (
<React.Fragment>
{wines
.filter(p => p.status === 'published')
.map((w, idx) => (
<WineRow wine={w} isMx={isMx} idx={idx} />
))}
</React.Fragment>
);
}
return (
<React.Fragment>
{renderWines()}
</React.Fragment>
);
};
and children:
const WineRow =({ wine, isMx, idx,}) => {
const {
name,
} = wine;
return (
<Row id={name.toLowerCase()}>
...content that is irrelevant for this example
</Row>
);
};
And that's it 😂 sometimes we are used to do complex things that we forgot our basis 🤦🏻‍♂️
Hope this help someone in the future

React state doesnt update, drilling seems ok though

I've got a modal that I want to be able to auto-shut itself using a drilled function. The console.log does work, but the state isn't actually updating. What am I doing wrong? Triggering the state via the dev tools works fine, so it's not the state itself. Is drilling within a component the problem?
index.js:
export default function Home() {
const [modalOpen, setModalOpen] = useState(false)
const handleModalOpen = () => {
console.log ("Setting modal to true")
setModalOpen (true)
}
const handleModalClose = () => {
console.log ("Setting modal to false")
setModalOpen (false)
}
// all the normal app body
{modalOpen ?
(<Modal handleModalClose={handleModalClose} height='30vh'>
<h4>Thank you for your contact request.</h4>
<h4>Keep in mind that this is only a demo website, not an actual business.</h4>
</Modal>): null}
</div>
)
}
Modal.js:
import { createPortal } from "react-dom";
import { useEffect, useState } from "react";
import styles from '../styles/Modal.module.css'
const Backdrop = (props) => {
return <div onClick={() => props.handleModalClose()} className={styles.backdrop} />
}
const Message = (props) => {
let width = '70vw'
let height = '80vh'
if (props.width) width = props.width
if (props.height) height = props.height
return (
<div style={{ width: width, height: height }} className={styles.message}>
{props.children}
</div>
)
}
const Modal = (props) => {
const [backdropDiv, setBackdropDiv] = useState(null)
const [modalDiv, setModalDiv] = useState(null)
useEffect(() => {
if (typeof (window) !== undefined) {
let backdropDiv = document.getElementById('backdrop')
setBackdropDiv(backdropDiv)
let modalDiv = document.getElementById('modal')
setModalDiv(modalDiv)
}
}, [])
return (
<>
{backdropDiv !== null && modalDiv !== null ? (
<>
{createPortal(<Backdrop handleModalClose = {props.handleModalClose} />, backdropDiv)}
{createPortal(<Message children={props.children} width={props.width} height={props.height} />, modalDiv)}
</>
) : null
}
</>
)
}
export default Modal

useEffect only runs on hot reload

I have a parent/child component where when there is a swipe event occurring in the child the parent component should fetch a new profile. The problem is the useEffect in the child component to set up the eventListeneners currently is not running, only occasionally on hot-reload which in reality should run basically every time.
Child component
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const [swiped, setSwiped] = useState(0)
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
setSwiped((swiped) => swiped +1)
props.parentCallback(swiped);
startX = null
}
}
}
// Following code block does not work
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
Parent component
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback}/>
))}
</div>
);
}
}
If you want the parent components swiped state to change, you need to pass "setSwiped" from the parent to the child compenent. You will also need to pass "swiped" to the child to use its current value to calculate the new value. I'm going to assume you declared the useState in the child component trying to set the parents state of the same name, so I'm going to remove that useState Declaration in the child altogether.
Here's an example of passing the setSwiped method and swiped value to the child:
PARENT
import React, {useState, useEffect, useCallback} from 'react';
import './Index.css';
import Profile from './Profile'
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback} setSwiped={setSwiped} swiped={swiped}/>
))}
</div>
);
}
}
export default Profiles;
CHILD
import React, {useState, useRef, useEffect } from 'react';
import './Index.css';
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
props.setSwiped((props.swiped) => props.swiped +1)
props.parentCallback(props.swiped);
startX = null
}
}
}
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
export default Profile;
I'm hoping I didn't miss anything here.
Best of luck.

Testing slider component in react

I want to test my slider component with react testing library. But I can't comprehend how to test it properly. I want to test changing slide when the user clicks the dot(StyledDotContainer). StyledDotContainer's background is gray but it is red when the active props is true. The component looks like this.
const Slider = ({
children,
autoPlayTime = 5000,
dots = true,
initialIndex= 0
}: SliderProps): React.ReactElement => {
const [activeIndex, setActiveIndex] = useState<number>(initialIndex)
const nextSlide = () => {
const newIndex = activeIndex >= length - 1 ? 0 : activeIndex + 1
setActiveIndex(newIndex)
}
useEffect(() => {
const timer = setTimeout(() => {
nextSlide()
}, autoPlayTime)
return () => clearTimeout(timer)
}, [activeIndex])
const length = useMemo(() => {
return React.Children.count(children)
}, [])
const setSlide = useCallback((index: number) => {
setActiveIndex(index)
}, [])
const value = useMemo(() => ({ activeIndex, setSlide }), [activeIndex])
return (
<SliderContext.Provider value={value}>
<StyledContainer>
{children}
{dots && (
<StyledDotsContainer data-testid="dots">
{[...Array(length)].map((_, index) => {
return (
<StyledDotContainer
data-testid={`dot-${index}`}
key={index}
onClick={() => setSlide(index)}
isActive={index === activeIndex}
/>
)
})}
</StyledDotsContainer>
)}
</StyledContainer>
</SliderContext.Provider>
)
}
Appreciate any suggestion.
It's a bad practice to test the styles of a Component. Instead you just want to test that the function you are trying to test is properly changing the props in your component. Styles should be inspected visually.
import screen from '#testing-library/dom'
import {render, screen} from '#testing-library/react'
import userEvent from '#testing-library/user-event'
describe("Slider", () => {
it('Should be red when slider is active', () => {
render(Slider)
const firstDot = screen.getByTestId('dots-0')
act(() => {
userEvent.click(firstDot)
})
waitFor(() => {
expect(screen.getByTestId('dots-0').props.isActive).toBeTruthy()
})
})
})

Resources