Passing Functional Components to React Spring's useTransition - javascript

I'm trying to write a React component which contains a panel of buttons, and each of the buttons needs to be able to be independently switched in and out depending on the state of the app. To do this I've created a useTransition, wrapped in a custom hook, which is supposed to trigger the transition when it recieves a new SVG functional component.
The issue I'm having is that while the buttons are currently transitioning in correctly, I can't seem to get the useTransition to swap them out when it receives a new component. It will remove one when the component is replaced with an empty array, which is intended, and a useEffect inside the hook triggers when a new component is passed in, but for some reason that change is not picked up by the useTransition.
I'm guessing it has something to do with object equality between the components but I don't know how to work around it without forcing the change with window.requestAnimationFrame to set the state twice. Here is a codesandbox with a minimal demo.
Parent Component:
import React, { useState } from "react";
import { a } from "react-spring";
import "./styles.css";
import PlaySVG from "./svgs/PlaySVG";
import SaveSVG from "./svgs/SaveSVG";
import SearchSVG from "./svgs/SearchSVG";
import TrashSVG from "./svgs/TrashSVG";
import { useVertTransition } from "./Hooks";
export default function ActionButtons() {
const svgs = {
play: <PlaySVG style={{ width: "100%", height: "auto" }} id="play" />,
search: <SearchSVG style={{ width: "80%", height: "auto" }} id="search" />,
save: <SaveSVG style={{ width: "80%", height: "auto" }} id="save" />,
trash: <TrashSVG style={{ width: "80%", height: "auto" }} id="trash" />
};
const [actions, setActions] = useState({
one: svgs.play,
two: svgs.search,
three: svgs.trash
});
const [slotOne] = useVertTransition(actions.one);
const [slotTwo] = useVertTransition(actions.two);
const [slotThree] = useVertTransition(actions.three);
function buttonHandler() {
setActions({
...actions,
one: [],
two: svgs.save
});
}
return (
<div className="container">
<div className="panel">
<div className="button-container">
{slotOne.map(({ item, props, key }) => (
<a.button key={key} style={props} onClick={buttonHandler}>
{item}
</a.button>
))}
</div>
<div className="button-container">
{slotTwo.map(({ item, props, key }) => (
<a.button key={key} style={props} onClick={buttonHandler}>
{item}
</a.button>
))}
</div>
<div className="button-container">
{slotThree.map(({ item, props, key }) => (
<a.button key={key} style={props} onClick={buttonHandler}>
{item}
</a.button>
))}
</div>
</div>
</div>
);
}
useVertTransition hook:
import React, { useEffect } from "react";
import { useTransition } from "react-spring";
export function useVertTransition(item) {
useEffect(() => {
console.log(item);
}, [item]);
const vertTransition = useTransition(item, null, {
from: { transform: "translateY(-20px) rotateX(-90deg)", opacity: 0 },
enter: { transform: "translateY(0px) rotateX(0deg)", opacity: 1 },
leave: { transform: "translateY(20px) rotateX(90deg)", opacity: 0 },
trail: 400,
order: ["leave", "enter"]
});
return [vertTransition];
}
SVG component example:
import React from "react";
function PlaySVG({ style }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" style={style} viewBox="0 0 24 24">
<path d="M7 6L7 18 17 12z" />
</svg>
);
}
export default PlaySVG;

