React-transition-group dynamic exit animation for SwitchTransition - reactjs

I'm trying to make an app with a dynamic exit animation. Exit animation for SwitchTransition is set when the component is rendered and can't be changed later. It can be achieved using childFactory and TransitionGroup like here, but in this way the animation happens simultaneously. I need to make react wait until previous is unmounted - like in SwitchTransition. So the question: is it possible to have something like childFactory for SwitchTransition or make TransitionGroup behave the same way as SwitchTransition mode 'out-in'. Thanks in advance!
export default function App() {
const [state, setState] = React.useState(true);
const [direction, setDirection] = React.useState("left");
return (
<div className="main">
<SwitchTransition mode="out-in">
<CSSTransition
key={state}
addEndListener={(node, done) => {
node.addEventListener("transitionend", done, false);
}}
classNames={direction === "left" ? "fade--left" : "fade--right"}
>
<div className="button-container">
<Button
onClick={() => {
setState((state) => !state);
const dir = Math.random() > 0.5 ? "right" : "left";
setDirection(dir);
}}
>
{direction}
</Button>
</div>
</CSSTransition>
</SwitchTransition>
</div>
);
}

Related

Close modal if another one is opened

Im trying to create a react Modal component from scratch. I would like to add the functionality of closing the modal when another one is opened.
I know the logic how to solve(i think i know), but cant implement it. My approach would be using context, where i store the current modal opened(currentModal) and if another one is opened then it would check if there is a currentModal and if so it would close it.
So far i have the modal component:
export function Modal({title, isOpen, children, onClose}){
return(
createPortal(
<trds-modal class={isOpen ? 'opened': ''} onClick={onClose}>
<trds-modal_container onClick={e => e.stopPropagation()}>
<trds-modal_header>
<h2>{title}</h2>
<Icon icon="x" onClick={onClose} />
</trds-modal_header>
<trds-modal_body>
{children}
</trds-modal_body>
</trds-modal_container>
</trds-modal>, document.body)
)
}
i figured it out.
Created a context provider where i store the id of the current modal opened.
export function ModalContextProvider({children}){
const [currentModalId, setCurrentModalId] = useState(null);
return(
<modalContext.Provider value={[currentModalId, setCurrentModalId]}>
{children}
</modalContext.Provider>
)
}
then in the modal component i generate a uniqe id and set the context's currentModalId to that. And if the currentModalId changes then the modal checks if that equals to the modalId. If not, it calls the onClose function.
export function Modal({title, isOpen, children, onClose}){
const modalId = useMemo(() => generateId(), []);
const [currentModalId, setCurrentModalId] = useContext(modalContext);
useEffect(() => {
if(isOpen){
setCurrentModalId(modalId);
}
}, [isOpen, setCurrentModalId, modalId]);
useEffect(() => {
if(currentModalId !== modalId) onClose();
}, [currentModalId, modalId, onClose]);
return(
createPortal(
<trds-modal class={isOpen ? 'opened': ''} onClick={onClose}>
<trds-modal_container onClick={e => e.stopPropagation()}>
<trds-modal_header>
<h2>{title}</h2>
<Icon icon="x" onClick={onClose} />
</trds-modal_header>
<trds-modal_body>
{children}
</trds-modal_body>
</trds-modal_container>
</trds-modal>, document.body)
)
}
I hope it help you as i couldn't find an approach to this problem. (maybe its not even an existing problem :D)

i want the data that is in form component to be shown when pickup button is clicked

