-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Endless componentWillUnmount loop with <Transition> when another update is triggered #136
Comments
Something is super fishy with the lifecycle hooks. Here's what happens when I keep smashing the Toggle Panel button with line 33 removed http://www.prinzhorn.me/vokoscreen-2018-06-28_12-22-37.mp4 I don't think they should stack/queue up like that. |
Looks like some discussion regarding the second issue is already happening in #134, so we should focus on the endless loop in this issue. |
It looks like it pushes the same key for all elements, can that be? |
What is pushing keys to where? If I log |
I’ll take a look later on. |
I found going back to v4 helped. Whilst v4 doesn't seem to be able to do the no-key feature, this worked just fine:
|
@Prinzhorn could you map this into a codesandbox? A lot of issues crept up while i was away, would make it easier on my part to get on top of it all. :-) |
@Prinzhorn i've looked through it, it's a bug in your code. First, you shouldn't use transition for a sidebar, it's a basic spring you're looking for. But since you do, you actually mount and unmount that bar on every change of this.state.showPanel. The infinite loop that's now caused is also in your app, you call this.props.onDataChange('bar') in componentWillUnmount, which then calls setState. Here's a fixed example: import React, { Component } from 'react'
import { Transition, animated } from 'react-spring'
import logo from './logo.svg'
import './App.css'
class Panel extends Component {
constructor(props) {
super(props)
this.state = { value: props.data }
}
handleChange = e =>
this.setState({ value: e.target.value }, () =>
this.props.onDataChange(this.state.value)
)
componentDidMount() {
console.log('componentDidMount')
}
componentDidUpdate() {
console.log('componentDidUpdate')
}
componentWillUnmount() {
console.log('componentWillUnmount')
}
render() {
console.log('render')
return (
<animated.div style={this.props.style} className="panel">
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
</animated.div>
)
}
}
export default class App extends Component {
state = { showPanel: false, data: 'foo' }
togglePanel = () =>
this.setState(prevState => ({ showPanel: !prevState.showPanel }))
handleDataChange = data => this.setState({ data })
render() {
return (
<div className="App">
<h1>{this.state.data}</h1>
<p className="App-intro">
<button onClick={this.togglePanel}>Toggle Panel</button>
</p>
<Transition
native
from={{ x: '-100%' }}
enter={{ x: '0%' }}
leave={{ x: '-100%' }}>
{this.state.showPanel &&
(({ x }) => (
<Panel
style={{ transform: x.interpolate(x => `translate3d(${x},0,0)`) }}
data={this.state.data}
onDataChange={this.handleDataChange}
/>
))}
</Transition>
</div>
)
}
} But like i said, i wouldn't cause the Sidebar to re-instanciate, it isn't good for performance. Better use Spring and maybe switch it off (display: none) when it's out, but you should let critical components always linger around in react and the dom in general because the cost of causing a full nested tree re-do is expensive. |
@will-stone would you open a new issue with an isolated csb example? until now i can't see a problem in transition, it behaves as it should. i'd need an example to look into it. |
@drcmda thank you very much for taking the time to take a closer look. I have to take a closer look at your proposed solution but two things from the top of my head:
|
@Prinzhorn I'd put the sidebar into a normal spring, a button controls its visible state, similar to this demo: https://codesandbox.io/embed/zl35mrkqmm As for the rest, could you make a demo without any animation? children notifying their parents is usually not a good idea, unless you do it with context, but setStating into the parent in the middle of an unmount is probably going to be problematic. The infinite loop i debugged in your example stemmed from that. If you can, codesandbox would make this quick and painless on my side. |
like I said there is not a single sidebar. I'll try to put a demo up.
That would be my exact demo but just remove the
It was my understanding that this in the primary way to pass data with plain React (pass a function down using props which the children can use to pass data back up). The new Context API merely makes this more comfortable by skipping a few intermediate parents and making it less verbose to pass things up and down, it's still the same thing. I am in fact using context and don't notify the parent directly, the parent just happens to consume it. If you abstract this away using something like Redux it gets worse because you can pass data to a parent without knowing it (all you do is update the store and don't care who's watching it).
React itself guarantees that Thanks again for taking the time. Maybe I'm using the |
I've created a more realistic test case. It is a bit more complex but we have already established that the issue can be reproduced with the basic test case in my first post. This demo shows a use-case that cannot trivially be replaced with a So let's say in my App I have an The demo shows three things that can be edited which are organized hiercharchically to demonstrate why I use Uncomment line 65 in App.js for the endless loop. https://codesandbox.io/embed/r1o71p7x1m?module=%2Fsrc%2FApp.js |
@Prinzhorn I tracked it for a while, i am still stuck - relying on lifecylces/componentWillUnmount to communicate back to the parent which then calls setState is extremely unconventional. If you find another solution for that your problem is fixed. There's something in Transition that instanciates a leaving parent with a new key. "metadata" gets renamed to "metadata_" for instance. This explains the mounts/unmounts. This has to be done, the old key can't be used lest the same object re-appears and stops the old one from transitioning out. This is probably what's causing the loop, but on react-springs side, what it does is fine. It's the componentWillUnmount/parent.setState thing that shouldn't be done. |
@Prinzhorn perhaps i should give transition a do-over, there must be another way. Currently leaving transitions are, as i explained, re-keyed in order to prevent incoming transitions with the same key to obstruct the leaving animation. But perhaps it would be possible to re-key transitions straight up when they're added by looking if their key is already present. Transition unfortunately is the most complex primitive with the most side effects - i run some tests and see what i can do. |
@drcmda thanks again. I do have very specific thoughts about that but I don't have time today to look into it. I'm sure we can find a clean way to handle that. |
I think it all boils down to this sentence. The fundamental question is if this assumption/behavior is correct. Is there really an "old" and a "new" one?
All that All that being said, I haven't seen the react-spring code and maybe I'm missing something. |
@drcmda Apologies for taking so long to set this up but here's my sandbox of this issue: https://codesandbox.io/s/1vjk1jn4ll In the Logo component, you can see that the animation stops at the end (using animation-fill-mode = forwards). However there is a flash of its initial state as it fades out because it has been remounted. |
And here's the working comparison when using v4 of React-Spring: https://codesandbox.io/s/pw318qn21x |
@will-stone yes, i see, that's unfortunate. The re-keying essentially re-mounts your Logo component which prompts the browser to run the css animation again. v4 didn't re-key, but then again, it couldn't guarantee an uninterrupted fade-out if an element with the same key entered again. I would like to support both, guaranteed exit without remount. But this isn't trivial - i'll need some time to get this right. |
Thanks @drcmda I ended-up making the animation return to the start as a sort of hack. In the end, I preferred that animation, anyway, as it worked well with the website: https://will-stone.github.io/SpotSpot/ |
Looks nice, but i can see now how Transition should support it - the css reset is a pretty serious strike against the current model. |
could you guys try 5.4.2-beta1? I implemented the key-deriving part from scratch and now it doesn't re-key exit transitions. edit: @Prinzhorn yours seems to work as expected, too :-) |
I guess i just release and close this |
@drcmda Looks good to me. Thanks! |
@drcmda thanks, the endless loop is gone! Now the only thing that is left is the fact that However, looking at my codesandbox it looks like now other things are broken: components are no longer unmounted after the transition has ended Steps to reproduce
I think before continuing we need tests for |
@Prinzhorn this could be a react error, update to react 16.4. They had a bug before where getDerivedStateFromProps isn't consistently called. I've seen the effect of it before, elements stay put after the exit phase. After the update this shouldn't happen. Tests would be great as Transition is the most complex primitive that can cause the most side effects. Edit: tried it, yep, they're gone now. |
maybe I'm doing something wrong, I'm not used to CodeSandbox. I've changed the dependency to 16.4.1 and to make sure it worked I render Edit: here's a video http://www.prinzhorn.me/vokoscreen-2018-07-23_17-38-28.mp4 |
react-dom is still on 16.3.1 in that box, you need to update both react and react-dom, then it should work. |
@drcmda nice, seems to work! |
react-spring is directly inspired by the API of react-motion but has much better performance Upgrading to latest version of react due to a bug in 16.3.x see: pmndrs/react-spring#136 (comment) pmndrs/react-spring#215 (comment) https://reactjs.org/blog/2018/05/23/react-v-16-4.html#bugfix-for-getderivedstatefromprops Signed-off-by: Neal Granger <neal@nealg.com>
react-spring is directly inspired by the API of react-motion but has much better performance Upgrading to latest version of react due to a bug in 16.3.x see: pmndrs/react-spring#136 (comment) pmndrs/react-spring#215 (comment) https://reactjs.org/blog/2018/05/23/react-v-16-4.html#bugfix-for-getderivedstatefromprops Signed-off-by: Neal Granger <neal@nealg.com>
## What is this change? ### Replace react-motion with react-spring React-spring is directly inspired by the API of react-motion but has much better performance Upgrading to latest version of react due to a bug in 16.3.x see: - pmndrs/react-spring#136 - pmndrs/react-spring#215 - https://reactjs.org/blog/2018/05/23/react-v-16-4.html#bugfix-for-getderivedstatefromprops ### Create "relocation" system This PR introduces "relocation" as more flexible alternative to `React.Portal` supporting both declarative (`Relocation.Sink`) and imperative (`Relocation.Consumer`) interfaces. The intention is to eventually rely on the published `relocation` package once it is updated to match the pattern used here. Showing a toast notification: ```jsx const SomeComponent = props => ( <ToastProvider> {({ addToast }) => ( <button onClick={someAsyncAction.then( result => addToast(({ id, remove }) => ( <Toast variant="success" message={`Action succeeded with result: ${result}`} onClose={remove} maxAge={5000} // 5 seconds /> )), error => addToast(({ id, remove }) => ( <Toast variant="error" message={`Action failed with error: ${error}`} onClose={remove} maxAge={5000} // 5 seconds /> )), )} > Fire Async Action </button> )} </ToastProvider> ); ``` ### Add feedback toast for check execution Displays a toast indicating pending and success states for each requested check execution. ![check toast](https://user-images.githubusercontent.com/1074748/45127989-58227580-b130-11e8-9995-1aba1f1e071b.gif) ## Why is this change necessary? Closes #1932 Closes #2012 ## Does your change need a Changelog entry? No ## Do you need clarification on anything? No ## Were there any complications while making this change? No ## Have you reviewed and updated the documentation for this change? Is new documentation required? No
Here's the reproduction, it's a plain create-react-app https://github.com/Prinzhorn/react-spring-will-unmount-bug/blob/master/src/App.js
Quick start
Open your JavaScript console, run the example app and hit the button "Toggle Panel" twice. When the panel is closing you can see an endless loop of:
(it's not actually endless, React will kill it after 1000 runs).
If you remove line 33
this.props.onDataChange('bar');
the endless loop is gone, however the lifecycle hooks are still not correctDetails
I have a side-panel in my app with some settings (text inputs and such). I want to save the changes once the panel is closed, that's why I pass the updated data to a central store from inside
componentWillUnmount
. However, the panel(s) are controlled by a react-spring<Transition>
which does weird things. The updated data will cause the<Transition>
(or the containing component) to re-render but always with an empty array (since the panel is currently closing). But it is still causing the to-be-removed panel to be mounted/unmounted endlessly.The text was updated successfully, but these errors were encountered: