How to animate mapped elements one at a time in react-native? - javascript

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.

Related

React-spring not animating as expected

So I'm using react-visibility-sensor with react-spring to animate text sliding char by char from any side I want.
In my home page the animation is running smoothly, I use it twice one from the right side and another from the top side.
When I switch routes and go to another page the animation does not work.
I have my code divided in an "Title" component "Char" component and a custom hook "useAnimatedText".
Title component:
import React from "react";
import VisibilitySensor from "react-visibility-sensor";
import useAnimatedText from "../../hooks/useAnimatedText";
import Char from './Char'
const Title = ({title, side}) => {
// HERE I CALL A CUSTOM HOOK THAT WILL DIFINE IF THE ELEMENT IS VISIBLE OR NOT
// AND HANDLE THE ANIMATION WHEN NECESSARY
const [isVisible, onChange, objArray] = useAnimatedText(title)
let elements = objArray.map((item, i) => {
return(
<Char
key={i}
isVisible={isVisible}
item={item}
delay={400 + (i * 40)}
side={side}
/>
)
})
console.log(isVisible)
return(
<VisibilitySensor onChange={onChange} >
<span className="title-box">
<h1 className="my-heading divided-heading">
{elements}
</h1>
<hr className="title-ruller"></hr>
</span>
</VisibilitySensor>
)
}
export default Title
Char component:
import { useSpring, animated } from "react-spring"
const Char = (props) => {
const { isVisible, item, delay, isBouncy, side} = props
const [ref, addBounce] = useBounce()
let springConfig = {}
if (side === 'right') {
springConfig = {
to: {
opacity: isVisible ? 1 : 0,
translateX : isVisible ? '0px' : '1000px'
},
config: { mass:2, tension: 200, friction: 30},
delay: delay
}
}
else if (side === 'top') {
springConfig = {
to: {
opacity: isVisible ? 1 : 0,
translateY: isVisible ? '0px' : '-500px'
},
config:{ mass:2, tension: 250, friction: 35},
delay: delay
}
}
const spring = useSpring({...springConfig})
return(
<animated.span
style={ spring }
className={isVisible ? 'is-visible' : 'is-not-visible'}
>
{item.char === ' ' ? <span> </span> : item.char}
</animated.span>
)
}
export default Char
This is the custom Hook:
import { useState } from "react";
import { stringToArray } from '../helpers'
// HOOK THAT HANDLES THE TEXT ANIMATION BY SETTING A STATE OF VISIBILITY
function useAnimatedText(string) {
const [isVisible, setVisibility] = useState(false);
const onChange = visiblity => {
visiblity && setVisibility(visiblity);
};
let objArray = stringToArray(string)
return [isVisible, onChange, objArray]
}
export default useAnimatedText
I did a console.log(isVisible) and the value was true but it was rendering in the page the spring values as if it was false(not visible).
I really can´t understand where I'm going wrong here, the only problem I have is when I'm not at my main route, could it be because of react-router-dom?
If someone has any clue, let me know.

Rendering an array of data in a carousel component

I'm working on a carousel in React JS which currently is rendering a set of numbers:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Wrapper,
NumbersWrapper,
NumbersScroller,
NumberText
} from "./Numbers.style";
const hundred = new Array(100)
.fill(0)
.map((k, v) => ({ key: v, label: v + 1 }));
class Carousel extends Component {
constructor(props) {
super(props);
this.state = {
activeNumber: 0
};
}
setActiveNumber(number) {
this.setState({
activeNumber: number
});
}
render() {
const { activeNumber } = this.state;
const intAciveNumber = Number(activeNumber);
return (
<Wrapper>
<NumbersWrapper
onClick={() => this.setActiveNumber(intAciveNumber + 1)}
>
<NumbersScroller
style={{
left: `${130 - intAciveNumber * 55}px`
}}
>
{hundred.map(({ key, label }) => {
const isNeighbor =
key + 1 === activeNumber || key - 1 === activeNumber;
const isActive = key === activeNumber;
return (
<NumberText
key={key}
isNeighbor={isNeighbor}
isActive={isActive}
>
{label}
</NumberText>
);
})}
</NumbersScroller>
</NumbersWrapper>
</Wrapper>
);
}
}
export default Carousel;
Now I'd like to use display custom numbers from an array like this example using the new keyword:
const hundred = new Array([100, 150, 200, 250, 400, 500, 650, 780, 830, 900, 1500])
.fill(0)
.map((k, v) => ({ key: v, label: v }));
However, when I make this change to the array, nothing happens. How can I correctly display the values inside of the component? If there is an NPM library that does this for React (web), I would take that as a solution also. Thanks in advance.
I have been using this a lot:
https://react-slick.neostack.com/
Works like a charm - and very customizable.
Something along the lines of this would probably work..
import React, { Component } from "react";
import Slider from "react-slick";
export default class SimpleSlider extends Component {
render() {
const settings = {
dots: true,
infinite: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1
};
return (
<div>
<h2> Single Item</h2>
<Slider {...settings}>
{hundred.map(({label}) => (
<div>
{label}
</div>
))}
</Slider>
</div>
);
}
}
You need to pass the values as separate parameters instead of an array like so.
const hundred = new Array(100, 150, 200, 250, 400, 500, 650, 780, 830, 900, 1500)
.fill(0)
.map((k, v) => ({ key: v, label: v }));
Right now, you are creating an array within the hundred array at position zero, so when you iterate over the objects with .map, you are only going over a single value, which contains the whole array.
Output of your current implementation of array initialization:
How it works with the change above:

Cant access property inside map function with spring transitions in ReactJs

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>
))}
);

How to display each element of an array with different timeout delay and each delay time being a value inside each element in react

Every element of the array should be displayed for some time and the time for which each element is displayed should be determined by a value in each element.
let array=[{display:"a",time:10},{display:"b",time:15},{display:"c",time:22}]
class App extends React.Component{
state={stateDisplay:"",
stateTime:""
}
componentWillMount(){
var i=0;
let handle=setInterval(()=>{
var element= array[i]
this.setState({
stateDisplay:element.display,
stateTime:element.time,
})
i=i+1;
if(i===array.length){
clearInterval(handle)
}
},10000)
}
render(){
return(
<div> {this.state.stateDisplay} </div>
)}}
i have done something like this but using setinterval the delay can only be set for a constant time,here 10s.
I want the first element to display for 10s and then the next element for 15s, third for 22s which is the time value for each element of the array.
I know i cant do that using setinterval is there a way to do this using Settimeout?
This was almost like a little challenge, heres what i managed to come up with, its in typescript, if you need js, just remove interfaces and type annotations
/* eslint-disable #typescript-eslint/no-explicit-any */
/* eslint-disable prettier/prettier */
/* eslint-disable no-shadow */
/* eslint-disable no-console */
import React, { FC, useState, useEffect, useCallback } from 'react';
import { View, Button, Text } from 'react-native';
interface Data {
duration: number;
bgColor: string;
}
const dataArr: Data[] = [
{ duration: 3, bgColor: 'tomato' },
{ duration: 6, bgColor: 'skyblue' },
{ duration: 9, bgColor: 'gray' },
];
const Parent = () => {
const [currentIdx, setCurrentIdx] = useState<number>(0);
const [elementData, setElementData] = useState<Data>(dataArr[currentIdx]);
useEffect(() => {
console.log('idx', currentIdx);
if (currentIdx > dataArr.length) return;
setElementData({ ...dataArr[currentIdx] });
}, [currentIdx]);
const pushNext = () => {
setCurrentIdx(currentIdx + 1);
};
const handleRestart = () => {
setCurrentIdx(0);
setElementData({ ...dataArr[0] });
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Timer
data={elementData}
onCountDownComplete={pushNext}
restart={handleRestart}
/>
</View>
);
};
interface Props {
data: Data;
onCountDownComplete: () => void;
restart: () => void;
}
const Timer: FC<Props> = ({ data, onCountDownComplete, restart }) => {
const [seconds, setSeconds] = useState<number>(data.duration);
// update on data change
useEffect(() => {
setSeconds(data.duration);
}, [data]);
const callback = useCallback(() => {
onCountDownComplete();
}, [onCountDownComplete]);
useEffect(() => {
let interval: any = null;
if (seconds > -1) {
interval = setInterval(() => {
if (seconds - 1 === -1) {
callback();
} else {
setSeconds(seconds - 1);
}
}, 1000);
} else {
return;
}
return () => {
clearInterval(interval);
};
}, [seconds, callback]);
return (
<View
style={{ backgroundColor: data.bgColor, padding: 16, borderRadius: 10 }}
>
<Text style={{ marginBottom: 24 }}>{seconds}</Text>
<Button title="restart" onPress={restart} />
</View>
);
};

