React - Carousel with transitions between slides, react-spring - javascript

I've currently made a carousel component, I'm passing my data to it but I would like to add a transition between switching the data.
I've heard a lot of people recommending react-spring so I thought I'd give it a try. I'm currently trying to implement the useTransition hook, but it's not working as desired.
I simply want to fade in/out only the data when the user presses the "left" / "right" buttons. Currently it starts to fade my content, but then creates a clone of my carousel, and doesn't fade... I'm not sure what I'm doing wrong or if this is the correct hook I should be using?
Ultimately I would prefer a stagger animation when the content fades
in with some delay between each element. For example: the title, then price, then
desciption, etc, would fade in but with a 300ms delay between each
element.
Here's what I currently have:
const mod = (n: any, m: any) => ((n % m) + m) % m;
const CarouselContainer: React.FC = () => {
const [index, setIndex] = useState(0);
const handlePrev = useCallback(() => {
setIndex(state => mod(state - 1, data.length));
}, [setIndex]);
const handleNext = useCallback(() => {
setIndex(state => mod(state + 1, data.length));
}, [setIndex]);
const transitions = useTransition([index], (item: number) => item, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<>
{transitions.map(({ item, props, key }) => (
<Carousel
prevClick={handlePrev}
icon={data[item].icon}
title={data[item].title}
price={data[item].price}
description={data[item].description}
href={data[item].href}
nextClick={handleNext}
style={props}
key={key}
/>
))}
</>
);
};
There's quite a lot of code so I'm providing a CodeSandBox, forks are appretiated! :)

When the mount and unmount happens during the transition, there is a moment when the new and the old element is in the dom simultaneously. Most of the time I use absolute positioning with transition for this reason. This way the change blend in well.
For this you have to change to position: absolute here:
const StyledWrapper = styled.div`
position: absolute;
display: grid;
grid-template-columns: auto auto 20% 20% 1fr auto auto;
grid-template-rows: 3em 1fr auto 1fr 3em;
grid-gap: 0;
`;
I would try to add some movement to it:
const transitions = useTransition([index], (item: number) => item, {
from: { opacity: 0, transform: 'translateX(-420px)' },
enter: { opacity: 1, transform: 'translateX(0px)' },
leave: { opacity: 0, transform: 'translateX(420px)' }
});
And here is your modified code:
https://codesandbox.io/s/empty-rgb-xwqh1
Next you will want to handle the direction of the movement, according to the button pressed. Here is an example for that: https://codesandbox.io/s/image-carousel-with-react-spring-usetransition-q1ndd

Related

Updating State via onClick, in a Component

