framer.motion animation is instant instead of animating - javascript

I have several boxes that I want to animate through,
Here's a simple app example (Also a codesandbox here)
Each "box" should fade in and fade out, however, in this example, the animation happens isntantly.
const Box = styled.div`
width: 100px;
height: 100px;
background: green;
`;
const Test = ({ isActive }) => {
return (
<motion.div
animate={isActive ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 3 }}
>
<Box>hello world</Box>
</motion.div>
);
};
export default function App() {
const [currentIndex, setCurrentIndex] = useState(0);
const boxes = [
{
component: ({ isActive }) => <Test isActive={isActive} />
},
{
component: ({ isActive }) => <Test isActive={isActive} />
}
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div onClick={() => setCurrentIndex(currentIndex === 0 ? 1 : 0)}>
{boxes.map((box, index) => {
const isActive = index === currentIndex;
return <box.component isActive={isActive} />;
})}
</div>
</div>
);

I have never used framer.motion before, but looking at their documentation, I think you can use variants, to achieve what you need. https://www.framer.com/api/motion/examples/
I've slightly refactored your code, to get it working:
import "./styles.css";
import { motion } from "framer-motion";
import styled from "styled-components";
import { useEffect, useState } from "react";
const Box = styled.div`
width: 100px;
height: 100px;
background: green;
`;
const variants = {
open: { opacity: 1 },
closed: { opacity: 0 }
};
const Test = ({ index, currentIndex }) => {
return (
<motion.div
animate={index === currentIndex ? "open" : "closed"}
variants={variants}
transition={{ duration: 3 }}
>
<Box>hello world</Box>
</motion.div>
);
};
export default function App() {
const [currentIndex, setCurrentIndex] = useState(0);
const boxes = ["a", "b"];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>{currentIndex}</h2>
<div onClick={() => setCurrentIndex(currentIndex === 0 ? 1 : 0)}>
{boxes.map((box, i) => {
return <Test index={i} currentIndex={currentIndex} />;
})}
</div>
</div>
);
}
the currentIndex is passed as props to the child Test components, they check themselves whether their index matches the currentIndex and update their animations accordingly.
Edited codesandbox here: https://codesandbox.io/s/suspicious-austin-tymvx

In framer motion, you have useCycle properties. Here is an example.
Code in example:
import * as React from "react";
import { render } from "react-dom";
import { Frame, useCycle } from "framer";
import "./styles.css";
export function MyComponent() {
const [animate, cycle] = useCycle(
{ scale: 1.5, rotate: 0, opacity: 1 },
{ scale: 1.0, rotate: 90, opacity: 0 }
);
return (
<Frame
animate={animate}
onTap={() => cycle()}
size={150}
radius={30}
background={"#fff"}
/>
);
}
const rootElement = document.getElementById("root");
render(<MyComponent />, rootElement);
and some simple css:
body {
margin: 0;
padding: 0;
}
#root {
font-family: sans-serif;
text-align: center;
width: 100vw;
height: 100vh;
display: flex;
place-content: center;
place-items: center;
background: rgba(0, 85, 255, 1);
margin: 0;
padding: 0;
perspective: 1000px;
}
I don't recommend you to use this type of construction: animate={index === currentIndex ? "open" : "closed"} , because you might have some lagging/breaking animation.
Try always to search examples/elements of MotionAPI lib. You will have less code lines and mostly "clean" code with no useless variables.

Related

React.js/Next.js Loader is showing in the wrong place on the page