You are right, that react-spring is not able to detect the difference between buttons. But it does not examine the items, it examine only the keys. So you have to add something for unique keys. So you may want to changed the input from svg to an object containing a name beside the svg.
const getSvg = name => ({ name, svg: svgs[name] });
const [actions, setActions] = useState({
one: getSvg("play"),
two: getSvg("search"),
three: getSvg("trash")
});
You can now add the key function:
const vertTransition = useTransition(item, svg => svg.name, {
And you should change the render:
{slotOne.map(({ item, props, key }) => (
<a.button key={key} style={props} onClick={buttonHandler}>
{item.svg}
</a.button>
))}
And now it works as intended. I think.
https://codesandbox.io/s/usetransition-demo-152zb?file=/src/ActionButtons.js:1253-1437
And I also learned a new trick, I never used the order property.

Related

Start animation in react-spring when component is in view

I am using react-spring for animation and all the animations start once the page is loaded. I want to control the start of the animation. The desired outcome is to let the components down in the screen start the animation once they are in view (i.e the user scrolled down). The code follows something like this :
const cols = [
/*Components here that will be animated ..*/
{component: <div><p>A<p></div> , key:1},
{component: <div><p>B<p></div> , key:2},
{component: <div><p>C<p></div> , key:3},
]
export default function foocomponent(){
const [items, setItems] = React.useState(cols);
const [appear, setAppear] = React.useState(false); // Should trigger when the component is in view
const transitions = useTransition(items, (item) => item.key, {
from: { opacity: 0, transform: 'translateY(70px) scale(0.5)', borderRadius: '0px' },
enter: { opacity: 1, transform: 'translateY(0px) scale(1)', borderRadius: '20px', border: '1px solid #00b8d8' },
// leave: { opacity: 1, },
delay: 200,
config: config.molasses,
})
React.useEffect(() => {
if (items.length === 0) {
setTimeout(() => {
setItems(cols)
}, 2000)
}
}, [cols]);
return (
<Container>
<Row>
{appear && transitions.map(({ item, props, key }) => (
<Col className="feature-item">
<animated.div key={key} style={props} >
{item.component}
</animated.div>
</Col>
))}
</Row>
</Container>
);
}
I tried using appear && transitions.map(...) but unfortunately that doesn't work. Any idea how should I control the start of the animation based on a condition?
I use https://github.com/civiccc/react-waypoint for this type of problems.
If you place this hidden component just before your animation. You can switch the appear state with it. Something like this:
<Waypoint
onEnter={() => setAppear(true) }
/>
You can even specify an offset with it. To finetune the experience.
If you wish to have various sections fade in, scroll in, whatever on enter, it's actually very simple to create a custom wrapper. Since this question is regarding React Spring, here's an example but you could also refactor this a little to use pure CSS.
// React
import { useState } from "react";
// Libs
import { Waypoint } from "react-waypoint";
import { useSpring, animated } from "react-spring";
const FadeIn = ({ children }) => {
const [inView, setInview] = useState(false);
const transition = useSpring({
delay: 500,
to: {
y: !inView ? 24 : 0,
opacity: !inView ? 0 : 1,
},
});
return (
<Waypoint onEnter={() => setInview(true)}>
<animated.div style={transition}>
{children}
</animated.div>
</Waypoint>
);
};
export default FadeIn;
You can then wrap any component you want to fade in on view in this FadeIn component as such:
<FadeIn>
<Clients />
</FadeIn>
Or write your own html:
<FadeIn>
<div>
<h1>I will fade in on enter</h1>
</div>
</FadeIn>

Clicking a left arrow or a right arrow to switch an element in an array with the element on the left or right React

I am new to React and am trying to build an app in which a user can create a card, delete a card, and change the order of the cards array by clicking left or right arrow to switch elements with the element on the left or on the right.
I am struggling to code this functionaliy. I have the function written to switch the card with that on the left, but this function is not doing anything right now. I also do not get any errors in the console from this function, so I really cannot determine where I am going wrong here.
Here is the code so far:
CardList.js will display the form to add a card and display the array of CardItems, passing the functions to switch these items to the left or right ('moveLeft', 'moveRight') as props.
import React from "react";
import CardItem from "./CardItem";
import CardForm from "./CardForm";
import './Card.css';
class CardList extends React.Component {
state = {
cards: JSON.parse(localStorage.getItem(`cards`)) || []
// when the component mounts, read from localStorage and set/initialize the state
};
componentDidUpdate(prevProps, prevState) { // persist state changes to longer term storage when it's updated
localStorage.setItem(
`cards`,
JSON.stringify(this.state.cards)
);
}
render() {
const cards = this.getCards();
const cardNodes = (
<div style={{ display: 'flex' }}>{cards}</div>
);
return (
<div>
<CardForm addCard={this.addCard.bind(this)} />
<div className="container">
<div className="card-collection">
{cardNodes}
</div>
</div>
</div>
);
}
addCard(name) {
const card = {
name
};
this.setState({
cards: this.state.cards.concat([card])
}); // new array references help React stay fast, so concat works better than push here.
}
removeCard(index) {
this.state.cards.splice(index, 1)
this.setState({
cards: this.state.cards.filter(i => i !== index)
})
}
moveLeft(index,card) {
if (index > 1) {
this.state.cards.splice(index, 1);
this.state.cards.splice((index !== 0) ? index - 1 : this.state.cards.length, 0, card)
}
return this.state.cards
}
moveRight(index, card) {
// ?
}
getCards() {
return this.state.cards.map((card) => {
return (
<CardItem
card={card}
index={card.index}
name={card.name}
removeCard={this.removeCard.bind(this)}
moveLeft={this.moveLeft.bind(this)}
moveRight={this.moveRight.bind(this)}
/>
);
});
}
}
export default CardList;
CardItem is taking in those props and ideally handling moving the card left or right in the array once the left or right icon is clicked.
import React from 'react';
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
class CardItem extends React.Component {
render() {
return (
<div>
<Card style={{ width: '15rem'}}>
<Card.Header as="h5">{this.props.name}</Card.Header>
<Card.Body>
<Button variant="primary" onClick={this.handleClick.bind(this)}>Remove</Button>
</Card.Body>
<Card.Footer style={{ display: 'flex' }}>
<i class="arrow left icon" onClick={this.leftClick.bind(this)} style={{ color: 'blue'}}></i>
<i class="arrow right icon" onClick={this.rightClick.bind(this)} style={{ color: 'blue'}}></i>
</Card.Footer>
</Card>
</div>
)
}
handleClick(index) {
this.props.removeCard(index)
}
leftClick(index, card) {
this.props.moveLeft(index, card)
}
rightClick(index, card) {
this.props.moveRight(index, card)
}
}
export default CardItem;
Not sure where I am going wrong here. Any help would be appreciated
Edit #1
Hey guys, so I wrote out a different function to handle moving the card to the left, and I decided to bind "this" to that method in the constructor because I was getting errors saying the program could not read it. However, I am still getting errors basically saying that everything is not defined when I pass the function from CardList to CardItem as props. Does anybody know what the problem is? I suspect its my syntax when I call the methods in CardItem.
CardList.js
import React from "react";
import CardItem from "./CardItem";
import CardForm from "./CardForm";
import './Card.css';
class CardList extends React.Component {
constructor(props) {
super();
this.moveLeft = this.moveLeft.bind(this);
this.moveRight = this.moveRight.bind(this);
this.state = {
cards: JSON.parse(localStorage.getItem(`cards`)) || []
// when the component mounts, read from localStorage and set/initialize the state
};
}
componentDidUpdate(prevProps, prevState) { // persist state changes to longer term storage when it's updated
localStorage.setItem(
`cards`,
JSON.stringify(this.state.cards)
);
}
render() {
const cards = this.getCards();
const cardNodes = (
<div style={{ display: 'flex' }}>{cards}</div>
);
return (
<div>
<CardForm addCard={this.addCard.bind(this)} />
<div className="container">
<div className="card-collection">
{cardNodes}
</div>
</div>
</div>
);
}
addCard(name) {
const card = {
name
};
this.setState({
cards: this.state.cards.concat([card])
}); // new array references help React stay fast, so concat works better than push here.
}
removeCard(index) {
this.state.cards.splice(index, 1)
this.setState({
cards: this.state.cards.filter(i => i !== index)
})
}
moveLeft(index, card) {
this.setState((prevState, prevProps) => {
return {cards: prevState.cards.map(( c, i)=> {
// also handle case when index == 0
if (i === index) {
return prevState.cards[index - 1];
} else if (i === index - 1) {
return prevState.cards[index];
}
})};
});
}
moveRight(index, card) {
// ?
}
getCards() {
return this.state.cards.map((card) => {
return (
<CardItem
card={card}
index={card.index}
name={card.name}
removeCard={this.removeCard.bind(this)}
moveLeft={this.moveLeft}
moveRight={this.moveRight}
/>
);
});
}
}
export default CardList;
CardItem.js
import React from 'react';
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
class CardItem extends React.Component {
render() {
return (
<div>
<Card style={{ width: '15rem'}}>
<Card.Header as="h5">{this.props.name}</Card.Header>
<Card.Body>
<Button variant="primary" onClick={this.handleClick.bind(this)}>Remove</Button>
</Card.Body>
<Card.Footer style={{ display: 'flex' }}>
<i class="arrow left icon" onClick={leftClick(index, card)} style={{ color: 'blue'}}></i>
<i class="arrow right icon" onClick={rightClick(index, card)} style={{ color: 'blue'}}></i>
</Card.Footer>
</Card>
</div>
)
}
handleClick(index) {
this.props.removeCard(index)
}
leftClick(index, card) {
this.props.moveLeft(index,card)
}
rightClick(index, card) {
this.props.moveRight(index, card)
}
}
export default CardItem;
To update state arrays in React, you shouldn't use splice, push or the [] operator.
Instead use the methods that return a new array object viz. map, filter, concat,slice.
For a detailed explanation, see this article.
So you can do something like :
moveLeft(index,card) {
this.setState((prevState, prevProps)=> {
return {cards: prevState.cards.map((c,i)=> {
// also handle case when index == 0
if(i == index) {
return prevState.cards[index-1];
} else if(i == index-1) {
return prevState.cards[index];
}
})};
});
}
When updating React state using the previous value, always use
setState((prevState,prevProps)=>{ return ...})
as such state updates may be asynchronous. See React docs.
Since you are calling the parent component method from child, it's better to bind these methods in the CardList constructor. Eg:
this.moveLeft = this.moveLeft.bind(this);
this.moveRight ....

onClick event binding (not a button)

I have a small React app that makes calls to a JSON file and returns the text of a card.
I have successfully made this work by calling the onClick on a button, but what I really want to do is add a onClick as an event to my Cards module I've created. I've looked at binding at this level but with no success;
<Cards name={card} onClick={() => setCard(Math.floor(Math.random() * 57) + 1)}/>
Do I need to write a custom handler in my cards.js to handle the onClick? Or can I append it within this file (App.js)?
The full code is below.
import React, { useState } from 'react';
import './App.css';
import Cards from '../cards';
function App() {
const [card, setCard] = useState('1');
return (
<div className="wrapper">
<h1>Card text:</h1>
<button onClick={() => setCard(Math.floor(Math.random() * 57) + 1)}>Nile</button>
<Cards name={card} />
</div>
);
}
export default App;
My card.js component code is;
export default function Cards({ name }) {
const [cardInformation, setCards] = useState({});
useEffect(() => {
getCards(name)
.then(data =>
setCards(data)
);
}, [name])
const FlippyOnHover = ({ flipDirection = 'vertical' }) => (
<Flippy flipOnHover={true} flipDirection={flipDirection}>
</Flippy>
);
return(
<div>
<Flippy flipOnHover={false} flipOnClick={true} flipDirection="vertical" style={{ width: '200px', height: '200px' }} >
<FrontSide style={{backgroundColor: '#41669d',}}>
</FrontSide>
<BackSide style={{ backgroundColor: '#175852'}}>
{cardInformation.continent}
</BackSide>
</Flippy>
<h2>Card text</h2>
<ul>
<li>{cardInformation.continent}</li>
</ul>
</div>
)
}
Cards.propTypes = {
name: PropTypes.string.isRequired
}
We can create an event. So let's call it onSetCard.
App.js file:
class App extends Component {
handleSetCard= () => {
// set state of card here
};
<Cards name={card}
card=card
onSetCard={this.handleSetCard}
/>
}
Cards.js file:
class Cards extends Component {
render() {
console.log("Cards props", this.props);
return (
<button
onClick={() => this.props.onSetCard(this.props.card)}
className="btn btn-secondary btn-sm m-2"
>
FooButton
</button>
)
}
}

Is it safe to useMemo for JSX?

I'm trying to do a memoize Modal and I have a problem here.
When I change input I dont need to re-render the Modal component.
For example:
Modal.tsx looks like this:
import React from "react";
import { StyledModalContent, StyledModalWrapper, AbsoluteCenter } from "../../css";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode
};
const ModalView: React.FC<ModalProps> = ({ open, onClose, children }) => {
console.log("modal rendered");
return (
<StyledModalWrapper style={{ textAlign: "center", display: open ? "block" : "none" }}>
<AbsoluteCenter>
<StyledModalContent>
<button
style={{
position: "absolute",
cursor: "pointer",
top: -10,
right: -10,
width: 40,
height: 40,
border: 'none',
boxShadow: '0 10px 10px 0 rgba(0, 0, 0, 0.07)',
backgroundColor: '#ffffff',
borderRadius: 20,
color: '#ba3c4d',
fontSize: 18
}}
onClick={onClose}
>
X
</button>
{open && children}
</StyledModalContent>
</AbsoluteCenter>
</StyledModalWrapper>
);
};
export default React.memo(ModalView);
Here is an example of how I wrap it.
import React from 'react'
import Modal from './modal';
const App: React.FC<any> = (props: any) => {
const [test, setTest] = React.useState("");
const [openCreateChannelDialog, setOpenCreateChannelDialog] = React.useState(false);
const hideCreateModalDialog = React.useCallback(() => {
setOpenCreateChannelDialog(false);
}, []);
return (
<>
<input type="text" value={test} onChange={(e) => setTest(e.target.value)} />
<button onClick={() => setOpenCreateChannelDialog(true)}>Create channel</button>
<Modal
open={openCreateChannelDialog}
onClose={hideCreateModalDialog}
children={<CreateChannel onClose={hideCreateModalDialog} />}
/>
</>
};
I know, Modal re-rendered because children reference created every time when App component re-renders (when I change an input text).
Know I'm interested, if I wrap <CreateChannel onClose={hideCreateModalDialog} /> inside React.useMemo() hook
For example:
const MemoizedCreateChannel = React.useMemo(() => {
return <CreateChannel onClose={hideCreateModalDialog} />
}, [hideCreateModalDialog]);
And change children props inside Modal
from:
children={<CreateChannel onClose={hideCreateModalDialog} />}
to
children={MemoizedCreateChannel}
It works fine, but is it safe? And it is only one solution that tried to memoize a Modal?
Memoizing JSX expressions is part of the official useMemo API:
const Parent = ({ a }) => useMemo(() => <Child1 a={a} />, [a]);
// This is perfectly fine; Child re-renders only, if `a` changes
useMemo memoizes individual children and computed values, given any dependencies. You can think of memo as a shortcut of useMemo for the whole component, that compares all props.
But memo has one flaw - it doesn't work with children:
const Modal = React.memo(ModalView);
// React.memo won't prevent any re-renders here
<Modal>
<CreateChannel />
</Modal>
children are part of the props. And React.createElement always creates a new immutable object reference (REPL). So each time memo compares props, it will determine that children reference has changed, if not a primitive.
To prevent this, you can either use useMemo in parent App to memoize children (which you already did). Or define a custom comparison function for memo, so Modal component now becomes responsible for performance optimization itself. react-fast-compare is a handy library to avoid boiler plate for areEqual.
Is it safe? Yes. At the end of the day the JSX is just converted into a JSON object, which is totally fine to memoize.
That said, I think it is stylistically a bit weird to do this, and I could foresee it leading to unexpected bugs in the future if things need to change and you don't think it through fully.

How to add classNames in conditional rendering in reactjs?

I have a list of data with images. I want to make image carousel. For this I have created card component and I want here to display 4 cards at a time and remaining should be hidden. Then i want to setTimeout of 5s to display remaining but only for at a time.
So far I have done this.
about.js
import './about.scss';
import data from '../../data/data';
import CardSection from './card';
class About extends React.Component{
constructor(props){
super(props);
this.state = {
properties: data.properties,
property: data.properties[0]
}
}
nextProperty = () => {
const newIndex = this.state.property.index+4;
this.setState({
property: data.properties[newIndex]
})
}
prevProperty = () => {
const newIndex = this.state.property.index-4;
this.setState({
property: data.properties[newIndex]
})
}
render() {
const {property, properties} = this.state;
return (
<div className="section about__wrapper">
<div>
<button
onClick={() => this.nextProperty()}
disabled={property.index === data.properties.length-1}
>Next</button>
<button
onClick={() => this.prevProperty()}
disabled={property.index === 0}
>Prev</button>
<Container className="card__container">
<div class="card__main" style={{
'transform': `translateX(-${property.index*(100/properties.length)}%)`
}}>
{
this.state.properties.map(property => (
<CardSection property={property}/>
))
}
</div>
</Container>
</div>
</div>
)
}
}
export default About
about.scss
.card__container{
overflow-x: hidden;
}
.card__main{
display: flex;
position: absolute;
transition: transform 300ms cubic-bezier(0.455, 0.03, 0.515, 0.955);
.card__wrapper {
padding: 20px;
flex: 1;
min-width: 300px;
}
}
card.js
import React from "react";
import { Card, CardImg, CardText, CardBody,
CardTitle, CardSubtitle, Button } from 'reactstrap';
class CardSection extends React.Component {
render() {
return (
<div className="card__wrapper">
<Card>
<CardImg top width="100%" src={this.props.property.picture} alt="Card image cap" />
<CardBody>
<CardTitle>{this.props.property.city}</CardTitle>
<CardSubtitle>{this.props.property.address}</CardSubtitle>
<CardText>Some quick example text to build on the card title and make up the bulk of the card's content.</CardText>
<Button>Button</Button>
</CardBody>
</Card>
</div>
);
}
}
export default CardSection;
I have added transition in them to change card onclick but i want them to auto change and hide the remaining card.
Right now it looks like this,
You can add items in componentDidMount method using setInterval
componentDidMount() {
this.interval = setInterval(() => this.setState({
properties:data.properties /* add your data*/ }), 4000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
You can have a property called showCardIds that holds an array of the Id of cards that need to be shown, and use that to set a Boolean property called hidden on the div of the card.
You could also do something like this as shown in the example below, this example also uses showCardIds as a state. It filters only for the property that needs to be rendered and filters out the rest.
Here is an example:
...
{
this.state.properties.filter((property, index) => showCardIds.includes(index)).map(property => (
<CardSection property={property}/>
))
}
...
That way only the ones that are present in the array of showCardIds would show up, there needs to be more logic to be written that would populate the ids in showCardIds
Hope this helps. The hidden property is supported from HTML5, and should work on most browsers, unless they are truly "ancient".

Categories