I'm new to StackOverflow and looking forward to contributing back to the community!
My first question, I am trying to make some squares change color on the screeen, after an onClick event. I'm nearly there, but I keep getting an error when I try to update the state, which then should updates the color. Please could you let me know what I'm doing wrong?
App.js
import React from "react"
import boxes from "./boxes"
import Box from "./Box"
export default function App() {
const [squares, setSquares] = React.useState(boxes)
function changeOn() {
console.log(squares)//just checking I'm getting the full object
setSquares({
id: 1, on: false //this was previously [...prev], on: !squares.on
})
}
const squaresElement = squares.map(props => (
<Box key={props.id} on={props.on} onClick={changeOn} />
))
return (
<main>
{squaresElement}
</main>
)
}
Box.js
import React from "react"
export default function Box (props) {
const styles= props.on ? {backgroundColor: "#222222"} : {backgroundColor: "none"}
return (
<div className="box" style={styles} onClick={props.onClick}></div>
)
}
Boxes.js
export default [
{
id: 1,
on: true
},
{
id: 2,
on: false
},
{
id: 3,
on: true
},
{
id: 4,
on: true
},
{
id: 5,
on: false
},
{
id: 6,
on: false
},
]
I hope somebody can easily spot what's wrong here?
I was expecting to see the color of the top left box change to a different color, after a click.
There are two issues:
setSquares needs the whole array, so you need to give it a new squares array
The styling back to None does not work always. better to give it the white color again
Please find the codesandbox
export default function App() {
const [squares, setSquares] = React.useState(boxes);
function changeOn(id) {
setSquares(
squares.map((square) => {
return { ...square, on: id === square.id ? !square.on : square.on };
})
);
}
const squaresElement = squares.map((props) => (
<Box key={props.id} on={props.on} onClick={() => changeOn(props.id)} />
));
return <main>{squaresElement}</main>;
}
And in Box.js
const styles = props.on
? { backgroundColor: "#222222" }
: { backgroundColor: "#fff" };
You're calling setSquares and passing it a single object instead of an array.
On the next render squares.map(...) blows up because squares is the object, and the object doesn't have a map method.
// after this call squares is just this one object
setSquares({
id: 1, on: false
})
Here's a possible implementation that pushes the on/off responsibility into the box component itself.
// generates a list of items (faking your boxes.js)
const boxes = Array.from({length: 9}, (_, id) => ({ id }));
// container element to render the list
function Boxen ({ items }) {
return (
<div className="container">
{items.map((item, idx) => (
<Box item={item} key={idx} />
))}
</div>
)
}
// component for a single box that can toggle its own on/off state
function Box ({item}) {
const [active, setActive] = React.useState();
return (
<div onClick={() => setActive(!active)} className={active ? 'active' : ''}>{item.id}</div>
)
}
ReactDOM.render(<Boxen items={boxes}/>, document.getElementById('root'));
.container {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 1em;
}
.container > * {
display: flex;
justify-content: center;
align-items: center;
background: skyblue;
}
.container > .active {
background: slateblue;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

React.js - How to check if the div is at the bottom of the screen (dynamically)

Is there any way to check if the div is at the bottom of the another div (acting as a parent, or container).
What I tried
So basically I made demo where there are child elements (items, setItems) in the div that can be added and deleted and also you can change the height of them by clicking on the divs (important here). Also there is another div, which is not in the items state, where I want to change the title of that item, if it is at the bottom of the his parent div (also items have the same parent as this div has).
Problem with my solution
I have tried something where I am looking at the getBoundingClientRect() of the parent container and this "blue" div, lets call it like that, and it will work fine, ONLY IF the items have the same height, but soon as a delete the one item and change the height of it by clicking on the div, it will not work. It will show that it is on the bottom of the screen (the title will be true) but in reality it is not.
My code
App.js - only for demo purposes
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function App() {
const arrayItems = [
{
id: 1,
name: "test",
resized: false
},
{
id: 2,
name: "test1",
resized: false
},
{
id: 3,
name: "test2",
resized: false
}
];
const [items, setItems] = useState(arrayItems);
const [title, setTitle] = useState(false);
const parentRef = useRef(null);
const itemsRef = useRef(null);
useEffect(() => {
if (
parentRef?.current.getBoundingClientRect().bottom -
itemsRef?.current.getBoundingClientRect().height <=
itemsRef?.current.getBoundingClientRect().top
) {
setTitle(true);
} else {
setTitle(false);
}
}, [parentRef, items]);
const handleClick = () => {
const maxValue = Math.max(...items.map((item) => item.id)) + 1;
setItems((prev) => [
...prev,
{ id: maxValue, name: "testValue", resized: false }
]);
};
const handleDelete = () => {
setItems((prev) => prev.slice(0, prev.length - 1));
};
const handleResize = (item) => {
setItems((prev) =>
prev.map((itemOld) => {
if (itemOld.id === item.id) {
return itemOld.resized === true
? { ...itemOld, resized: false }
: { ...itemOld, resized: true };
} else {
return itemOld;
}
})
);
};
console.log(items);
return (
<div className="App">
<button onClick={handleClick}>Add new</button>
<button onClick={handleDelete}>Delete last</button>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div ref={parentRef} className="container">
{items?.map((item) => {
return (
<div
onClick={() => handleResize(item)}
style={{ height: item.resized ? "70px" : "20px" }}
key={item.id}
className="container-item"
>
<p>{item.name}</p>
</div>
);
})}
<div ref={itemsRef} id="title-div">
{title ? "At the bottom" : "Not at the bottom"}
</div>
</div>
</div>
);
}
styles.css
.App {
font-family: sans-serif;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 1rem;
}
* {
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 300px;
background-color: gray;
display: flex;
flex-direction: column;
}
.container-item {
width: 100%;
background-color: hotpink;
}
#title-div {
width: 100%;
padding: 1rem;
background-color: blue;
color: white;
}
What I want to make
As the title suggest I want to see if the div is at the bottom of the container/parent div. That is it, and other items in that parent div, cannot interfere with this div, in sense that adding, resizing, deleting those items, will not suddenly change the position of the div that I want to analyse (to see if it is at the bottom of the screen)
I have come up with my own solution and it works always. I just have to deduct the "top" from parentsRef and "top" from the itemsRef, and add to that the clientHeight of the itemsRef. This way it will always be at the bottom of the container, doesnt matter if I delete the items, resize them etc.
The code
useEffect(() => {
if (
parentRef?.current.clientHeight <=
itemsRef?.current.getBoundingClientRect().top -
parentRef?.current.getBoundingClientRect().top +
itemsRef?.current.clientHeight
) {
setTitle(true);
} else {
setTitle(false);
}
}, [parentRef, items, itemsRef]);

