'How can I use React Material UI's transition components to animate adding an item to a list?
I have this class.
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.add = this.add.bind(this);
this.clear = this.clear.bind(this);
}
add() {
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
}
clear() {
this.setState({ items: [] });
}
render() {
return (
<div>
<div>
<button onClick={this.add}>Add</button>
<button onClick={this.clear}>Clear</button>
</div>
{/* This is wrong, not sure what to do though... */}
<Collapse in={this.state.items.length > 0}>
<ul>{this.state.items}</ul>
</Collapse>
</div>
);
}
}
Sandbox link: https://codesandbox.io/s/material-demo-ggv04?file=/Demo.js
I'm trying to make it so that every time I click the "add" button, a new item gets animated into existence at the top of the list and the existing items get pushed down. Not sure how to proceed though.
Extra Resources
- Example of what I'm trying to achieve: https://codeburst.io/yet-another-to-do-list-app-this-time-with-react-transition-group-7d2d1cdf37fd
- React Transition Group Transition docs: http://reactcommunity.org/react-transition-group/transition (which seem to be used internally by
Collapse
)
Solution 1:[1]
I updated your Sandbox code to achieve what you wanted, but I don't think MaterialUI is the best library for that (I could be missing a better way to do it).
The challenge is that when you add a new item, that doesn't exist in the DOM yet. And most of those animation libraries/components require the element to be in the DOM and they just "hide" and "show" it with a transition time.
I had a similar situation and after some research, the better library I found that can handle animation for elements that are not yet in the DOM, was the Framer Motion. (You can check their documentation for mount animations)
Anyway, here is the link for the new Code Sandbox so you can take a look. The changes I made:
Removed random key
In the map
function that creates your list using the <Collapse />
component, there was a function to get a random integer and assign that as a key
to your component. React needs to have consistent keys to properly do its pretenders, so removing that random number fixes the issue where your "Toggle" button wasn't animating properly. (If your list of items doesn't have an unique ID, just use the index of the map
function, which is not a good solution, but still better than random numbers).
<Collapse key={i} timeout={this.state.collapseTimeout} in={this.state.open}>
{it}
</Collapse>
Added a new function to control the toggle
The approach here was: add the item in your list and, after the element is in the DOM, close the <Collapse />
, wait a little bit and open it again (so you can visually see the animation). In order to do that, we needed a new "toggle" function that can explicit set the value of the collapse.
toggleValue(value) {
this.setState(() => {
return {
open: value
};
});
}
Added a variable timeout for the collapse
The last issue was that, closing the <Collapse />
when the new item is added, was triggering the animation to close it. The solution here was to dynamically change the timeout of the collapse, so you don't see that.
setCollapseTimeout(value) {
this.setState(() => {
return {
collapseTimeout: value
};
});
}
When adding the element to the list, wait to trigger the animation
Again, to work around the issue with elements not yet in the DOM, we need to use a setTimeout
or something to wait to toggle the <Collapse />
. That was added in your add()
function.
add() {
this.toggleValue(false);
this.setCollapseTimeout(0);
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
setTimeout(() => {
this.setCollapseTimeout(300);
this.toggleValue(true);
}, 100);
}
Again, this is a hacky solution to make <Collapse />
from MaterialUI work with elements that are not yet in the DOM. But, as mentioned, there are other libraries better for that.
Good luck :)
Solution 2:[2]
Ended up here earlier on and then came back to create a sandbox showing hopefully a simple method for this scenario. The material-ui
docs are a bit (lot) light in this area and I was fighting with a very similar situation, but I tried something with TransitionGroup
from react-transition-group
, crossed my fingers and it seemed to work.
Forked CodeSandbox with TransitionGroup
The gist is that you
- wrap all of the components you want to transition in the
<TransitionGroup>
component - Inside the
TransitionGroup
, put in the "condition" (logic or loop output) for the data you want to render - Wrap the individual components you want to transition with transition component of your choice -
<Collapse>
in this example
e.g. In its most simple setup where "items" is an array of unique numbers coming from either props, state or a redux store
<TransitionGroup>
{items.map(item => (
<Collapse key={item}>
I am item {item}
</Collapse>
))}
</TransitionGroup>
With this setup I have found that I didn't need to put any props on the TransitionGroup
or Collapse
, and the TransitionGroup
handled all the mounting and unmounting in the loop rendering. Material UI doesn't produce the lightest of HTML output, but I guess it's all rendered on the fly so maybe that makes it better (unless you have thousands of elements, then things start to drag).
You can even go a step further and wrap the whole thing in another TransitionGroup
to cover situations where you want to remove the whole thing without transitioning all of the individual items - in this instance I switched it to a <Slide>
. I was absolutely certain that this wouldn't work, but it seemed to not care. You can also try and be semantic and use the "component" property rather than wrapping in another element e.g.
{items.length > 0 && (
<TransitionGroup>
<Slide>
<TransitionGroup component="ul">
{items.map((item) => (
<Collapse component="li" key={item}>I am item {item}</Collapse>
))}
</TransitionGroup>
</Slide>
</TransitionGroup>
)}
I have changed the sandbox in the following ways
- Included
TransitionGroup
fromreact-transition-group
- Changed the "add" logic so that the components aren't part of the "items" array - the array only contains the data required to render the components
- I have added a simple "count" and pushed that to the array to give the items a unique index (had originally used
Math.random
, but I wanted a "prettier" output). Generally your items will probably be coming from a database somewhere where a unique id will already be set. - Rendered the components in a loop based on the data in the array (this could be done in a separate function, but the gist is that the components aren't being stored in the array)
- added a "delete" function to show the removal of single items
- wrapped the whole group in a second
<TransitionGroup>
to show that the unmounting can happen in a group level - Put in some simple styling to get a better idea of the effect. You could use Material UI components here, but just wanted to keep it simple.
Hope this helps someone in the future.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |