I am new in React and in React Spring. What I try to do is do some fade in and out effects with useTranstion. I have divs with texts and images and both will have different transitions, but for testing purposes are both same now. My inspiration is from React Spring examples, here: https://codesandbox.io/embed/morr206pv8.
But whatever I try to do, I still getting errors with TypeError: can't access property "text", item is undefined from map function. Same happens with images.
And one more error is this: and I have no idea what it is.
Can someone please help me and explain what I did wrong? I will be very thankful.
Here is my code:
import React, { useState, useEffect } from 'react';
import { useTransition, animated } from 'react-spring';
import Slide1 from "../images/slide1.jpg";
import Slide2 from "../images/slide2.jpg";
import Slide3 from "../images/slide3.jpg";
const Slider = () =>{
const texts = [
{id: 0, text: "Text1"},
{id: 1, text: "Text2"},
{id: 2, text: "Text3"},
];
const images = [
{ id: 0, image: Slide1 },
{ id: 1, image: Slide2 },
{ id: 2, image: Slide3 },
];
const [index, setIndex] = useState(0);
const textTransitions = useTransition(texts[index], item => item.id, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
const imageTransitions = useTransition(images[index], item => item.id, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
useEffect(() => void setInterval(() => setIndex(state => (state + 1) % 3), 2500), []);
return(
<div className="sliderContainer">
{textTransitions.map(({ item, props, key }) => (
<animated.div className="sliderText" style={props} key={key} >
<h1><span></span>Autorizovaný chiptuning</h1>
<p>{item.text}</p>
</animated.div>
))}
<div className="sliderImage">
{imageTransitions.map(({ item, props, key }) => (
<animated.img src={item.image} key={key} style={props} alt="" />
))}
</div>
</div>
);
}
export default Slider;
Here is full error:
EDIT #1
So answer is that this syntax of useTransition dont work in versions 9 and up. If you want to work with this syntax, use for example version 8.0.27. Hope it will help someone with same problem.
EDIT #2
If you want to write this in version 9 or higher, write it like this or change it for your own purpose.
const [index, setIndex] = useState(0);
useEffect(() => void setInterval(() => setIndex((state) => (state + 1) % 3), 4000), []);
const texts = [
{id: 0, text: "Chiptuning osobních aut"},
{id: 1, text: "Chiptuning nákladních aut"},
{id: 2, text: "Chiptuning zemědělské techniky"},
];
const textTransitions = useTransition(texts[index], {
from: { opacity: 0, transform: "translate3d(0,-15%,0) scale3d(1,1.1,1)" },
enter: { opacity: 1, transform: "translate3d(0,0,0) scale3d(1,1,1)" },
leave: { opacity: 0, transform: "translate3d(0,15%,0) scale3d(1,0.95,1)" },
config: config.gentle,
key: texts[index].id,
});
return(
{textTransitions((props, item) => (
<animated.div className="sliderText" style={props}>
<h1>
<span>A</span>utorizovaný chiptuning QUANTUM
</h1>
<p>{item.text}</p>
</animated.div>
))}
);
Related
I have a function to populate certain sections of my site with new content on a regular interval (5000), however my function isn't executing when I run the app?
If I set the interval to a console.log function, it works, but it doesn't seem to be working on my 'updateTestimonial' function. If I manually change the idx, it works fine...
I am just learning so apologies if this is obvious -_-
import coverphoto from './img/bkg/moss2.jpg';
import quotes from './img/quotes.png';
import quotes2 from './img/quotes2.png';
import React, { useState, useEffect } from 'react';
const Home = props =>{
const testimonials = [
{
name: 'Ben Frank',
position: 'CEO',
photo: require('./img/chrisphoto.png'),
text:
"Test"
},
{
name: 'Jill Cha',
position: 'Software Engineer',
photo: require('./img/chrisphoto.png'),
text:
'Testimonial1'
},
{
name: 'Adam Niskanen',
position: 'Data Entry',
photo: require('./img/chrisphoto.png'),
text:
"Testimonial2"
},
];
let idx = 0;
let name = testimonials[idx].name;
let position= testimonials[idx].position;
let photo= testimonials[idx].photo;
let text = testimonials[idx].text;
function updateTestimonial() {
idx++;
if (idx > testimonials.length - 1) {
idx = 0;
}
name = testimonials[idx].name;
position= testimonials[idx].position;
photo= testimonials[idx].photo;
text = testimonials[idx].text;
}
setInterval(updateTestimonial, 5000);
return (
<div className="home_main" style={{
backgroundImage: `url(${coverphoto})`,
backgroundRepeat: 'no-repeat'}}>
<div className="home-testimonial-container"><img className="quotes" src={quotes}/><img className="quotes2" src={quotes2}/><div className='testimonial-entry'>
<img className='testimonial-photo'
src={photo}
></img>
<div className='testimonial-text'>
<h3 className='titlestyle2'>{name}</h3><h3 className='subtitlestyle' style={{fontSize: "10pt"}}>{position}</h3></div>
<div className='testimonial-body-container'><h3 className='bodystyle' style={{fontStyle:"italic"}}>{text}</h3>
</div>
</div></div>
</div>
);
}
export default Home;
You need to call setInterval inside a useEffect hook and don't forget to clear the interval when the component unmounts
check the following article https://upmostly.com/tutorials/setinterval-in-react-components-using-hooks
You can do like this
const [idx, setIdx] = useState(0);
useEffect(() => {
const interval = setInterval(() => setIdx((previousValue) => previousValue
+1), 5000);
return () => {
clearInterval(interval);
};
}, []);
pic1
pic2
pic3
When I select one item, I want to apply the new style of one item and the original style of the rest of the items
Example
when I click SubBookmark33
Current
pic1 -> pic2
But I want to
pic1 -> pic3
BookmarksFolder.js
import BookmarksFolderNode from './BookmarksFolderNode';
import classes from './BookmarksFolder.module.css';
import { folders } from '../../resources/data';
function BookmarksFolder() {
return (
<div className={classes.bookmarksFolder}>
{folders.map((folder) => (
<BookmarksFolderNode
key={folder.id}
folder={folder}
/>
))}
</div>
);
}
export default BookmarksFolder;
BookmarksFolderNode.js
import { useState, useRef } from 'react';
import { AiFillCaretDown, AiFillCaretRight } from 'react-icons/ai';
import Folder from '../../resources/img/folder.svg';
import OpenedFolder from '../../resources/img/opened_folder.svg';
import classes from './BookmarksFolderNode.module.css';
function BookmarksFolderNode(props) {
const [folderIsOpen, setFolderIsOpen] = useState(false);
const tab = useRef();
const img = useRef();
const title = useRef();
const selectFolderHandler = () => {
tab.current.style.backgroundColor = '#1a73eb';
img.current.src = OpenedFolder;
title.current.style.color = '#1a73eb';
};
const openFolderHandler = () => {
setFolderIsOpen((prevState) => !prevState);
};
const paddingLeft = 20 * (props.folder.depth - 1);
return (
<div className={classes.bookmarksFolderNode}>
<div className={classes.bookmarksMainFolderNode}>
<div className={classes.verticalTab} ref={tab}></div>
<div className={classes.innerContainer} style={{paddingLeft}}>
<div className={classes.icon} onClick={openFolderHandler}>
{folderIsOpen ? (
<AiFillCaretDown className={classes.ironIcon} />
) : (
<AiFillCaretRight className={classes.ironIcon} />
)}
</div>
<img src={Folder} className={classes.folderIcon} ref={img} />
<div
className={classes.menuLabel}
onClick={selectFolderHandler}
ref={title}
>
{props.folder.title}
</div>
</div>
</div>
<div className={classes.bookmarksSubFolderNode}>
{props.folder.subFolder &&
props.folder.subFolder.map((subFolder) => (
<BookmarksFolderNode key={subFolder.id} folder={subFolder} />
))}
</div>
</div>
);
}
export default BookmarksFolderNode;
data.js
export const folders = [
{
id: 1,
depth: 1,
title: 'Bookmark 1',
subFolder: [
{
id: 1,
depth: 2,
title: 'SubBookmark 1',
subFolder: [
{
id: 1,
depth: 3,
title: 'SubBookmark 11',
},
{
id: 2,
depth: 3,
title: 'SubBookmark 22',
},
{
id: 3,
depth: 3,
title: 'SubBookmark 33',
subFolder: [
{
id: 1,
depth: 4,
title: 'SubBookmark 111',
},
],
},
],
},
{
id: 2,
depth: 2,
title: 'SubBookmark 2',
},
],
},
];
The idea is that, when you select a folder, you should deselect the others from parent component BookmarksFolder. The solution is a little bit tricky because you select the folder using BookmarksFolderNode local state (I mean folderIsOpen).
Well, lets start to use parent component's state to select/deselect folder:
import BookmarksFolderNode from './BookmarksFolderNode';
import classes from './BookmarksFolder.module.css';
import { folders } from '../../resources/data';
function BookmarksFolder() {
const [foldersSelected, setFoldersSelected] = useState(new Array(folders.length).fill(false));
const selectFolder = (index) => {
let result = new Array(folders.length).fill(false);
result[index] = true;
setFoldersSelected(result);
}
return (
<div className={classes.bookmarksFolder}>
{folders.map((folder, index) => (
<BookmarksFolderNode
key={folder.id}
folder={folder}
isSelected={foldersSelected[index]}
select={(index) => selectFolder(index)}
index={index}
/>
))}
</div>
);
}
export default BookmarksFolder;
So now in BookmarksFolderNode we have to use isSelected and select instead of folderIsOpen and openFolderHandler. Not only but we have to replicate the same logic for subfolders:
import { useState, useRef, useEffect } from 'react';
import { AiFillCaretDown, AiFillCaretRight } from 'react-icons/ai';
import Folder from '../../resources/img/folder.svg';
import OpenedFolder from '../../resources/img/opened_folder.svg';
import classes from './BookmarksFolderNode.module.css';
function BookmarksFolderNode(props) {
const [subfoldersSelected, setSubFoldersSelected] = useState(new Array(props.folder.subFolder.length).fill(false));
const tab = useRef();
const img = useRef();
const title = useRef();
useEffect(() => {
if (props.isSelected) {
tab.current.style.backgroundColor = '#1a73eb';
img.current.src = OpenedFolder;
title.current.style.color = '#1a73eb';
}
else {
tab.current.style.backgroundColor = 'black';
img.current.src = Folder;
title.current.style.color = 'black';
}
}, [props.isSelected])
const openFolderHandler = () => {
props.select(props.index)
};
const paddingLeft = 20 * (props.folder.depth - 1);
const selectSubfolder = (index) => {
let result = new Array(props.folder.subFolder.length).fill(false);
result[index] = true;
setSubFoldersSelected(result);
}
return (
<div className={classes.bookmarksFolderNode}>
<div className={classes.bookmarksMainFolderNode}>
<div className={classes.verticalTab} ref={tab}></div>
<div className={classes.innerContainer} style={{paddingLeft}}>
<div className={classes.icon} onClick={openFolderHandler}>
{props.isSelected ? (
<AiFillCaretDown className={classes.ironIcon} />
) : (
<AiFillCaretRight className={classes.ironIcon} />
)}
</div>
<img src={Folder} className={classes.folderIcon} ref={img} />
<div
className={classes.menuLabel}
onClick={openFolderHandler}
ref={title}
>
{props.folder.title}
</div>
</div>
</div>
<div className={classes.bookmarksSubFolderNode}>
{props.folder.subFolder &&
props.folder.subFolder.map((subFolder, index) => (
<BookmarksFolderNode key={subFolder.id} folder={subFolder} index={index} isSelected={subfoldersSelected[index]} select={(index) => selectSubfolder(index)} />
))}
</div>
</div>
);
}
export default BookmarksFolderNode;
So now, if you select a folder (or a subfolder) the others folder (or subfolders) will be deselected and the useEffect will apply the desidered css.
Your problem seems to be that you are just editing the currently clicked node. What do you need to do is in "selectFolderHandler" to reset the image of all other items.
One approach would be to iterate through each item, and set the image to initial, and then change the currently selected (not wise because of performance).
A suggested approach would be to find an open folder image (if it exists) and reset it. The simplest is to search by class or ref attribute.
I mapped an object array to create a tag element with the details being mapped onto the element. And then I created an animation so on render, the tags zoom in to full scale. However, I was wanting to take it to the next step and wanted to animate each tag individually, so that each tag is animated in order one after the other. To me, this seems like a common use of animations, so how could I do it from my example? Is there any common way to do this that I am missing?
import {LeftIconsRightText} from '#atoms/LeftIconsRightText';
import {LeftTextRightCircle} from '#atoms/LeftTextRightCircle';
import {Text, TextTypes} from '#atoms/Text';
import VectorIcon, {vectorIconTypes} from '#atoms/VectorIcon';
import styled from '#styled-components';
import * as React from 'react';
import {useEffect, useRef} from 'react';
import {Animated, ScrollView} from 'react-native';
export interface ICustomerFeedbackCard {
title: string;
titleIconName: string[];
tagInfo?: {feedback: string; rating: number}[];
}
export const CustomerFeedbackCard: React.FC<ICustomerFeedbackCard> = ({
title,
titleIconName,
tagInfo,
...props
}) => {
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
const zoomAnim = useRef(new Animated.Value(START_ZOOM_SCALE)).current;
/**
* Creates an animation with a
* set duration and scales the
* size by a set factor to create
* a small zoom effect
*/
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: FINAL_ZOOM_SCALE,
duration: FAST_ZOOM,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
/**
* Sorts all tags from highest
* to lowest rating numbers
* #returns void
*/
const sortTags = () => {
tagInfo?.sort((a, b) => b.rating - a.rating);
};
/**
* Displays the all the created tags with
* the feedback text and rating number
* #returns JSX.Element
*/
const displayTags = () =>
tagInfo?.map((tag) => (
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
return (
<CardContainer {...props}>
<HeaderContainer>
<LeftIconsRightText icons={titleIconName} textDescription={title} />
<Icon name="chevron-right" type={vectorIconTypes.SMALL} />
</HeaderContainer>
<ScrollOutline>
<ScrollContainer>
{sortTags()}
{displayTags()}
</ScrollContainer>
</ScrollOutline>
<FooterContainer>
<TextFooter>Most recent customer compliments</TextFooter>
</FooterContainer>
</CardContainer>
);
};
And here is the object array for reference:
export const FEEDBACKS = [
{feedback: 'Good Service', rating: 5},
{feedback: 'Friendly', rating: 2},
{feedback: 'Very Polite', rating: 2},
{feedback: 'Above & Beyond', rating: 1},
{feedback: 'Followed Instructions', rating: 1},
{feedback: 'Speedy Service', rating: 3},
{feedback: 'Clean', rating: 4},
{feedback: 'Accommodating', rating: 0},
{feedback: 'Enjoyable Experience', rating: 10},
{feedback: 'Great', rating: 8},
];
Edit: I solved it by replacing React-Native-Animated and using an Animated View and instead using Animatable and using an Animatable which has built in delay. Final solution:
const displayTags = () =>
tagInfo?.map((tag, index) => (
<TagContainer animation="zoomIn" duration={1000} delay={index * 1000}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
Here is a gif of the animation
This is an interesting problem. A clean way you could approach this problem is to develop a wrapper component, DelayedZoom that will render its child component with a delayed zoom. This component would take a delay prop that you can control to add a delay for when the component should begin animation.
function DelayedZoom({delay, speed, endScale, startScale, children}) {
const zoomAnim = useRef(new Animated.Value(startScale)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
delay: delay,
toValue: endScale,
duration: speed,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
return (
<Animated.View
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
{children}
</Animated.View>
);
}
After this, you can use this component as follows:
function OtherScreen() {
const tags = FEEDBACKS;
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
function renderTags() {
return tags.map((tag, idx) => {
const delay = idx * 10; // play around with this. Main thing is that you get a sense for when something should start to animate based on its index, idx.
return (
<DelayedZoom
delay={delay}
endScale={FINAL_ZOOM_SCALE}
startScale={START_ZOOM_SCALE}
speed={FAST_ZOOM}>
{/** whatever you want to render with a delayed zoom would go here. In your case it may be TagContainer */}
<TagContainer>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</DelayedZoom>
);
});
}
return <View>{renderTags()}</View>;
}
I hope this helps to point you in the right direction!
Also some helpful resources:
Animation delays: https://animationbook.codedaily.io/animated-delay/
Demo
It is a bit of work to implement this, I didn't have your components to try it out so I have created a basic implementation, I hope this will help
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View, Animated } from "react-native";
const OBJ = [{ id: 1 }, { id: 2 }, { id: 3 }];
const Item = ({ data, addValue }) => {
const zoomAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start(() => {
addValue();
});
};
zoomIn();
}, [zoomAnim]);
return (
<View>
<Animated.View
ref={zoomAnim}
style={[
{
transform: [{ scale: zoomAnim }]
}
]}
>
<Text style={styles.text}>{data}</Text>
</Animated.View>
</View>
);
};
function App() {
const [state, setState] = useState([OBJ[0]]);
const addValue = () => {
const currentId = state[state.length - 1].id;
if (OBJ[currentId]) {
const temp = [...state];
temp.push(OBJ[currentId]);
setState(temp);
}
};
return (
<View style={styles.app}>
{state.map((item) => {
return <Item data={item.id} key={item.id} addValue={addValue} />;
})}
</View>
);
}
const styles = StyleSheet.create({
text: {
fontSize: 20
}
});
export default App;
Basically, I am adding an element to the state at the end of the previous animation, one thing to note is that the key is very important, and don't use the index as a key. instead of Ids you might want to add any other value that is sorted or maybe link an item by passing the id of the previous item.
ADDING A SOLUTION USING REANIMATED AND MOTI
There is this library which you can use moti(https://moti.fyi/) it will work with reanimated, so you need to add reanimated too. Before using Reanimated you must consider that your normal chrome dev tools for that particular application will stop working with reanimated 2.0 and above you can use flipper though.
coming to the solution.
import { View as MotiView } from 'moti';
...
const displayTags = () =>
tagInfo?.map((tag, index) => (
<MotiView
key = {tag.id}
from={{ translateY: 20, opacity: 0 }}
animate={{ translateY: 0, opacity: 1 }}
transition={{ type: 'timing' }}
duration={500}
delay={index * 150}>
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</MotiView>
));
...
That's it, make sure to use a proper key, don't use index as key.
Side Note: If you are doubtful that sould you use reanimated or not, just go through https://docs.swmansion.com/react-native-reanimated/docs/ this page. Using Moti you can have really cool animation easily also if you reanimated version 2.3.0-alpha.1 then you need not to use Moti but as it is alpha version so it is not advisable to use in production you can wait for its stable release too.
I have list items, clicking a list item should turn it red, i want every clicked item to wait for 3000ms to restore its black color again, this is my code
my trial is to use setTimeout in useEffect and setting all red fields to false but this didn't work, especially i need to make every red item to wait 3000ms from turning red to turn black again
The first item to click turn to black after 3000ms correctly, but after that items going black faster!!
const App = () => {
const items = [
{
name: 'mark',
id: 1,
red: false
},
{
name: 'peter',
id: 2,
red: false
},
{
name: 'john',
id: 3,
red: false
}
]
const [names, setNames] = useState(items);
const turnItemRed= (id) => {
setNames(
names.map(i => i.id === id ? {...i, red: true} : i))
}
// this doesn't work
useEffect(() => {
setTimeout(() => {
setNames( prev => prev.map( i => ({...i, red: false})))
}, 3000)
})
return (
<div class="items-cont">
<ul class="items">
{
names.map(i => {
return (
<Item
item={i}
turnItemRed={turnItemRed}
/>
)
})
}
</ul>
</div>
)
}
const Item = ({ item, ...props }) => {
const { turnItemRed } = props;
return (
<li
className={`${item.red ? 'red' : ''}`}
onClick={() => {
turnItemRed(item.id)
}}
>
{item.name}
</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
CSS:
.red {
color: red
}
Because your code basically set all item back to original color at once.
Try the following code:
Note: This is simple way but the color when you can see it red then turn back to original color is not guarantee 3000 ms, because it take abit time to re-render component to show red
import React from "react";
import "./styles.css";
const App = () => {
const items = [
{
name: 'mark',
id: 1,
red: false
},
{
name: 'peter',
id: 2,
red: false
},
{
name: 'john',
id: 3,
red: false
}
]
const [names, setNames] = useState(items);
const turnItemRed= (id) => {
setNames(
names.map(i => i.id === id ? {...i, red: true} : i))
// move set timout here
setTimeout(() => {
setNames( prev => prev.map( i => i.id === id ? {...i, red: false} : i))
}, 3000)
}
return (
<div class="items-cont">
<ul class="items">
{
names.map(i => {
return (
<Item
item={i}
turnItemRed={turnItemRed}
/>
)
})
}
</ul>
</div>
)
}
const Item = ({ item, ...props }) => {
const { turnItemRed } = props;
return (
<li
className={`${item.red ? 'red' : ''}`}
onClick={() => {
turnItemRed(item.id)
}}
>
{item.name}
</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
First of all, your useEffect is running every 3 seconds, which means your component is re-rendering every 3 seconds with no UI actual updates.
useEffect(() => {
setTimeout(() => {
setNames( prev => prev.map( i => ({...i, red: false})))
}, 3000)
})
You need to change your useEffect to run only when names changes. Also make sure it runs only if some name has red property set to `true.
useEffect(() => {
if (names.some(item => item.red)) {
setTimeout(() => {
setNames(prev => prev.map(i => ({ ...i, red: false })));
}, 3000);
}
}, [names]);
I have a very simple example about useTransition, my expectation is whenever i click on the shuffle button, the items below swap around by a smooth animation. But i doesn't work, the item does swapping but also the pos property. I think my understand about key in useTransition has something wrong, but i can't find it.
my current code: https://codesandbox.io/s/wonderful-solomon-c0sve?file=/src/index.jsx
what im trying to do is something like this
function App() {
const [items, setState] = useState([
{ name: 'C' },
{ name: 'D' },
{ name: 'E' },
{ name: 'F' },
{ name: 'G' },
{ name: 'A' },
{ name: 'B' },
]);
let index = -1;
const gridItems = items.map((item) => {
index += 1;
return { ...item, pos: index * 60 };
});
const transitions = useTransition(gridItems, item => item.name, {
from: () => ({ pos: -100 }),
enter: ({ pos }) => ({ pos }),
udpate: ({ pos }) => ({ pos }),
leave: () => ({ pos: -100 }),
});
return (
<div>
This is app<br/>
<button onClick={ () => setState(Lodash.shuffle) }>shuffle</button><br/><br/>
<div>
{transitions.map(({ item, props, key }) => {
return (
<animated.div
key={key}
className="item"
style={{ transform: props.pos.interpolate(pos => `translateY(${pos}px)`) }}
>
{`${item.name}`}
</animated.div>
)
})}
</div>
</div>
)
}
It was an age to figuring it out. You made a typo.
Try with this one:
update: ({ pos }) => ({ pos }),