How to pass two state variables into the same React useState? - Array & Integer

Working on an e-commerce store project where I have a Slider component.
I am fetching my own JSON data from a localhost server & mapping over the array for each slide.
My goal is to find a way to use the same useState() to render the items onto the page & click a button to move to the next slide.
I have tried to do something like useState([], 0)
One for my array & another one to change index on button click however this did not work lol...
The array is of course for the data to be displayed however the tricky part for me is figuring out a way to move to the next page.
I am trying to use transform:translateX in my Wrapper styled component and attempting to pass props in so I can change the slide to the next slide and still render the data on the page.
How can I go about using state in this way described above based on my code?
*** Before reading the code snippets, my current code shows that I have tried creating two states, and passing the second state with my integer into my fetch request, no errors pop up but it doesn't work obviously as it doesn't have an array of items to index through.
In the code snippet I have included the code for the entire slider & also the information inside my data.json file.
import {useState, useEffect} from 'react';
import { ArrowLeftOutlined, ArrowRightOutlined } from "#material-ui/icons";
import styled from "styled-components";
const Container = styled.div`
width: 100%;
height: 95vh;
display: flex;
// background-color: #b3f0ff;
position: relative;
overflow: hidden;s
`;
const Arrow = styled.div`
width: 50px;
height: 50px;
background-color: #e6ffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
bottom: 0;
left: ${props => props.direction === "left" && "10px"};
right: ${props => props.direction === "right" && "10px"};
margin: auto;
cursor: pointer;
opacity: 0.5;
z-index: 2;
`;
const Wrapper = styled.div`
height: 100%;
display: flex;
transform: translateX({props => props.arrowIndx * -100}vw);
`
const Slide = styled.div`
width: 100vw;
height: 100vw;
display: flex;
align-items: center;
background-color: ${props => props.bg};
`
const ImgContainer = styled.div`
height: 100%;
flex:1;
`
const Image = styled.img`
padding-left: 30px;
align-items: left;
`
const InfoContainer = styled.div`
height: 80%;
flex:1;
padding: 30px;
`
const Title = styled.h1`
font-size: 50px
`
const Desc = styled.p`
margin: 50px 0px;
font-size: 20px;
font-weight: 500;
letter-spacing: 3px;
`
const Button = styled.button`
padding: 10px;
font-size: 20px;
background-color: transparent;
cursor: pointer;
`
const Slider = () => {
const [slideIndx, setSlideIndx] = useState([]);
const [arrowIndx, setArrowIndx] = useState(0);
const handleClick = (direction) => {
if(direction === "left"){
setArrowIndx(arrowIndx > 0 ? arrowIndx - 1 : 2)
} else{
setArrowIndx(arrowIndx < 2 ? arrowIndx + 1 : 0)
}
}
const fetchSliderItems = () => {
fetch('http://localhost:3000/sliderItems')
.then(resp => resp.json())
.then(data => {
console.log(data)
setArrowIndx(data)
setSlideIndx(data)
})
}
useEffect(() => {fetchSliderItems()}, [])
return (
<Container>
<Arrow direction="left" onClick={() => handleClick("left")}>
<ArrowLeftOutlined />
</Arrow>
<Wrapper arrowIndx={arrowIndx}>
{slideIndx.map((item) => (
<Slide bg={item.bg}>
<ImgContainer>
<Image src={item.img}/>
</ImgContainer>
<InfoContainer>
<Title>{item.title}</Title>
<Desc>{item.desc}</Desc>
<Button>SHOP NOW</Button>
</InfoContainer>
</Slide>
))}
</Wrapper>
<Arrow direction="right" onClick={() => handleClick("right")}>
<ArrowRightOutlined />
</Arrow>
</Container>
)
}
export default Slider
{
"sliderItems": [
{
"id": 1,
"img": "../images/model1.png",
"title": "SPRING CLEANING",
"desc": "DONT MISS OUR BEST COLLECTION YET! USE #FLATIRON10 TO RECEIVE 10% OFF YOUR FIRST ORDER",
"bg": "#b3ecff"
},
{
"id": 2,
"img": "../images/model2.png",
"title": "SHOW OFF HOW YOU DRESS",
"desc": "WITH OUR HUGE SELECTION OF CLOTHES WE FIT ALL YOUR STYLING NEEDS",
"bg": "#ccf2ff"
},
{
"id": 3,
"img": "../images/model3.png",
"title": "POPULAR DEALS",
"desc": "RECEIVE FREE SHIPPING ON ALL ORDERS OVER $50!",
"bg": "#fe6f9ff"
}
]
}
if you want to use 2 data at 1 use state you have 2 way to achieve that
store data as cell of array like below.
const [data,setData] = useState([1,2])
console.log(data[1])
set data as Object in useState
const [data,setData] = useState({data1: 1, data2: 2 })
but i suggest you to use second approach because it's easy to use.
one point you must have care about it in this approach is, if you want to update state with object-state you have to update state with deep copy and then react can re-render component
for example if you want to update data2 in object of state you have to dod this
const [data,setData] = useState({data1: 1, data2: 2 })
setData((prevState) => ({
...prevState,
data2: 'value',
}));
white this snippet you will get update data2 and react get re-render
in your case if you want save array and update it and if you want get re-render when pass new array you have to clone new array and pass it into useState and you can archive this with spread operator in JavaScript.
Example:
setData((prevState) => ({
...prevState,
data2: [...newArray],
}));
if you pass newArray instead of [...newArray] you just pass the reference of memory if array into state and if after setState you change newAereay at the rest of code your state will update to and you don't want this.

