The below snippet has four boxes. The purpose is that these boxes order will be shuffled and a transition animation occurs as they go to a new location. Each box's key corresponds with a color value from the source array in useState. Each update via the shuffle button, the source array's values are shuffled. Then I map through the array in the return function. I set 2 classNames for each box. One classname corresponds with the index and is for positioning. The other classname corresponds with the source array value and is always in unison with the key for that box.
My issue is that react seems to randomly be deciding what keys to pay attention to and reconcile, and what keys to disregard and just remount those elements. You can see here, some elements properly transition while others just jump to their target location. I'm at a loss as to why this is occuring. Can someone help?
EDIT: I don't believe this is a reconcile issue with respect to unwanted remounting. React is properly respecting the keys and not remounting any. So the issue is with how React handles CSS transition classes added during updates. Some transitions work and others don't. It may just be a limitation of the engine, but if anyone has any further incite please share.
const {useState} = React;
function App() {
const [state, setState] = useState(['Red', 'Green', 'Blue', 'Black'])
function handleShuffle() {
const newState = _.shuffle(state)
setState(newState)
}
return (
<div className="App">
{state.map((sourceValue, index) => {
return (
<div className={
'box positionAt' + index + ' sourceValue' + sourceValue
}
key={sourceValue} ></div>
)
})}
<button id="shuffle" onClick={handleShuffle}> shuffle < /button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( <
App / > ,
rootElement
);
.App {
position: relative;
width: 200px;
height: 200px;
background-color: gray;
}
.box {
width: 25px;
height: 25px;
position: absolute;
transition: transform 1s;
}
.positionAt0 {
transform: translate(0px, 0px);
}
.positionAt1 {
transform: translate(175px, 0px);
}
.positionAt2 {
transform: translate(0px, 175px);
}
.positionAt3 {
transform: translate(175px, 175px);
}
.sourceValueGreen {
background-color: green;
}
.sourceValueBlue {
background-color: blue;
}
.sourceValueRed {
background-color: red;
}
.sourceValueBlack {
background-color: black;
}
#shuffle {
position: absolute;
top: 0px;
left: 75px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="root"></div>
Took me a while to figure it out, because setting the correct keys for the boxes seemed to be just the right thing.
I verified with the developer tools that the keys work, by inspecting a box and storing it in a variable (b = $0), shuffling, re-inspecting the box with the same key (color) and comparing it with the stored node ($0 === b, was true). So The DOM nodes for each key are stable.
But it's not sufficient for CSS transitions because the way browsers are changing the order of elements in the DOM.
You can see it here in a minimized example for efficiently reordering elements in the DOM (I assume that React does similar things internally when elements have to be reordered):
function reorder() {
const list = document.querySelector("ul");
list.appendChild(list.firstElementChild);
}
<ul>
<li>List-item #1</li>
<li>List-item #2</li>
</ul>
<button onclick="reorder()">reorder!</button>
Run the example and set a DOM breakpoint on the resulting <ul> DOM node for "Subtree modification", see screenshot.
If you click on "reorder!", the browser breaks first on the removal of a <li>. If you continue, and immediately after continuing (Firefox: <F8>) the browser breaks again with an insertion of a <li>.
(In my tests, the information Chrome gave about the breaks was a bit misleading, Firefox was better at that)
So the browsers implement reordering technically as "remove and insert", which breaks CSS transitions.
With that knowledge the code can easily be fixed by having fixed order of the boxes in the DOM (The order in DOM doesn't need to be changed, because the position is only set via classes):
(Note: *HTML and CSS unchanged, changes in JavaScript are marked with NEW or CHANGE *)
const {useState} = React;
// NEW: List of boxes for a stable order when rendering to the DOM:
const boxes = ['Red', 'Green', 'Blue', 'Black'];
function App() {
const [state, setState] = useState(boxes); // CHANGE: reuse boxes here
function handleShuffle() {
const newState = _.shuffle(state)
setState(newState)
}
return (
<div className="App">
{/* CHANGE: loop over boxes, not state and lookup position, which is used for the positionAt... class */
boxes.map((sourceValue, index) => {
const position = state.indexOf(sourceValue);
return (
<div className={
'box positionAt' + position + ' sourceValue' + sourceValue
}
key={sourceValue} ></div>
)
})}
<button id="shuffle" onClick={handleShuffle}> shuffle < /button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( <
App / > ,
rootElement
);
.App {
position: relative;
width: 200px;
height: 200px;
background-color: gray;
}
.box {
width: 25px;
height: 25px;
position: absolute;
transition: transform 1s;
}
.positionAt0 {
transform: translate(0px, 0px);
}
.positionAt1 {
transform: translate(175px, 0px);
}
.positionAt2 {
transform: translate(0px, 175px);
}
.positionAt3 {
transform: translate(175px, 175px);
}
.sourceValueGreen {
background-color: green;
}
.sourceValueBlue {
background-color: blue;
}
.sourceValueRed {
background-color: red;
}
.sourceValueBlack {
background-color: black;
}
#shuffle {
position: absolute;
top: 0px;
left: 75px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="root"></div>
Note: it now works even if no key is set.
Related
I'm building a personal portfolio webpage with React and I'm trying to update the content of the div element but when the elements are updated, I want to involve styling, (like the old content to exit on the left and the new content to enter from the left) Sorry for my bad English.
This is the part I'm trying to update and add styling
I've tried to use the componentDidMount method with a function that changes the heading but I don't know how to add animations/styling...
export default class Home extends Component {
skills_section_update = () => {
let heading = document.getElementById('heading');
const headings = [
{
head: 'Web Development'
},
{
head: 'Game Development'
}
];
const update_head = (index) => {
heading.innerText = headings[index].head;
};
update_head(1);
}
componentDidMount = () => {
setInterval(this.skills_section_update, 2000);
};
I'm new to React so any help would be much appreciated.
:)
I don't know if there are special methods to do this using React but I know that you can't animate the content of a DOM element using CSS. What I would recommend is to add the updated content in another DOM element next to your current div and then animate the two to make a nice transition.
You could, for example, do something like that:
.container {
width: 400px;
height: 400px;
border: #000 solid 1px;
position: relative;
overflow: hidden;
}
.content {
width: 100%;
height: 100%;
transition: all .25s ease-in-out;
}
.container:hover .content {
transform: translateY(-100%);
transition: all .25s ease-in-out;
}
.original {
background-color: wheat;
}
.next {
background-color: teal;
}
<div class="container">
<div class="content original">
Content 1
</div>
<div class="content next">
New content
</div>
</div>
I'm trying to make it so that a box would expand (in width and height) and transition from its origin to the center of a screen upon being clicked. Here's what I have so far:
I'm running into two problems here -- when I click on the box, the DOM automatically shifts, because the clicked element has its position changed to 'absolute'. The other problem is that the box doesn't transition from its origin, it transitions from the bottom right corner (it also doesn't return to its position from the center of the screen, when make inactive is clicked).
What am I doing wrong here?
import React from "react";
import styled from "styled-components";
import "./styles.css";
export default function App() {
const [clickedBox, setClickedBox] = React.useState(undefined);
const handleClick = React.useCallback((index) => () => {
console.log(index);
setClickedBox(index);
});
return (
<Container>
{Array.from({ length: 5 }, (_, index) => (
<Box
key={index}
active={clickedBox === index}
onClick={handleClick(index)}
>
box {index}
{clickedBox === index && (
<div>
<button
onClick={(e) => {
e.stopPropagation();
handleClick(undefined)();
}}
>
make inactive
</button>
</div>
)}
</Box>
))}
</Container>
);
}
const Container = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100vh;
`;
const Box = styled.div`
flex: 1 0 32%;
padding: 0.5rem;
cursor: pointer;
margin: 1rem;
border: 1px solid red;
transition: 2s;
background-color: white;
${({ active }) => `
${
active
? `
position: absolute;
width: 50vw;
height: 50vh;
background-color: tomato;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
: ""
}
`}
`;
With CSS
Wery unlikely you can achieve that with plain css. And for sure impossible to achieve a versatile solution.
You have a dynamic size and position to adopt to (starting div)
You have to adapt to the current scrolling position
If you remove the div from the layout is almost impossible to avoid screwing up the layout (even if you can, there will always be some edge case).
transition from a relative to a fixed position.
With the current css standard is impossible to perform these things.
With JS
The solution is to do some javascript magic. Since you are using React i developed you a solution using react-spring (an animation framework). Here you have a wrapping component that will do what you want:
The complete SandBox
import React, { useEffect, useRef, useState } from "react";
import { useSpring, animated } from "react-spring";
export default function Popping(props) {
const cont = useRef(null);
const [oriSize, setOriSize] = useState(null);
const [finalSize, setFinalSize] = useState(null);
useEffect(() => {
if (props.open && cont.current) {
const b = cont.current.getBoundingClientRect();
setOriSize({
diz: 0,
opacity: 0,
top: b.top,
left: b.left,
width: b.width,
height: b.height
});
const w = window.innerWidth,
h = window.innerHeight;
setFinalSize({
diz: 1,
opacity: 1,
top: h * 0.25,
left: w * 0.25,
width: w * 0.5,
height: h * 0.5
});
}
}, [props.open]);
const styles = useSpring({
from: props.open ? oriSize : finalSize,
to: props.open ? finalSize : oriSize,
config: { duration: 300 }
});
return (
<>
<animated.div
style={{
background: "orange",
position: "fixed",
display:
styles.diz?.interpolate((d) => (d === 0 ? "none" : "initial")) ||
"none",
...styles
}}
>
{props.popup}
</animated.div>
<div ref={cont} style={{ border: "2px solid green" }}>
{props.children}
</div>
</>
);
}
Note: This code uses two <div>, one to wrap your content, and the second one is always fixed but hidden. When you toggle the popup visibility, the wrapping div gets measured (we obtain its size and position on the screen) and the fixed div is animated from that position to its final position. You can achieve the illusion you are looking for by rendering the same content in both <div>, but there is always the risk of minor misalignment.
The idea is similar to what newbie did in their post but without any extra libraries. I might have done some things a bit non-standard to avoid using any libraries.
CodeSandbox
import React from "react";
import { StyledBox } from "./App.styles";
export const Box = (props) => {
const boxRef = React.useRef(null);
const { index, active, handleClick } = props;
const handleBoxClick = () => {
handleClick(index);
};
React.useEffect(() => {
const b = boxRef.current;
const a = b.querySelector(".active-class");
a.style.left = b.offsetLeft + "px";
a.style.top = b.offsetTop + "px";
a.style.width = b.offsetWidth + "px";
a.style.height = b.offsetHeight + "px";
});
return (
<StyledBox active={active} onClick={handleBoxClick} ref={boxRef}>
box {index}
<div className="active-class">
box {index}
<div>
<button
onClick={(e) => {
e.stopPropagation();
handleClick(undefined);
}}
>
make inactive
</button>
</div>
</div>
</StyledBox>
);
};
import styled from "styled-components";
export const StyledContainer = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100vh;
`;
export const StyledBox = styled.div`
flex: 1 0 32%;
padding: 0.5rem;
cursor: pointer;
margin: 1rem;
border: 1px solid red;
background-color: white;
.active-class {
position: absolute;
transition: 0.3s all ease-in;
background-color: tomato;
z-index: -1;
${({ active }) =>
active
? `
width: 50vw !important;
height: 50vh !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
z-index: 1;
opacity: 1;
`
: `
z-index: -1;
transform: translate(0, 0);
opacity: 0;
`}
}
`;
first, transition with position or display don't work on css(it can work but without transition).
here you have:
flex: 1 0 32%;
that is equivalent to :
flex-grow: 1;
flex-shrink: 1;
flex-basis: 32%;
so, when active is true, width would jump to 50vw and height to 50vh but roughly without transition. so the solution is to use scale like this:
z-index: 99;
transform: scaleY(5) scaleX(2) translate(20%, 20%);
background-tomato: tomato
and you need to tweak the values of scaleY, scaleX and translate (for each Box) until you get it to work.
you can take a look at what i did in this codesandbox: https://codesandbox.io/s/peaceful-payne-ewmxi?file=/src/App.js:1362-1432
here is also a link if you want master flex: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Controlling_Ratios_of_Flex_Items_Along_the_Main_Ax
be sure that all your items have the following css properties : transform: translateX(0) translateY(0); transition: transform Xms timing-function-of-your-choice, left Xms timing, top Xms timing;
Until your page is completly loaded, let all the item in your page have the css property : position: static.
When page loads, retrive the items' properties : x offset from left of screen, y offset from top of document, width and height.
Use javascript to change the items properties : set position to fixed and affect the left, top, width and height css properties from the values we just retrieved. this way, the items will keep their exact position after their position property changes.
with javascript, when the box is clicked on, to center it inside your page, just apply the following css properties via javascript : left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%); width: the width of your choice; height: the height of your choice;
This way, your item will move in the center of your screen no matter what their width and origin offset were. Also, the transition will be very smooth.
You might also want to change the z-index of an item when it is clicked.
I'm trying to animate a sidebar component following the first section on this page. When I follow this the component doesn't animate, but simply mounts/unmounts.
The component SidePage is as follows:
import React from "react"
import { TransitionGroup, CSSTransition } from "react-transition-group"
import "./sidePage.css"
class SidePage extends React.Component {
componentWillMount() {
console.log("will mount")
}
componentDidMount() {
console.log("did mount")
}
componentWillUnmount() {
console.log("will unmount")
}
render() {
const { content, sidePageOpen } = this.props
return (
<TransitionGroup component={null}>
{sidePageOpen && (
<CSSTransition key={content.id} classNames="sidepage" timeout={2000}>
<div
key={content.id}
className="sidepage"
dangerouslySetInnerHTML={{ __html: content.html }}
/>
</CSSTransition>
)}
</TransitionGroup>
)
}
}
export default SidePage
and the css file:
.sidepage-enter {
opacity: 0;
}
.sidepage-enter-active {
opacity: 1;
transition: all 2s;
}
.sidepage-exit {
opacity: 1;
}
.sidepage-exit-active {
opacity: 0;
transition: all 2s;
}
.sidepage {
background: white;
padding: 10px;
height: 100%;
width: 90vw;
position: absolute;
top: 0;
right: 0;
z-index: 10;
opacity: 0.4;
transition: all 0.6s;
}
Basic stuff I think — the sidePageOpen is a boolean state passed down, I have a button on another page that toggles this state. If anyone has any ideas/suggestions that would be brilliant and appreciated.
Remove the opacity property from sidepage class.
.sidepage {
background: white;
padding: 10px;
height: 100%;
width: 90vw;
position: absolute;
top: 0;
right: 0;
z-index: 10;
opacity: 0.4; // remove me
transition: all 0.6s;
}
The element get's added with a class of sidepage which has a opacity of 0.4, thats whats breaking the animation. Working demo here
Eventually found the solution — I had a styled <Wrapper> div created using emotion.sh styled components, I was using this to contain all of my content, not sure why but this didn't allow any animations — changing this to a simple <div> seemed to fix it.
Edit: Probably because it was recreating the Wrapper component on every state change.
I am making a simple notification service in React, and have encountered a very strange issue which I cannot find any mention of or solution to.
My component has an array of notification messages. Each "notification" has both a "onClick" and "onAnimationEnd" binding that call a function which will remove them from the array of notifications. The basic idea is that the notification will fade away (using a CSS animation) and then be removed, or allow the user to manually click on the notification to remove it.
The interesting bug is as follows. If you add two notifications, the first one will trigger its onAnimationEnd and remove itself. The remaining notification will suddenly jump to the end of its css animation and never trigger its own onAnimationEnd.
Even more curiously if you add four notifications, exactly two of them will have the above bug, while the other two function properly.
It is also worth mentioning that if you create two notifications, and click on one two manually remove it, the remaining notification will act normally and trigger its own onAnimationEnd functionality.
Thus I am forced to conclude that some combination of looping through an array and the onAnimationEnd trigger is bugged somehow in react, unless someone can point out a solution to this problem.
React Code
class Test extends React.Component {
constructor (props) {
super(props)
this.state = {
notifications: []
}
}
add = () => {
this.setState({notifications: [...this.state.notifications, 'Test']})
}
remove = (notification) => {
let notificationArray = this.state.notifications
notificationArray.splice(notificationArray.indexOf(notification), 1)
this.setState({notifications: notificationArray})
}
render() {
return (
<div>
<button onClick={this.add}>Add Notification</button>
<div className="notification-container">
{this.state.notifications.map(
(notification,i) => {
return (
<div onAnimationEnd={()=>this.remove(notification)}
onClick={() => this.remove(notification)}
className="notification"
key={i}>
{notification}
</div>
)
}
)}
</div>
</div>
);
}
}
ReactDOM.render(
<Test />,
document.getElementById('root')
);
CSS
.notification-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 200px;
}
.notification {
border: 1px solid;
box-shadow: 0 10px 20px rgba(0,0,0,0.2), 0 6px 6px rgba(0,0,0,0.25);
color: white;
background: rgba(0,0,0,0.75);
cursor: pointer;
padding: 10px;
margin-top: 10px;
user-select: none;
animation: fade 7s linear forwards;
}
#keyframes fade {
0% {
transform: translate(100%,0);
}
2% {
transform: translate(-20px, 0);
}
5% {
transform: translate(0,0);
}
20% {
opacity: 1;
}
100% {
opacity: 0.25;
}
}
Working Codepen link
https://codepen.io/msorrentino/pen/xeVrwz
You're using an array index as your component key:
{this.state.notifications.map(
(notification,i) => {
return (
<div onAnimationEnd={()=>this.remove(notification)}
onClick={() => this.remove(notification)}
className="notification"
key={i}>
{notification}
</div>
)
}
)}
When you do this, React can't properly detect when your component is removed. For example, by removing the item at index 0, and moving the item at index 1 into its place, React will think the item with key 0 has merely been modified rather than removed. This can have various side-effects, such as what you're seeing.
Try using a unique identifier if you have one, otherwise use some kind of incrementing key.
To test this real fast in your codepen, change these bits (I don't actually recommend using your message as your key, of course):
add = () => {
this.setState({notifications: [...this.state.notifications, Math.random().toString()]})
}
...
key={notification}
Array indexes should only be used as component keys as a last resort.
I want to achieve an effect like this one
in a React webpage but without using jQuery. I've looked for alternatives to that library, but without results. I've seen a lot of similar questions, but each of them are answered using jQuery.
The effect is basically changing the color of the logo (and other elements in the page) as I scroll down through different sections.
Does anyone know a way to achieve this?
A way this could be done is by centering the logo's to their own containers dynamically, kinda like simulating position fixed, but using position absolute, so each logo is contained in their own section, and not globally like position fixed would do.
This way when you scroll to the next section, the second section covers the first section making it look like its transitioning.
I created a proof of concept here:
https://codesandbox.io/s/9k4o3zoo
NOTE: this demo is a proof of concept, it could be improved in performance by using something like request animation frame, and throttling.
Code:
class App extends React.Component {
state = {};
handleScroll = e => {
if (!this.logo1) return;
const pageY = e.pageY;
// 600 is the height of each section
this.setState(prevState => ({
y: Math.abs(pageY),
y2: Math.abs(pageY) - 600
}));
};
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
render() {
const { y, y2 } = this.state;
return (
<div>
<section className="first">
<h1
className="logo"
style={{ transform: `translateY(${y}px)` }}
ref={logo => {
this.logo1 = logo;
}}
>
YOUR LOGO
</h1>
</section>
<section className="second">
<h1
className="logo"
style={{ transform: `translateY(${y2}px)` }}
ref={logo => {
this.logo2 = logo;
}}
>
YOUR LOGO
</h1>
</section>
</div>
);
}
}
CSS would be:
section {
height: 600px;
width: 100%;
position: relative;
font-family: helvetica, arial;
font-size: 25px;
overflow: hidden;
}
.first {
background: salmon;
z-index: 1;
}
.first .logo {
color: black;
}
.second {
background: royalBlue;
z-index: 2;
}
.second .logo {
color: red;
}
.logo {
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 230px;
height: 30px;
}