How to preprocess fetched redux data for a React Native component?

I have a react-native application built with expo.
It's a budgeting app. I am trying to display a pie chart of the types of expenditure for the given month.
My logic is as follows:
Retrieve expenditures from redux via a dispatch() in useEffect
Push the expenditure along with other fields to an array that will be supplied to the pie chart.
Supply to Pie chart with that array as a prop.
What I'm experiencing:
Retrieve expenditures from redux via dispatch() in useEffect
Attempt to push expenditure with other fields to data array.
Try and supply this array to Pie chart.
Pie chart renders completely blank. (Logging the array at this point shows it's empty also)
(Logging the array in the useEffect hook show's the non empty array)
My code:
import React, {useEffect} from 'react';
import { StyleSheet, StatusBar, View, Text, AsyncStorage, Button, Dimensions } from 'react-native';
import {useSelector,useDispatch} from 'react-redux';
import {getExp, clearExp} from './../actions/expActions.js';
import _uniqueId from 'lodash/uniqueId';
import { getRecurrExp } from '../actions/recurrExpActions.js';
import { PieChart } from "react-native-chart-kit";
export default function Report() {
const expR = useSelector(state => state.expR)
const recurrExpR = useSelector(state => state.recurrExpR)
const dispatch = useDispatch();
const screenWidth = Dimensions.get("window").width
const chartConfig ={
backgroundColor: "#e26a00",
backgroundGradientFrom: "#fb8c00",
backgroundGradientTo: "#ffa726",
decimalPlaces: 2, // optional, defaults to 2dp
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
style: {
borderRadius: 16
},
propsForDots: {
r: "6",
strokeWidth: "2",
stroke: "#ffa726"
}
}
var piePieces = [];
const getAllExps = () => {
dispatch(getExp())
dispatch(getRecurrExp())
}
useEffect(() => {
getAllExps()
expR.catCounts.map(cat => {
piePieces.push({value: cat.count / expR.cat * 100, name: cat.category, color: cat.category==="cat1" ? '#E38627' : '#C13C37' })
})
console.log(piePieces) //Log's a filled array
},[])
// Deprecated, saving for
const clearAsyncStorage = async() => {
AsyncStorage.clear()
}
const clearExpTest = () => {
dispatch(clearExp())
}
return (
<View style={styles.main}>
<View style={styles.container}>
<StatusBar hidden />
{
<PieChart
data={piePieces}
width={220}
height={220}
chartConfig={chartConfig}
accessor="value"
backgroundColor="transparent"
paddingLeft="15"
absolute
/>
}
{console.log(piePieces)} //Logs []
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: '#ffffe6',
},
main: {
flex: 1,
}
});
My guess is that this has something to do with re-rendering your component. Since you're pushing onto piePieces and not reassigning it will not re-render.
I also recommend to use useSate for piePieces to circumvent this exact problem.
Add useState to your imports
import React, {useEffect, useState} from 'react';
Top of your component define piePieces
const [piePieces, setPiePiecse] = useState([]);
You can use forEach instead of map since map returns a new array with the returned values
expR.catCounts.forEach(cat => {
setPiePieces([...piePieces, {value: cat.count / expR.cat * 100, name: cat.category, color: cat.category==="cat1" ? '#E38627' : '#C13C37' }])
})
For anyone in future, Bloatlords answer is correct but will give rise to an async issue with setting state in a for each loop.
In order to fix this, the correct way to set state is:
setPiePieces(piePieces => [...piePieces, {value: ((cat.count / expR.cat) * 100),
name: cat.category,
color: (cat.category.localeCompare("cat1") ? "green" : "yellow"),
legendFontColor: "black", legendFontSize: 15}
])

Categories

Resources