Framer Motion (React): How to order initial, exit and layout animations?

I am using framer-motion to animate a change in grid columns.
Here is what I want to do:
I've got nine buttons in a grid (the #db-wrapper is the grid container).
The user switches to the "maximized" view (props.minimize changes) A fourth column gets added to the grid, the buttons which are already there move to their new positions, leaving empty cells for the remaining buttons. Then, new buttons slide in from the bottom, filling the empty cells. The buttons in the grid are now positioned in consecutive cells.
Now the user switches back to the minimized view. First, all the buttons that will get deleted should slide out to the bottom.
Then, the remaining buttons should move to their new positions in the grid (their old positions, like at the beginning), so that they fill consecutive cells.
(Basically, I want to do step 1 and 2 backwards after the user has switched back)
I've already achieved step 1 and 2. Here is the functional component:
const DrumButtons = (props) => {
const drumButtonsVariants = {
hidden: {
y: "140vh",
},
visible: {
y: 0,
transition: {
type: "tween",
duration: 1,
delay: 0.1,
},
},
exit: {
y: "140vh",
transition: {
type: "tween",
duration: 1,
delay: 0.1,
},
},
};
let dbWrapperStyle = {};
if (!props.minimized) {
dbWrapperStyle = {
gridTemplateColumns: "1fr 1fr 1fr 1fr",
};
}
let singleWrapperStyle = {
width: "100%",
height: "100%",
};
let buttonTransition = { duration: 0.5, delay: 0.1 };
return (
<div id="db-wrapper" style={dbWrapperStyle}>
<AnimatePresence>
{props.buttonsarr.map((elem, i) => {
return (
<motion.div
variants={drumButtonsVariants}
initial="hidden"
animate="visible"
exit="exit"
key={elem.press}
style={singleWrapperStyle}
>
<motion.button
layout
transition={buttonTransition}
key={elem.press}
className="drum-pad"
onClick={dbHandleClickWrapper(
props.changetext,
props.buttonsarr
)}
id={elem.name}
>
<span className="front">{elem.press.toUpperCase()}</span>
<audio
key={elem.press.toUpperCase()}
id={elem.press.toUpperCase()}
src={props.buttonsarr[i].source}
preload="auto"
className="clip"
></audio>
</motion.button>
</motion.div>
);
})}
</AnimatePresence>
</div>
);
};
What is the problem, exactly?
So here's what currently happens when switching back from "maximized" to "minimized":
The buttons that are no longer needed slide out to the bottom. At the same time, the 4 grid columns get reduced to 3 columns. So the remaining buttons slide to their position in the 3-column-wide grid, leaving empty cells for the buttons that are currently sliding out, but still present in the DOM.
After the other buttons have been removed, the remaining buttons move again to fill consecutive cells of the grid.
Here is what I've tried:
I've tried adding a when: "beforeChildren" to the transitions of the drumButtonsVariants. Nothing changed.
I've played around with the delays, but I've never achieved the desired outcome. If I delay the layout animation, the removal of the buttons from the DOM will get delayed too, resulting in the same unwanted outcome.
You can view my full code here:
(main branch) https://github.com/Julian-Sz/FCC-Drum-Machine/tree/main
(version with the problem) https://github.com/Julian-Sz/FCC-Drum-Machine/tree/284606cac7cbc4bc6e13bf432c563eab4814d370
Feel free to copy and fork this repository!
The trick is to use State for the dbWrapperStyle.
Then, AnimatePresence with onExitComplete can be used.
This happens when the user switches to minimized again:
The removed buttons slide out (but they are still in the DOM - occupying grid cells)
Now they get removed from the DOM, and onExitComplete fires: The grid columns change to 3 columns and the component gets rerendered. The removal from the DOM and the grid change are happening after each other (without any delay) - resulting in a smooth animation!
Here is the new component:
const DrumButtons = (props) => {
const drumButtonsVariants = {
visible: {
y: 0,
transition: {
type: "tween",
duration: 0.8,
delay: 0.1,
},
},
exit: {
y: "140vh",
transition: {
type: "tween",
duration: 0.8,
delay: 0.1,
},
},
};
// BEFORE------------
// let dbWrapperStyle = {};
// if (!props.minimized) {
// dbWrapperStyle = {
// gridTemplateColumns: "1fr 1fr 1fr 1fr",
// };
// }
// AFTER-------------
const [dbWrapperStyle, setWrapperStyle] = useState({});
useEffect(() => {
if (!props.minimized) {
setWrapperStyle({ gridTemplateColumns: "1fr 1fr 1fr 1fr" });
}
}, [props.minimized]);
let singleWrapperStyle = {
width: "100%",
height: "100%",
};
let buttonTransition = {
type: "spring",
duration: 0.9,
delay: 0.1,
bounce: 0.5,
};
return (
<div id="db-wrapper" style={dbWrapperStyle}>
<AnimatePresence
onExitComplete={() => {
setWrapperStyle({ gridTemplateColumns: "1fr 1fr 1fr" });
}}
>
{props.buttonsarr.map((elem, i) => {
return (
<motion.div
variants={drumButtonsVariants}
initial="exit"
animate="visible"
exit="exit"
key={elem.press}
style={singleWrapperStyle}
>
<motion.button
layout
transition={buttonTransition}
key={elem.press}
className="drum-pad"
onClick={dbHandleClickWrapper(
props.changetext,
props.buttonsarr
)}
id={elem.name}
>
<span className="front">{elem.press.toUpperCase()}</span>
<audio
key={elem.press.toUpperCase()}
id={elem.press.toUpperCase()}
src={props.buttonsarr[i].source}
preload="auto"
className="clip"
></audio>
</motion.button>
</motion.div>
);
})}
</AnimatePresence>
</div>
);
};