i want the data that is in form component to be shown when pickup button is clicked. the data from myvehicles to be entered inside the pickup component, when the yes or no button is clicked. i have tried like this but it doesnot work.
import React,{ useState } from 'react'
import './Pickup.css'
import logo from './images/logo.png'
import { Link, useHistory } from "react-router-dom";
import InfoYes from './InfoYes'
import InfoNo from './InfoNo'
function Pickup() {
const [showInfoYes, setShowInfoYes] = useState(false);
const [showInfoNo, setShowInfoNo] = useState(false);
const history = useHistory()
return (
<div className="pickup">
<div className="vehicles__navbar">
<Link to="/" className="header__link">
<img
className="navbar__logo"
src={logo}
alt="logo"
/>
</Link>
<button className="navbar__button" onClick={() => history.push('/')}>Logout</button>
</div>
<div className="pickup__info">
<h1 className="h1__pickup">do you want bluebook pickup service?</h1>
<div className="pickup__button">
<button className="yes__button" onClick={() => setShowInfoYes(!showInfoYes)}>yes</button>
<button className="no__button" onClick={() => setShowInfoNo(!showInfoNo)}>no</button>
</div>
{showInfoYes && <InfoYes setShowInfoYes={setShowInfoYes} />} </div>
{showInfoNo && <InfoNo setShowInfoNo={setShowInfoNo} />}
</div>
)
}
export default Pickup
I think you may be able to achieve this through conditional rendering.
Which involves if statements and true & false conditions.
Please look at this documentation to see how this works. Documentation
(Reactjs.org has really great documentation, I really encourage you go through it.)
No where in your code, I see your state to be set TRUE for it to render.
You'll need to create an onClick handler to set this to true or false.
A quick example would be:
const [userClicked, setUserClicked] = useState(false);
{ userClicked ?
( <button onClick={() => setUserClicked(false)> No </button>
) : (
<button onClick={() => setUserClicked(true)> Yes </button>
)}
This is a shorthand of a ternary operator to produce an if/then statement in react.
It would be the easiest to accomplish your goal, although I do suggest that you research a little bit more on it!
{userClicked ? ( ) : ( ) }
You could also take out:
const [showInfoYes, setShowInfoYes] = useState(false);
const [showInfoNo, setShowInfoNo] = useState(false);
It's a bit redundant as you're trying to achieve something that could be done in a singular state.

state is not updating when the component re renders?

I'm making a Nextjs flashcard app. I'm passing a deck structure like this:
const deck = {
title: 'React 101',
flashcards: [flashcardOne, flashcardTwo],
};
as props to the Deck component. This component shows the first card in flashcards and a "next" button to increment the index and showing the next card in flashcards.
The Card component is very simple and shows the front and the back of the card depending of the state front.
This is what I got so far and it's working but if I click "next" when the card is showing the answer (flashcard.back), the next card is going to appear with the answer. And I'm not sure why, isn't the Card component re rendering when I click "next"? And if the component re renders, front is going to be set to true?
export default function Deck({ deck }) {
const [cardIndex, setCardIndex] = useState(0);
const { title, flashcards } = deck;
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>{title}</h1>
{cardIndex < flashcards.length ? (
<>
<div className={styles.grid}>
<Card flashcard={flashcards[cardIndex]} />
</div>
<button onClick={() => setCardIndex((cardIndex) => cardIndex + 1)}>
Next
</button>
</>
) : (
<>
<div>End</div>
<button>
<Link href='/'>
<a>Go to Home</a>
</Link>
</button>
<button onClick={() => setCardIndex(0)}>Play again</button>
</>
)}
</main>
</div>
);
}
export function Card({ flashcard }) {
const [front, setFront] = useState(true);
return (
<>
{front ? (
<div
className={`${globalStyles.card} ${styles.card}`}
onClick={() => setFront(false)}
>
<p className={styles.front}>{flashcard.front}</p>
</div>
) : (
<div
className={`${globalStyles.card} ${styles.card}`}
onClick={() => setFront(true)}
>
<p className={styles.back}>{flashcard.back}</p>
</div>
)}
</>
);
}
When state changes, the card will re-render, but it will not re-mount. So, existing state will not be reset.
Call setFront(true) when the flashcard prop has changed:
const [front, setFront] = useState(true);
useLayoutEffect(() => {
setFront(true);
}, [flashcard]);
I'm using useLayoutEffect instead of useEffect to ensure front gets set ASAP, rather than after a paint cycle (which could cause flickering).
You can also significantly slim down the Card JSX:
export function Card({ flashcard }) {
const [front, setFront] = useState(true);
const face = front ? 'front' : 'back';
return (
<div
className={`${globalStyles.card} ${styles.card}`}
onClick={() => setFront(!front)}
>
<p className={styles[face]}>{flashcard[face]}</p>
</div>
);
}
Okay, I guess I had the same issue. Since you're using functional components, and you're re-using the same component or in better words, you're not unmounting and remounting the component really, you're just changing the props, this happens. For this, you need to do useEffect() and then setFront(true).
Here's the code I used in my App.
useEffect(() => {
setFront(true);
}, [flashcard]);
This is what I have used in my Word.js file.

Unexpected Behavior After State Change in React Component

RenderImages = (): React.ReactElement => {
let selected = this.state.results.filter(x=>this.state.selectedGroups.includes(x.domain))
console.log(selected)
return(
<div className="results_wrapper">
{selected.map((r,i)=>{
let openState = (this.state.selectedImage==i)?true:false;
return(
<RenderPanel panelType={PanelType.large} openState={openState} title={r.domain+'.TheCommonVein.net'} preview={(openIt)=>(
<div className="result" onClick={openIt} style={{ boxShadow: theme.effects.elevation8}}>
<img src={r.url} />
</div>
)} content={(closeIt)=>(
<div className="panel_wrapper">
<div className="panel_content">{r.content}</div>
{this.RenderPostLink(r.domain,r.parent)}
<div onClick={()=>{
closeIt();
this.setState({selectedImage:2})
console.log('wtfff'+this.state.selectedImage)
}
}>Next</div>
<img src={r.url} />
</div>
)}/>
)
})}
</div>
)
}
When I change the state of 'selectedImage', I expect the variable 'openState' to render differently within my map() function. But it does not do anything.
Console.log shows that the state did successfully change.
And what is even stranger, is if I run "this.setState({selectedImage:2})" within componentsDidMount(), then everything renders exactly as expected.
Why is this not responding to my state change?
Update
I have tried setting openState in my component state variable, but this does not help either:
RenderImages = (): React.ReactElement => {
let selected = this.state.results.filter(x=>this.state.selectedGroups.includes(x.domain))
console.log(selected)
let html = selected.map((r,i)=>{
return(
<RenderPanel key={i} panelType={PanelType.large} openState={this.state.openState[i]} title={r.domain+'.TheCommonVein.net'} preview={(openIt)=>(
<div className="result" onClick={openIt} style={{ boxShadow: theme.effects.elevation8}}>
<img src={r.url} />
</div>
)} content={(closeIt)=>(
<div className="panel_wrapper">
<div className="panel_content">{r.content}</div>
{this.RenderPostLink(r.domain,r.parent)}
<div onClick={()=>{
closeIt();
let openState = this.state.openState.map(()=>false)
let index = i+1
openState[index] = true;
this.setState({openState:openState},()=>console.log(this.state.openState[i+1]))
}
}>Next</div>
<img src={r.url} />
</div>
)}/>
)
})
return(
<div className="results_wrapper">
{html}
</div>
)
}
https://codesandbox.io/s/ecstatic-bas-1v3p9?file=/src/Search.tsx
To test, just hit enter at the search box. Then click on 1 of 3 of the results. When you click 'Next', it should close the pane, and open the next one. That is what I'm trying to accomplish here.
#Spitz was on the right path with his answer, though didn't follow through to the full solution.
The issue you are having is that the panel's useBoolean doesn't update it's state based on the openState value passed down.
If you add the following code to panel.tsx, then everything will work as you described:
React.useEffect(()=>{
if(openState){
openPanel()
}else{
dismissPanel();
}
},[openState, openPanel,dismissPanel])
What this is doing is setting up an effect to synchronize the isOpen state in the RenderPanel with the openState that's passed as a prop to the RenderPanel. That way while the panel controls itself for the most part, if the parent changes the openState, it'll update.
Working sandbox
I believe it's because you set openState in your map function, after it has already run. I understand you think the function should rerender and then the loop will run once more, but I think you'll need to set openState in a function outside of render.
The problem is that even though you can access this.state from the component, which is a member of a class component, there's nothing that would make the component re-render. Making components inside other components is an anti-pattern and produces unexpected effects - as you've seen.
The solution here is to either move RenderImages into a separate component altogether and pass required data via props or context, or turn it into a normal function and call it as a function in the parent component's render().
The latter would mean instead of <RenderImages/>, you'd do this.RenderImages(). And also since it's not a component anymore but just a function that returns JSX, I'd probably rename it to renderImages.
I tire to look at it again and again, but couldn't wrap my head around why it wasn't working with any clean approach.
That being said, I was able to make it work with a "hack", that is to explicitly call openIt method for selectedImage after rendering is completed.
RenderImages = (): React.ReactElement => {
let selected = this.state.results.filter((x) =>
this.state.selectedGroups.includes(x.domain)
);
return (
<div className="results_wrapper">
{selected.map((r, i) => {
let openState = this.state.selectedImage === i ? true : false;
return (
<RenderPanel
key={i}
panelType={PanelType.medium}
openState={openState}
title={r.domain + ".TheCommonVein.net"}
preview={(openIt) => {
/* This is where I am making explicit call */
if (openState) {
setTimeout(() => openIt());
}
/* changes end */
return (
<div
className="result"
onClick={openIt}
style={{ boxShadow: theme.effects.elevation8 }}
>
<img src={r.url} />
</div>
);
}}
content={(closeIt) => (
<div className="panel_wrapper">
<div className="panel_content">{r.content}</div>
{this.RenderPostLink(r.domain, r.parent)}
<div
onClick={() => {
closeIt();
this.setState({
selectedImage: i + 1
});
}}
>
[Next>>]
</div>
<img src={r.url} />
</div>
)}
/>
);
})}
</div>
);
};
take a look at this codesandbox.

React Countup trigger start when state is true

I would like to start the Counter when the related state value is true.
According to the document, the counter can be triggered with a button for example if the button is inside the Component. I would like to find a way how to start is from outside of the Component. I can't figure it out how to trigger the start.
<CountUp start={0} end={100}>
{({ countUpRef, start }) => (
<div>
<span ref={countUpRef} />
<button onClick={start}>Start</button>
</div>
)}
</CountUp>
Reference: https://www.npmjs.com/package/react-countup
I think this is said in the documentation
<CountUp start={0} end={100} delay={0}>
{({ countUpRef }) => (
<div>
<span ref={countUpRef} />
</div>
)}
</CountUp>
https://www.npmjs.com/package/react-countup#autostart-with-render-prop
EDIT: You can use the useCountUp hook
const { countUp, start, pauseResume, reset, update } = useCountUp(...)
Then you can get the start function and trigger it from outside. Below a simplified example from the doc.
import { useCountUp } from 'react-countup';
const ExternalComponent = () => {
const { countUp, start } = useCountUp({
start: 0,
end: 1234567,
});
useEffect(()=> start(), [])
return (
<div>
<div>{countUp}</div>
</div>
);
};
Here I trigger after the first render but you can easily put other condition on it.
if you pass redraw={true} as a prop to CountUp component ,then it will rerendered after any state change (or after any render)

Resources