Smooth animation when turning off a conditionally rendered component in React
Looks like you need a simple Accordion thingy.
You could try something like that (snippet below).
One of the main moments here is setting the height
to the auto
value. It allows the content to change, and it won't strict its dimensions.
AccordionItem
conditionally renders its children. If it should be closed and the animation is over, then no children will be rendered.
const AccordionItem = (props) => {
const { className, headline, open, children } = props
const [height, setHeight] = React.useState(0)
const [isOver, setOver] = React.useState(false)
const bodyRef = React.useRef(null)
const getDivHeight = React.useCallback(() => {
const { height } = bodyRef.current ? bodyRef.current.getBoundingClientRect() : {}
return height || 0
}, [])
// set `auto` to allow an inner content to change
const handleTransitionEnd = React.useCallback(
(e) => {
if (e.propertyName === 'height') {
setHeight(open ? 'auto' : 0)
if (!open) {
setOver(true)
}
}
},
[open]
)
React.useEffect(() => {
setHeight(getDivHeight())
setOver(false)
if (!open) {
requestAnimationFrame(() => {
requestAnimationFrame(() => setHeight(0))
})
}
}, [getDivHeight, open])
const shouldHide = !open && isOver
return (
<div style={{overflow: 'hidden'}}>
<div
style={{ height, transition: "all 2s" }}
onTransitionEnd={handleTransitionEnd}
>
<div ref={bodyRef}>
{shouldHide ? null : children}
</div>
</div>
</div>
)
}
const App = () => {
const [open, setOpen] = React.useState(false)
return (
<div>
<button onClick={() => setOpen(isOpen => !isOpen)}>toggle</button>
<table style={{width: '100%'}}>
<tr>
<td>
Hot Pongal
<AccordionItem open={open}>
<button>-</button>
<input />
<button>+</button>
</AccordionItem>
</td>
<td>
Hot Pongal
<AccordionItem open={open}>
<button>-</button>
<input />
<button>+</button>
</AccordionItem>
</td>
</tr>
<tr>
<td>
Hot Pongal
<AccordionItem open={open}>
<button>-</button>
<input />
<button>+</button>
</AccordionItem>
</td>
<td>
Hot Pongal
<AccordionItem open={open}>
<button>-</button>
<input />
<button>+</button>
</AccordionItem>
</td>
</tr>
<tr>
<td>
Hot Pongal
<AccordionItem open={open}>
<button>-</button>
<input />
<button>+</button>
</AccordionItem>
</td>
<td>
Hot Pongal
<AccordionItem open={open}>
<button>-</button>
<input />
<button>+</button>
</AccordionItem>
</td>
</tr>
</table>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<div id="root"></div>
How to animate conditionally-rendered components?
yes - the answer found here effectively addresses the problem. To solve it, I moved the conditional logic of the component up, created appropriate variable, and encapsulated it inside a <Transition>
in render()
. If there is a lesson to be learned here, it is that <Transition>
from Reakt Motion UI Pack (and, perhaps, elsewhere) does not fire its leave
animation if surrounded by a conditional statement, making it impossible to use it together with ternary operator
if you don't want the false
component to be animated as well.
Conditional Rendering and ReactCSSTransitionGroup Animation
I've seen this same problem posted many times. In short: you need to conditionally render the children inside of <ReactCSSTransitionGroup>
, not <ReactCSSTransitionGroup>
itself. <ReactCSSTransitionGroup>
needs to mount once and then stay, it's the children that get added and removed.
content.js
class Content extends Component {
render() {
const transitionOptions = {
transitionName: "fade",
transitionEnterTimeout: 500,
transitionLeaveTimeout: 500
}
let theChild = undefined;
if (this.props.page === 'one') {
theChild = <Comp1 key="comp1" />;
} else {
theChild = <Comp2 key="comp2" />;
}
return (
<div>
<ReactCSSTransitionGroup {...transitionOptions}>
{theChild}
</ReactCSSTransitionGroup>
</div>
);
}
}
Note that you should also add a unique key
prop to each child inside of a <ReactCSSTransitionGroup>
. That helps the component identify which children are unique in order to properly animate them in and out.
Issues on animating a conditionally rendered component using React-spring
What you were missing is this:
return expand.map(({ item, props, key }) => (
item && <animated.div
// ...etc
When you're controlling the mounting of a single component with useTransition
, you need to conditionally render it based on the item
being passed. In your case, when it's false
it won't render (which will unmount if already mounted) and when it's true
it will render (mount if unmounted).
Here's a working sandbox forked from yours: https://codesandbox.io/s/infallible-agnesi-cty5g.
A little more info
The first argument to useTransition
is the list you want to transition. That watches for changes and sends back an array mapped with each item, a key and a style object (props
) based on whether the item is truthy (entering) or falsy (leaving). So for a transition that mounts/unmounts a single element, conditionally rendering based on the truthiness of the item is key.
Check out the examples again here and you'll see the differences between transitioning a list, a toggle between two elements, and a single item.
For a list, no need to check for the existence of the item because the array changes.
For toggling between two elements, you use the truthiness of item
to determine which element to render.
For a single element, item
determines whether to render at all. This means it won't mount initially when you default to false
, and will make sure you don't render 2 items whenever your isActive
value changes.
Related Topics
How to Override Bootstrap 3 Styles with External Custom CSS
How to Center Div with Col-Md-6
Overriding Bootstrap Variables in Rails with Bootstrap-Sass Gem
Remove the Material Stepper Header
Is There a Trick to Show Arial Black in Firefox
Align an Element to the Bottom of Window
Make a Pause During Infinite CSS3 Animation
CSS Border Shorthand When Each Border Has a Different Width
Javafx CSS Button with Image - How to Define the Size of the Image
Convert New Line /N to a Line Break in Angular
CSS - "Position: Fixed" Acting Like "Absolute" in Firefox
/ (Forward Slash) in CSS Style Declarations
CSS Background Sizing Polyfill
Is Position: Fixed Z-Index Relative to Its Parent's Z-Index
Angular Material 6 Grid List Align-Items and Justify-Content to Flex-Start