Can someone explain how to make smooth transition like opacity 0 - opacity 1 with CSStransiton or react-spring animation, when data comes from server and i'm doing map div instantly appears without transition.
i want to make transition on form submit, when im returning data from map(), could someone show me how to add this transition with CSStransition or react-spring.
import React, {useState} from "react";
import axios from "axios";
import moment from "moment";
import { KEY } from "../../const";
import { cloudy, hurricane, rain, snow, sunny } from "./weatherType";
import "./winderCondition.scss";
import "./weather.scss";
import {CSSTransition} from "react-transition-group";
export const Weather = () => {
const [currentWeatherData, setCurrentWeatherData] = useState([]);
const [foreCast, setForeCast] = useState([]);
const [query, setQuery] = useState("");
const getCurrentWeather = async (query: string) => {
const response = await axios.get(`https://api.weatherbit.io/v2.0/current?&city=${query ? query : ""}&key=${KEY}`)
setCurrentWeatherData(response.data.data)
};
const getForecast = async (query: string) => {
const response = await axios.get(`https://api.weatherbit.io/v2.0/forecast/daily?&city=${query ? query : ""}&key=${KEY}&days=5`)
setForeCast(response.data.data);
foreCast.shift();
};
const handleCityChange = (e: any) => {
setQuery(e.target.value);
};
const handleOnSubmit = async (e: any) => {
e.preventDefault();
await getCurrentWeather(query);
await getForecast(query);
};
const getCondition = (weatherCode: number) => {
if (weatherCode >= 200 && weatherCode <= 233) {
return hurricane;
}
if (weatherCode >= 300 && weatherCode <= 522) {
return rain;
}
if (weatherCode >= 600 && weatherCode <= 610) {
return snow;
}
if (weatherCode === 800) {
return sunny;
}
if (weatherCode >= 801 && weatherCode <= 900) {
return cloudy;
}
};
return (
<div className="weather">
<form onSubmit={handleOnSubmit}>
<div className="input_wrapper">
<input className="city-input"
type="text"
onChange={(e) => handleCityChange(e)}
value={query}
name="city"
/>
<label className={query.length !== 0 ? "move-up" : "city-label"} htmlFor="city">Your City</label>
</div>
<button type="submit">Search</button>
</form>
<div className="weather-wrapper">
{currentWeatherData &&
currentWeatherData.map((weather: any) => {
return (
<CSSTransition classNames="my-node" key={weather.city_name} in={true} timeout={300}>
<div className="currentWeather">
<div className="gradient">
<div className="country">
Location: {`${weather.city_name}, ${weather.country_code}`}
</div>
<div className="temperature">
{Math.floor(weather.temp)} °C
</div>
{getCondition(weather.weather.code)}
<div>{weather.weather.description}</div>
</div>
</div>
</CSSTransition>
);
})}
<div className="forecast-wrapper">
{foreCast &&
foreCast.map((weather: any) => {
return (
<div className="forecast" key={weather.ts}>
<div className="forecast-date">
{moment(weather.ts * 1000).format("dddd")}
</div>
<div>{Math.round(weather.temp)} °C</div>
<img
className="forecast-icon"
src={`https://www.weatherbit.io/static/img/icons/${weather.weather.icon}.png`}
alt="weather-condition"
/>
</div>
);
})}
</div>
</div>
</div>
);
};
CSS
.my-node-enter {
opacity: 0;
}
.my-node-enter-active {
opacity: 1;
transition: opacity 500ms;
}
.my-node-exit {
opacity: 1;
}
.my-node-exit-active {
opacity: 0;
transition: opacity 500ms;
}
just needed to add another prop with value true appear={true} and classNames for it.
<CSSTransition classNames="fade" key={weather.city_name} in={true} timeout={500} appear={true]>
<div className="currentWeather">
<div className="gradient">
<div className="country">
Location: {`${weather.city_name}, ${weather.country_code}`}
</div>
<div className="temperature">
{Math.floor(weather.temp)} °C
</div>
{getCondition(weather.weather.code)}
<div>{weather.weather.description}</div>
</div>
</div>
</CSSTransition>
.fade-appear {
opacity: 0.01;
}
.fade-appear.fade-appear-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
thanks to user "wherewereat" from reddit
Related
I have a goUp and goDown functions that doesn't work in a .jsx file. There's a textarea and two buttons (up and down). Using the buttons the functions should scroll up and scroll down in the textarea. I tested the same code in a .js file and they worked fine. However in a .jsx file they didn't work at all. I did some research, but I couldn't figure out the problem.
import { useState } from 'react'
import Dialog from '#mui/material/Dialog'
import Button from '#mui/material/Button'
import CircularProgress from '#mui/material/CircularProgress'
import CloseIcon from '#mui/icons-material/Close'
import KeyboardArrowDownIcon from '#mui/icons-material/KeyboardArrowDown'
import KeyboardArrowUpIcon from '#mui/icons-material/KeyboardArrowUp'
import { styled } from '#mui/material/styles'
import { UpdateBookModalForm } from './updateBookModalSkin'
import { updateBook } from 'api/book'
import Toast from 'shared/Toast'
import { queryClient } from 'App'
import AppContainer from '../../appSkin.js'
const UpdateBookModal = ({
book_id,
book_title,
book_author,
book_issn,
book_note,
handleClose,
modalOpen,
}) => {
const okayy = document.getElementById("textt");
const [bookTitle, setBookTitle] = useState()
const [author, setAuthor] = useState()
const [issn, setIssn] = useState()
const [note, setNote] = useState()
const [bookUpdating, setBookUpdating] = useState(false)
const [toastMessage, setToastMessage] = useState('')
const [toastType, setToastType] = useState('')
const [toastOpen, setToastOpen] = useState(false)
const goUp = (id) => {
if (id.scrollTop !== 0) {
id.scrollTo({
top: id.scrollTop - 10,
left: 0,
behavior: "smooth"
});
}
};
const goDown = (id) => {
id.scrollTo({
top: id.scrollTop + 10,
left: 0,
behavior: "smooth"
});
};
const handleSubmit = async ev => {
ev.preventDefault()
setBookUpdating(true)
const result = await updateBook(book_id, {
book_title: bookTitle,
book_author: author,
book_issnisbn: issn,
book_notes: note,
})
setBookUpdating(false)
handleClose()
setToastOpen(true)
if (result.error) {
setToastType('error')
if (result.error.response) {
setToastMessage(result.error.response.data.message)
} else if (result.error.request) {
setToastMessage(result.error.request)
} else {
setToastMessage(result.error.message)
}
} else {
setToastType('success')
setToastMessage('Book updated successfully!')
queryClient.invalidateQueries('books')
}
}
const toastClose = (event, reason) => {
if (reason === 'clickaway') {
return
}
setToastOpen(false)
}
return (
<BootstrapDialog
onClose={handleClose}
aria-labelledby="customized-dialog-title"
open={modalOpen}
>
<UpdateBookModalForm onSubmit={handleSubmit}>
<AppContainer>
<div className="modal_top">
<input
style={{ lineHeight: 2.5, /*paddingLeft: 15*/ }}
className="h8 input-box font"
type="text"
placeholder="TITLE"
value={bookTitle}
onChange={ev => setBookTitle(ev.target.value)}
/>
<span className="close-icon" onClick={handleClose}>
<CloseIcon />
</span>
</div>
<div className="top" onClick={() => goUp(okayy)}>
<KeyboardArrowUpIcon style={{ cursor: 'pointer' }} />
</div>
<textarea
id="textt"
rows="4"
cols="50"
maxLength="400"
className="h8 text-box font"
placeholder="YOUR NOTES"
value={note}
onChange={ev => setNote(ev.target.value)}
/>
<div className="bottom" onClick={() => goDown(okayy)}>
<KeyboardArrowDownIcon style={{ cursor: 'pointer' }} />
</div>
<div className="modal_bottom">
<div className='note_buttons_container'>
<div className="h7 note">Please note that in most cases we can retrieve <br /> Metadata directly from the uploaded file</div>
<div className="buttons">
<Button
onClick={handleClose}
variant="contained"
className="h6 login-btn font"
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
className="h6 login-btn"
disabled={bookUpdating}
>
{bookUpdating ? (
<CircularProgress
style={{ height: '28px', width: '28px' }}
color="inherit"
/>
) : (
'Save'
)}
</Button>
</div>
</div>
</div>
</AppContainer>
</UpdateBookModalForm>
<Toast
toastClose={toastClose}
open={toastOpen}
severity={toastType}
message={toastMessage}
/>
</BootstrapDialog>
)
}
export default UpdateBookModal
You need to use hook useRef:
const ref = useRef<HTMLTextAreaElement>();
const handleDown = () => {
if (ref.current) {
ref.current.scrollTo({
top: ref.current.scrollTop + 10,
left: 0,
behavior: "smooth"
});
}
};
// .....
<textarea ref={ref} {..otherProps} />
Here is a live example:
This is my home page and getting list data then creating List component as much as need.
Then going to List component and creating list item. so creating new component using map function. I tried different ways for it but I can't figure out. It goes infinite loop whatever I did.
Home.jsx
const [lists, setLists] = useState([]);
useEffect(() => {
const getRandomList = async () => {
try {
const res = await axios.get(`/lists${type ? "?type=" + type : ""}`, {
headers: {
"Content-Type": "application/json;charset=UTF-8",
token: token,
},
});
setLists(res.data);
} catch (error) {
console.log(error);
}
};
getRandomList();
}, [type]);
return (
<div className="home">
<Navbar />
<Featured type={type} />
{lists.map((list, index) => {
return <List key={index} list={list}/>;
})}
</div>
);
}
in List component, creating List item component as much as need.
List.jsx
export default function List({ list }) {
const [isMoved, setIsMoved] = useState(false);
const [slideNumber, setSlideNumber] = useState(0);
const [clickLimit, setClickLimit] = useState(window.innerWidth / 230);
const [lists, setLists] = useState([])
const listRef = useRef();
useEffect( () =>{
const getList = ()=>{
setLists(list.content)
}
getList()
},[list])
const handleClick = (direction) => {
setIsMoved(true);
let distance = listRef.current.getBoundingClientRect().x - 50;
if (direction === "left" && slideNumber > 0) {
setSlideNumber(slideNumber - 1);
listRef.current.style.transform = `translateX(${230 + distance}px)`;
}
if (direction === "right" && slideNumber < 10 - clickLimit) {
setSlideNumber(slideNumber + 1);
listRef.current.style.transform = `translateX(${-230 + distance}px)`;
}
};
return (
<div className="list">
<span className="listTitle">Continue to watch</span>
<div className="listWrapper">
<ArrowBackIosOutlined
className="sliderArrow left"
onClick={() => handleClick("left")}
style={{ display: !isMoved && "none" }}
/>
<div className="listContainer" ref={listRef}>
{lists.map((item, index) => {
return <ListItem key={index} index={index} item={item} />;
})}
</div>
<ArrowForwardIosOutlined
className="sliderArrow right"
onClick={() => handleClick("right")}
/>
</div>
</div>
);
}
last component here.
ListItem.jsx
export default function ListItem({ index, item }) {
const [isHovered, setIsHovered] = useState(false);
const [movie, setMovie] = useState({});
useEffect(() => {
const getMovie = async (item) => {
try {
const res = await axios.get("/movies/find/" + item, {
headers: {
"Content-Type": "application/json;charset=UTF-8",
token: token,
},
});
setMovie(res.data);
} catch (error) {
console.log(error);
}
};
getMovie()
}, [item]);
return (
<div
className="listItem"
style={{
left:
isHovered &&
Object.values(index) * 225 - 50 + Object.values(index) * 2.5,
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<img src={movie.img} alt="" />
{isHovered && (
<>
<video src={movie.trailer} autoPlay loop></video>
<div className="itemInfo">
<div className="itemIcons">
<PlayArrow className="itemIcon" />
<Add className="itemIcon" />
<ThumbUpAltOutlined className="itemIcon" />
<ThumbDownAltOutlined className="itemIcon" />
</div>
<div className="itemInfoTop">
<span>1 hour 14 mins</span>
<span className="limit">{movie.limit}</span>
<span>{movie.year}</span>
</div>
<div className="itemDescription">{movie.description}</div>
<div className="itemGenre">{movie.genre}</div>
</div>
</>
)}
</div>
);
}
Try to remove the useEffect from the List component and map on the list.content directly:
export default function List({ list }) {
const [isMoved, setIsMoved] = useState(false);
const [slideNumber, setSlideNumber] = useState(0);
const [clickLimit, setClickLimit] = useState(window.innerWidth / 230);
const listRef = useRef();
const handleClick = (direction) => {
setIsMoved(true);
let distance = listRef.current.getBoundingClientRect().x - 50;
if (direction === "left" && slideNumber > 0) {
setSlideNumber(slideNumber - 1);
listRef.current.style.transform = `translateX(${230 + distance}px)`;
}
if (direction === "right" && slideNumber < 10 - clickLimit) {
setSlideNumber(slideNumber + 1);
listRef.current.style.transform = `translateX(${-230 + distance}px)`;
}
};
return (
<div className="list">
<span className="listTitle">Continue to watch</span>
<div className="listWrapper">
<ArrowBackIosOutlined
className="sliderArrow left"
onClick={() => handleClick("left")}
style={{ display: !isMoved && "none" }}
/>
<div className="listContainer" ref={listRef}>
{list.content.map((item, index) => {
return <ListItem key={index} index={index} item={item} />;
})}
</div>
<ArrowForwardIosOutlined
className="sliderArrow right"
onClick={() => handleClick("right")}
/>
</div>
</div>
);
}
I'm working on this code to make the image change on click, it works, but the transition between them is rough. Image and name both come from parent component <App.js/>, and I'd like to add an effect to them to make it more subtle. Images already have this kind of effect when they are visible on screen. This is the code for the <Message.jsx/> child component:
function Message(props){
return (
<div className="msg-div-index msg-div-height my-5">
<h3 className="text-start fs-5 m-0 px-2">Make us</h3>
<h2 className="msg-margin msg-lbl">{props.label}</h2>
<img src={props.img} className={"img-fluid msg-img-index msg-img-opacity msg-img-border inline-photo" + (props.visible ? " is-visible" : "")} alt={props.imgAlt}></img>
</div>
)
}
export default Message
And this is the code for the <App.js/> parent component in case you need it:
function useOnScreen(options){
const [ref, setRef] = useState(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setVisible(entry.isIntersecting);
}, options);
if (ref) {
observer.observe(ref);
}
return ( () => {
if (ref) {
observer.unobserve(ref);
}
})
}, [ref, options]);
console.log(visible);
return [setRef, visible];
}
function App() {
const [setRef, visible] = useOnScreen({ threshold: 0.2 });
const [index, setIndex] = useState(0);
const imgArray = [{img: dance, label:"Dance", imgAlt:"People dancing"},
{img: cry, label:"Cry", imgAlt:"Sunset with guitars"},
{img: mariachi, label:"Remember", imgAlt:"Mariachi singing"},
{img: concert, label:"Sing along", imgAlt:"Concert"}];
function handleClick(){
if (index === imgArray.length - 1){
setIndex(0)
} else {
setIndex(index + 1)
}
}
return (
<div className="App karaoke_bgr">
<Header />
<div ref={setRef} onClick={()=>handleClick()}>
<Message visible={visible} img={imgArray[index].img} label={imgArray[index].label} imgAlt={imgArray[index].imgAlt}/>
</div>
<Options date="Friday, September 24th"/>
<Footer />
</div>
);
}
export default App;
I want to add a transition effect each time the props change (when the user clicks it), so the next image slowly appears. Thank you!
Any suggestions about the code itself are highly appreciated. I'm new with React and trying to see what it can do.
You can use react-transition-group for this
styles.css
.fade-enter {
opacity: 0;
}
.fade-exit {
opacity: 1;
}
.fade-enter-active {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
}
.fade-enter-active,
.fade-exit-active {
transition: opacity 500ms;
}
import { useState, useEffect } from "react";
import { SwitchTransition, CSSTransition } from "react-transition-group";
import "./styles.css";
function App() {
const [setRef, visible] = useOnScreen({ threshold: 0.2 });
const [index, setIndex] = useState(0);
const imgArray = [
{ img: "dance", label: "Dance", imgAlt: "People dancing" },
{ img: "cry", label: "Cry", imgAlt: "Sunset with guitars" },
{ img: "mariachi", label: "Remember", imgAlt: "Mariachi singing" },
{ img: "concert", label: "Sing along", imgAlt: "Concert" }
];
function handleClick() {
if (index === imgArray.length - 1) {
setIndex(0);
} else {
setIndex(index + 1);
}
}
return (
<div className="App karaoke_bgr">
<div ref={setRef} onClick={() => handleClick()}>
<SwitchTransition>
<CSSTransition
key={index}
addEndListener={(node, done) =>
node.addEventListener("transitionend", done, false)
}
classNames="fade"
>
<Message
visible={visible}
img={imgArray[index].img}
label={imgArray[index].label}
imgAlt={imgArray[index].imgAlt}
/>
</CSSTransition>
</SwitchTransition>
</div>
</div>
);
}
export default App;
First let me show some relevant code before I explain my problem
CountDownTimer component
import { FC, useState, useEffect, useRef } from "react";
interface CountDownTimerProps {
defaultMinutes: number;
defaultSeconds: number;
ticking: boolean;
}
const CountDownTimer: FC<CountDownTimerProps> = (props) => {
const { defaultMinutes, defaultSeconds, ticking } = props;
const [[mins, secs], setTime] = useState([defaultMinutes, defaultSeconds]);
const timerId = useRef<NodeJS.Timeout | null>(null);
const stopTimerInterval = () => timerId.current !== null && clearInterval(timerId.current);
const tick = () => {
if (mins === 0 && secs === 0) {
return;
} else if (secs === 0) {
setTime([mins - 1, 59]);
} else {
setTime([mins, secs - 1]);
}
};
const reset = () => {
stopTimerInterval();
setTime([defaultMinutes, defaultSeconds]);
};
useEffect(() => {
if (!ticking) {
reset();
return;
} else {
timerId.current = setInterval(() => tick(), 1000);
console.log("creating interval");
}
return () => {
stopTimerInterval();
};
});
return (
<p className="text-white text-5xl">{`${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`}</p>
);
};
export default CountDownTimer;
PomodoroClock component
import { FC } from "react";
import { buildStyles, CircularProgressbarWithChildren } from "react-circular-progressbar";
import CountDownTimer from "./CountDownTimer";
import MinusIcon from "./MinusIcon";
import PlusIcon from "./PlusIcon";
interface PomodoroClockProps {
ticking: boolean;
mins: number;
onIncrementClock(): any;
onDecrementClock(): any;
}
const PomodoroClock: FC<PomodoroClockProps> = (props) => {
const { mins, ticking, onDecrementClock, onIncrementClock } = props;
return (
<div className="w-96 h-96 bg-red-400 rounded-full">
<CircularProgressbarWithChildren
value={47}
strokeWidth={4}
styles={buildStyles({
trailColor: "#fff",
pathColor: "#EF4444",
})}
>
<div className="flex items-center gap-4">
<button onClick={onDecrementClock}>
<MinusIcon />
</button>
<CountDownTimer defaultMinutes={mins} defaultSeconds={0} ticking={ticking} />
<button onClick={onIncrementClock}>
<PlusIcon />
</button>
</div>
</CircularProgressbarWithChildren>
</div>
);
};
export default PomodoroClock;
App component
import { FC, useState } from "react";
import PomodoroClock from "./PomodoroClock";
const App: FC = () => {
const [ticking, setTicking] = useState<boolean>(false);
const [mins, setMins] = useState<number>(5);
const toggleTicking = () => setTicking(!ticking);
const toggleButtonText = ticking ? "STOP" : "START";
type updateClockKind = "inc" | "dec";
const updateClockTime = (kind: updateClockKind, by: number) => {
if (kind === "inc" && mins + by < 20) {
setMins(mins + by);
} else if (kind === "dec" && mins - by > 0) {
setMins(mins - by);
} else {
return;
}
};
return (
<div className="flex content-center flex-col items-center gap-8">
<PomodoroClock
mins={mins}
ticking={ticking}
onIncrementClock={() => updateClockTime("inc", 5)}
onDecrementClock={() => updateClockTime("dec", 5)}
/>
<button
className="bg-red-500 py-4 px-6 rounded-lg text-white text-2xl"
onClick={toggleTicking}
>
{toggleButtonText}
</button>
</div>
);
};
export default App;
SO currently, when I press the start button on the clock everything seems to be working correct it counts down and when I press stop it resets. BUT the problem im having is that on every tick of the clock its creating a new interval and when I stop the clock its erroring in the console with thousands of Warning: Maximum update depth exceeded
Any help is appreciated, it is probably just something small but I have been stuck on this for some time. thanks!
I have a component that switches some content and the animation of the content depending on the side it is switching it from:
import React, { Component } from "react";
class Skills extends Component {
constructor(props) {
super(props);
this.state = {
shownSkill: 0,
fallIn: true,
slideUp: false
};
}
getPreviousSkill = () => {
const { shownSkill } = this.state;
const newSkill = shownSkill < 1 ? 3 : shownSkill - 1;
this.updateShownSkill(newSkill, false);
};
getNextSkill = () => {
const { shownSkill } = this.state;
const newSkill = shownSkill > 2 ? 0 : shownSkill + 1;
this.updateShownSkill(newSkill, true);
};
updateShownSkill = (skillIndex, fallIn) => {
this.setState({
shownSkill: skillIndex,
fallIn: fallIn,
slideUp: !fallIn
});
};
getSkillData = () => {
const { skills } = this.props;
const { shownSkill } = this.state;
return skills[shownSkill];
};
render() {
const { name, skill, description } = this.getSkillData();
const { shownSkill, slideUp } = this.state;
const { skills } = this.props;
return (
<div className="route-container skills">
<div className="skills-content-container">
{slideUp ? (
<div className="skills-right-content slide-up">
<div className="subtitle">{name}</div>
{description.map((p, i) => (
<div className="text" key={i}>
{p}
</div>
))}
</div>
) : (
<div className="skills-right-content
fall-in">
<div className="subtitle">{name}</div>
{description.map((p, i) => (
<div className="text" key={i}>
{p}
</div>
))}
</div>
)}
</div>
</div>
);
}
}
export default Skills;
Then I am animating the .fall-in class with css:
#keyframes fall-in {
0% {
margin-top: -600px;
}
100% {
margin-top: 0;
}
}
.fall-in {
animation-name: fall-in;
animation-duration: 0.5s;
animation-timing-function: linear;
animation-iteration-count: 1;
}
I would like this animation to trigger once every time the content of the .subtitle and .text divs changes, regardless of whether or not the animation changed.
This example will only trigger the animation the first time the css class is added.
Hey maybe you want to give a try on my OSS.
https://github.com/bluebill1049/react-simple-animate
I think it does what you want above, maybe worth to give it a try?
import Animate from 'react-simple-img';
import React from 'react';
export default ({ready}) => {
return <Animate startAnimation={ready} startStyle={{
marginTop: '-600px',
}} endStyle={{
marginTop: '0',
}}>
<YourComponent />
</Animate>
};