I want to show a Loader on top of everything while fetching data from my API. I asked this question and implemented the answer, and it is working, but it shows it not on top level, but inside the page itself. When looking at the html tree I can see that it is on the top level.
This is what happens when btnAll is clicked:
After scrolling down:
This is the HTML tree:
Why does it put it inside the page?
loading.provider.js
import { createContext, useContext, useState } from "react";
const LoadingContext = createContext({
loading: false,
setLoading: null
});
export function LoadingProvider({ children }) {
const [loading, setLoading] = useState(false);
const value = { loading, setLoading };
console.log(`LoadingProvider: ${loading}`);
return (
<LoadingContext.Provider value={value}>{children}</LoadingContext.Provider>
);
};
export function useLoading() {
const context = useContext(LoadingContext);
console.log(`Using LoadingContextProvider`);
if (!context) {
throw new Error('useLoading must be used within LoadingProvider');
}
return context;
};
app.js
export default function App({ Component, pageProps }) {
return (
<>
<CssBaseline />
<AppStateProvider>
<LoadingProvider>
<Loader />
<Layout>
<Component {...pageProps} />
</Layout>
</LoadingProvider>
</AppStateProvider>
</>
);
};
Loader.js
import { useLoading } from "#/Providers/loading.provider";
import { useEffect } from "react";
import LoadingScreen from "./LoadingScreen";
const Loader = () => {
const { loading } = useLoading();
useEffect(() => {
console.log(`[app.js/#useEffect]: useLoading() value changed to: ${loading}`);
}, [loading]);
return loading && <LoadingScreen loading={true} bgColor='#fff' spinnerColor={'#00A1FF'} textColor='#676767'></LoadingScreen>;
};
export default Loader;
LoadingScreen.js
import styles from './LoadingScreen.module.css';
export default function LoadingScreen() {
return (
<div className={styles.loading}>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
</div>
);
}
This is the css part of style.loading inside LoadingScreen.module.css:
.loading {
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99;
background-color: #FFFFFF;
}
I am calling it inside [identifier].js page like that:
import { useLoading } from "#/Providers/loading.provider";
export default function MainPage({ response }) {
const { loading, setLoading } = useLoading();
const btnClickedAll = async (data) => {
setLoading(true);
};
return (
<Fragment>
<Button sx={{ ml: 2 }} key={'btnAll'} onClick={btnClickedAll} variant="contained">All</Button>,
</Fragment>
);
}
Updated:
Try set the style .loading position: fixed;
Old:
Try moving down the <Loader /> in app.js
...
<LoadingProvider>
<Layout>
<Component {...pageProps} />
</Layout>
<Loader />
</LoadingProvider>
...
Or it's better to put Loader in loading.provider.js and remove <Loader /> from app.js
...
<LoadingContext.Provider value={value}>
{children}
<Loader />
</LoadingContext.Provider>
...

Same component, multiple times used - applying different css styles