Problem with exit and enter animations using react-transition-group

I'm new to React and I'm looking for some help.
I'm trying to build a simple login page. The page gives users three options ('sign in with email', 'sign in with Google', or 'sign up') and displays a component based on the option chosen.
I wanted to add a simple cross-fade as these components enter and exit the DOM so I turned to react-transition-group.
The problem, however, is my components don't exit and enter properly. The old component appears to disappear instantly rather than fade out, two copies of the 'new' component are stacked on top of each other. One fades out and the other fades in. The old and new don't elegantly cross-fade into each other as I would like.
Example:
Initial login page
On click, two copies of the 'new' component appear, one fades in and the other fades out
transition completes and container now holds the new component as desired
I've looked over the docs for react-transition-group but I can't quite figure out what I'm doing wrong.
Here's my main component:
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
display: "loginInitial"
}
};
handleEmailLogin = () => {
this.setState(() => ({
display: "emailLogin"
}))
};
handleGoogleLogin = () => {
console.log("Logging in with google!")
this.props.startGoogleLogin()
};
handleEmailSignUp= () => {
this.setState(() => ({
display: "emailSignUp"
}))
};
handleBackToStart = () => {
this.setState(() => ({
display: "loginInitial",
}))
}
displayBox = () => {
if (this.state.display === "loginInitial") {
return <LoginPageInitial
handleEmailLogin={this.handleEmailLogin}
handleGoogleLogin={this.handleGoogleLogin}
handleEmailSignUp={this.handleEmailSignUp}
/>
} else if (this.state.display === "emailSignUp") {
return <LoginPageEmailSignUp
handleBackToStart={this.handleBackToStart}
/>
} else if (this.state.display === "emailLogin") {
return <LoginPageEmailLogin
handleBackToStart={this.handleBackToStart}
/>
}
}
render() {
return (
<div className="box-layout">
<div className="box-layout__box">
<h1 className="box-layout__title">My app</h1>
<p>Subtitle goes here blah blah blah blah.</p>
<TransitionGroup>
<CSSTransition
classNames="fade"
timeout={600}
key={this.state.display}
>
{this.displayBox}
</CSSTransition>
</TransitionGroup>
</div>
</div>
)
};
}
const mapDispatchToProps = (dispatch) => ({
startGoogleLogin: () => dispatch(startGoogleLogin())
})
export default connect(undefined, mapDispatchToProps)(LoginPage)
And here's the relevant CSS:
.box-layout {
align-items: center;
background: url('/images/bg.jpg');
background-size: cover;
display: flex;
justify-content: center;
height: 100vh;
width: 100vw;
}
.box-layout__box {
background: fade-out(white,.15);
border-radius: 3px;
text-align: center;
width: 30rem;
padding: $l-size $m-size
}
.box-layout__title {
margin: 0 0 $m-size 0;
line-height: 1;
}
// FADE
// appear
.fade-appear {
opacity: 0;
z-index: 1;
}
.fade-appear.fade-appear-active {
opacity: 1;
transition: opacity 600ms linear;
}
// enter
.fade-enter {
opacity: 0;
z-index: 1;
}
.fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 600ms linear;
}
// exit
.fade-exit {
opacity: 1;
}
.fade-exit.fade-exit-active {
opacity: 0;
transition: opacity 600ms linear;
}
I would be extremely appreciative if anyone can offer some advice. I've done a lot of googling but I can't find a solution to my problem.
[EDIT: MOSTLY SOLVED - I managed to solve the bulk of my problem. It appears I needed to place my 'key' prop in my component rather than component. Two versions of the 'new' component no longer appear and the container doesn't stretch. It does appear, however, that the 'old' component still doesn't fade out as I expected. It looks a lot better than it did though. I welcome any further insight on what I was/am doing wrong or how I can improve my code.)

Categories