I want box to get coloured but only the one i click on and the rest should have default one. Then, when i click on the next one the previous box should go back to the default color. Currently, when i click on one of them, all of the boxes get unwanted background color. I know that i basically pass "color" prop to each of them so all get coloured because of the same prop and state. But how to do it properly with only one state and without changing prop name?
const {useState} = React;
const App = () => {
const [color, setColor] = useState("");
const firstBox = () => {
setColor("firstBox");
};
const secondBox = () => {
setColor("secondBox");
};
const thirdBox = () => {
setColor("thirdBox");
};
return (
<div className="container">
<Box setColor={firstBox} color={color} />
<Box setColor={secondBox} color={color} />
<Box setColor={thirdBox} color={color} />
</div>
);
}
const Box = ({ setColor, color }) => {
return (
<div
onClick={setColor}
className={`box ${
color === "firstBox" || color === "secondBox" || color === "thirdBox"
? "active"
: ""
}
`}
></div>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App/>
);
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
display: flex;
gap: 2rem;
}
.box {
width: 250px;
height: 250px;
background: lightblue;
}
.active {
background: red;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Multiple ways to do this, but this is what I'd do:
const {useState} = React;
const App = () => {
const [color, setColor] = useState("");
return (
<div className="container">
<Box setColor={() => setColor('First Box')} selected={color === 'First Box'} />
<Box setColor={() => setColor('Second Box')} selected={color === 'Second Box'} />
<Box setColor={() => setColor('Third Box')} selected={color === 'Third Box'} />
</div>
);
}
const Box = ({ setColor, selected }) => {
return (
<div
onClick={setColor}
className={selected ? 'active box' : 'box'}
/>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App/>
);
Since you asked you didn't want to use any extra prop or state.
All you have to do is change your Box component code to this,
const Box = ({ setColor, color }) => {
return (
<div
onClick={setColor}
className={`box ${setColor.name === color ? "active" : ""}
`}
></div>
);
};

setTimeout runs indefinetly inside useEffect

I am trying to make a notification toast component. And I want it to be removed after 2 seconds (not-shown on the screed) Although, it is removed (not-shown on the screen via top:-100 argument), the component is getting rendered infinitely. You can see it from the console.log's I have placed inside the component and inside the useEffect call with setTimeout.
My expectation is that setTimeout should run setShowState after 2 seconds and then useEffect should do the cleanup and remove the timer. So everything is back to normal until showState changes.
import React, {useEffect, useState} from 'react'
import I18n from '../../i18n'
import styled from 'styled-components'
import {createGlobalStyle} from 'styled-components'
import {useSelector} from 'react-redux'
const NotificationStyle = createGlobalStyle`
#media (max-width: 500px) {
.notification_mssg {
left: 10px;
}
}
`
const Container = styled.div`
color: white;
position: fixed;
top: ${(props) => props.top}px;
right: 16px;
z-index: 2000;
transition: top 0.5s ease;
`
const NoticitactionIcon = styled.div`
float: left;
font-size: 27px;
width: 40px;
height: 40px;
text-align: center;
`
const NotificationMessage = styled.span`
padding: 10px;
line-height: 40px;
`
function NotificationAlertRoot(props) {
const create_notification = useSelector((state) => state.notifications.create_notification)
const {message, code} = create_notification.success_info
const [showState, setShowState] = useState({top: -100, msg: message, bgColor: '#444'})
// show notification
useEffect(() => {
setShowState({top: 96, msg: I18n.t(message), bgColor: backgroundColor(code)})
}, [message, code])
console.log('amIrendered', showState) // although showState doesn't change, component is getting rendered infinitely :/
// hide notification after 2 seconds
useEffect(() => {
const timerId = setTimeout(() => {
setShowState({
top: -100,
msg: '',
bgColor: `#ffffff00`,
})
console.log("timerId", timerId) // I see timerId is changing so the problem most probably in this useEffect call.
}, 2000)
return () => {
clearTimeout(timerId)
}
}, [showState])
const notificationIcon = (bgColor) => {
switch (bgColor) {
case '#32c786':
return (
<NoticitactionIcon style={{background: '#2aa872'}}>
<i className="zmdi zmdi-info" />
</NoticitactionIcon>
)
case '#ffc721':
return (
<NoticitactionIcon style={{background: '#fabb00'}}>
<i className="zmdi zmdi-alert-triangle" />
</NoticitactionIcon>
)
case '#ff6b68':
return (
<NoticitactionIcon style={{background: '#ff4642'}}>
<i className="zmdi zmdi-alert-circle" />
</NoticitactionIcon>
)
default:
return <span></span>
}
}
function backgroundColor(code) {
switch (Math.floor(code / 100)) {
case 2:
return '#32c786'
case 3:
return '#ffc721'
case 4:
return '#ff6b68'
case 5:
return '#ff6b68'
default:
return '#444'
}
}
return (
<React.Fragment>
<NotificationStyle />
<Container
className="notification_mssg"
top={showState.top}
style={{background: showState.bgColor}}
>
{notificationIcon(showState.bgColor)}
<NotificationMessage>{showState.msg}</NotificationMessage>
</Container>
</React.Fragment>
)
}
export default NotificationAlertRoot
Do you have an idea what is wrong above?
I guess the problem comes from your dependency array. Your useEffect is dependent on showState and each time, you are calling setShowState in your useEffect when setShowState is called showState changes and then again, your useEffect
gets invoked(it is dependent on ShowState), and again setShowState is called and ...
infinity loop!
I found the root of the problem. Sometimes you are too focused on something and you forget the little details of useEffect. It is always dangerous to provide objects as dependency arrays to useEffect. The dependency array values should be simple values. So now I introduced a new state (flag, setFlag) with just boolean values and I make the second useEffect just to follow that simple value. Everything is working just fine now.
import React, {useEffect, useState} from 'react'
import I18n from '../../i18n'
import styled from 'styled-components'
import {createGlobalStyle} from 'styled-components'
import {useSelector} from 'react-redux'
const NotificationStyle = createGlobalStyle`
#media (max-width: 500px) {
.notification_mssg {
left: 10px;
}
}
`
const Container = styled.div`
color: white;
position: fixed;
top: ${(props) => props.top}px;
right: 16px;
z-index: 2000;
transition: top 0.5s ease;
`
const NotificationIcon = styled.div`
float: left;
font-size: 27px;
width: 40px;
height: 40px;
text-align: center;
`
const NotificationMessage = styled.span`
padding: 10px;
line-height: 40px;
`
function NotificationAlertRoot(props) {
const create_notification = useSelector((state) => state.notifications.create_notification)
const {message, code} = create_notification.success_info
const [showState, setShowState] = useState({top: -100, msg: message, bgColor: '#444'})
const [flag, setFlag] = useState(false) // when you follow the showState at the second useEffect you have an infinite loop. Because it is an object.
// show notification
useEffect(() => {
setShowState({top: 96, msg: I18n.t(message), bgColor: backgroundColor(code)})
setFlag(true)
}, [message, code])
// hide notification after 2 seconds
useEffect(() => {
const timerId = setTimeout(() => {
setShowState({top: -100,msg: '', bgColor: `#ffffff00`})
setFlag(false)
}, 2000)
return () => {
clearTimeout(timerId)
}
}, [flag]) // showState
const notificationIcon = (bgColor) => {
switch (bgColor) {
case '#32c786':
return (
<NotificationIcon style={{background: '#2aa872'}}>
<i className="zmdi zmdi-info" />
</NotificationIcon>
)
case '#ffc721':
return (
<NotificationIcon style={{background: '#fabb00'}}>
<i className="zmdi zmdi-alert-triangle" />
</NotificationIcon>
)
case '#ff6b68':
return (
<NotificationIcon style={{background: '#ff4642'}}>
<i className="zmdi zmdi-alert-circle" />
</NotificationIcon>
)
default:
return <span></span>
}
}
const backgroundColor = (code) => {
switch (Math.floor(code / 100)) {
case 2:
return '#32c786'
case 3:
return '#ffc721'
case 4:
return '#ff6b68'
case 5:
return '#ff6b68'
default:
return '#444'
}
}
return (
<React.Fragment>
<NotificationStyle />
<Container
className="notification_mssg"
top={showState.top}
style={{background: showState.bgColor}}
>
{notificationIcon(showState.bgColor)}
<NotificationMessage>{showState.msg}</NotificationMessage>
</Container>
</React.Fragment>
)
}
export default NotificationAlertRoot

React: can I change a state (useState) without causing a repaint, so that I can see a css transition?

I have different "cards" that on click onClick I want their margin-left property to be modified
To do that I use useState, for which I have only one state that is an object that stores the states for all the cards
The below example code shows the problem, but a simplified version that doesn't have a component <Type> and that uses a simple elements array works as expected
So, if I need to use a structure like the one below, how could I keep the transition effect?
Example code
https://codesandbox.io/s/keen-shadow-2v16s?fontsize=14&hidenavigation=1&theme=dark
import React, { useState } from "react";
import styled from "#emotion/styled";
export default function App() {
const [userTap, setUserTap] = useState({});
const elements1 = [...Array(5)];
const elements2 = [...Array(3)];
const Type = ({ list }) =>
list.map((el, i) => (
<Ingredient
key={"draggable" + i}
onClick={e => {
e.stopPropagation();
e.preventDefault();
userTap[i] = userTap[i] ? 0 : 1;
setUserTap({ ...userTap }); // create a new ref to provoke the rerender
return;
}}
userTap={userTap[i]}
>
<div>item</div>
</Ingredient>
));
return (
<>
<Type list={elements1} />
<Type list={elements2} />
</>
);
}
const Ingredient = styled.li`
list-style: none;
cursor: pointer;
margin: 5px;
padding: 5px;
background: #ccc;
border-radius: 3px;
width: 50px;
margin-left: ${props => (props.userTap ? "100px" : "15px")};
transition: all 0.2s ease-in;
`;
The only thing needed to be done, as #larz suggested in the comments, is to move the useState to the last component, as shown below
https://codesandbox.io/s/affectionate-hawking-5p81d?fontsize=14&hidenavigation=1&theme=dark
import React, { useState } from "react";
import styled from "#emotion/styled";
export default function App() {
const elements1 = [...Array(5)];
const elements2 = [...Array(3)];
const Type = ({ list, type }) => {
const [userTap, setUserTap] = useState({});
return list.map((el, i) => (
<Ingredient
key={"draggable" + i}
onClick={e => {
e.stopPropagation();
e.preventDefault();
userTap[type + i] = userTap[type + i] ? 0 : 1;
setUserTap({ ...userTap }); // create a new ref to provoke the rerender
return;
}}
userTap={userTap[type + i]}
>
<div>item</div>
</Ingredient>
));
};
return (
<>
<Type list={elements1} type="one" />
<Type list={elements2} type="two" />
</>
);
}
const Ingredient = styled.li`
list-style: none;
cursor: pointer;
margin: 5px;
padding: 5px;
background: #ccc;
border-radius: 3px;
width: 50px;
margin-left: ${props => (props.userTap ? "100px" : "15px")};
transition: all 0.2s ease-in;
`;

States aren't being updated

I'm currently working on an image uploader component in React. Everything works fine but the deleting method. I've read a couple of articles on how to update arrays/objects and the idea of immutable state. Here's what I've tried:
.filter()
.slice()
.splice() (I doubt this would work as it modifies the original array)
And I always got this error no matter what I tried:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
And this is my code:
ImageUploader.js
import React, { Component } from 'react';
import styled from 'styled-components';
import FileUploadButton from '../FileUploadButton';
import ImagePreviewer from './ImagePreviewer';
import {
Typography,
Button
} from '#material-ui/core';
import theme from '../../../theme';
import uuidv5 from 'uuid/v5';
const StyledPreviewerContainer = styled.div`
display: flex;
margin: ${theme.spacing.unit}px 0;
overflow: hidden;
overflow-x: auto;
`;
export default class ImageUploader extends Component {
state = {
uploadedImages: []
}
updateImages = e => {
const { uploadedImages } = this.state,
files = [...e.target.files],
inexistentImages = files.filter(image => uploadedImages.indexOf(image) === -1);
this.setState(prevState => ({
uploadedImages: [...prevState.uploadedImages, ...inexistentImages]
}));
this.props.onChange(e);
}
removeImages = image => {
const { uploadedImages } = this.state,
imageIndex = uploadedImages.indexOf(image);
this.setState(prevState => ({
uploadedImages: prevState.uploadedImages.filter((image, index) => index !== imageIndex)
}));
};
render() {
const {
className,
label,
id,
multiple,
name,
onBlur
} = this.props, {
uploadedImages
} = this.state;
return (
<div className={className}>
<Typography>
{label}
</Typography>
<StyledPreviewerContainer>
{uploadedImages.map(image =>
<ImagePreviewer
src={URL.createObjectURL(image)}
image={image}
removeImages={this.removeImages}
key={uuidv5(image.name, uuidv5.URL)}
/>
)}
</StyledPreviewerContainer>
<FileUploadButton
id={id}
multiple={multiple}
name={name}
onChange={this.updateImages}
onBlur={onBlur}
/>
<Button>
Delete all
</Button>
</div>
);
}
}
ImagePreviewer.js
import React, { Component } from 'react';
import styled from 'styled-components';
import AnimatedImageActions from './AnimatedImageActions';
import { ClickAwayListener } from '#material-ui/core';
import theme from '../../../theme';
const StyledImagePreviewer = styled.div`
height: 128px;
position: relative;
user-select: none;
cursor: pointer;
&:not(:last-child) {
margin-right: ${theme.spacing.unit * 2}px;
}
`;
const StyledImage = styled.img`
height: 100%;
`;
export default class ImagePreviewer extends Component {
state = {
actionsOpened: false
};
openActions = () => {
this.setState({
actionsOpened: true
});
};
closeActions = () => {
this.setState({
actionsOpened: false
});
};
render() {
const {
actionsOpened
} = this.state,
{
src,
image,
removeImages
} = this.props;
return (
<ClickAwayListener onClickAway={this.closeActions}>
<StyledImagePreviewer onClick={this.openActions}>
<StyledImage src={src} />
<AnimatedImageActions
actionsOpened={actionsOpened}
image={image}
removeImages={removeImages}
/>
</StyledImagePreviewer>
</ClickAwayListener>
);
}
}
AnimatedImageActions.js
import React from 'react';
import styled from 'styled-components';
import { Button } from '#material-ui/core';
import { Delete as DeleteIcon } from '#material-ui/icons';
import { fade } from '#material-ui/core/styles/colorManipulator';
import theme from '../../../theme';
import {
Motion,
spring
} from 'react-motion';
const StyledImageActions = styled.div`
position: absolute;
top: 0;
left: 0;
color: ${theme.palette.common.white};
background-color: ${fade(theme.palette.common.black, 0.4)};
width: 100%;
height: 100%;
display: flex;
`;
const StyledImageActionsInner = styled.div`
margin: auto;
`;
const StyledDeleteIcon = styled(DeleteIcon)`
margin-right: ${theme.spacing.unit}px;
`;
const AnimatedImageActions = ({ actionsOpened, removeImages, image }) =>
<Motion
defaultStyle={{
scale: 0
}}
style={{
scale: spring(actionsOpened ? 1 : 0, {
stiffness: 250
})
}}
>
{({ scale }) =>
<StyledImageActions style={{
transform: `scale(${scale})`
}}>
<StyledImageActionsInner>
<Button
color="inherit"
onClick={removeImages(image)}
>
<StyledDeleteIcon />
Delete
</Button>
</StyledImageActionsInner>
</StyledImageActions>
}
</Motion>
;
export default AnimatedImageActions
Any help would be greatly appreciated!
Could it be that onClick={removeImages(image)} should be onClick={()=>removeImages(image)}?
Otherwise, removeImages is calling setState in AnimatedImageActions's render pass.

Categories