TransitionGroup with react and redux: replace old element with animation - javascript

Consider a use case: a block with a text inside (text is fetched from store). When text changes - block smoothly goes away and the other block appears.
Pseudo code for better illustration:
import TransitionGroup from 'react-addons-transition-group'
#connect((state) => ({text: state.text}))
class Container extends React.Component {
render() {
return (
<div>
<TransitionGroup>
<Block key={this.props.text}/> // change block when text changes
</TransitionGroup>
</div>
)
}
}
#TransitionWrapper() // pass componentWillEnter through wrapper
#connect((state) => ({text: state.text}), null, null, {withRef: true})
class Block extends React.Component {
componentWillEnter(callback) {
// fancy animations!!!
const el = ReactDOM.findDOMNode(this);
TweenMax.fromTo(el, 1, {
alpha: 0,
}, {
alpha: 1,
onComplete: callback
});
}
componentWillLeave (callback) {
const el = ReactDOM.findDOMNode(this);
TweenMax.to(el, 1, {
alpha: 0,
onComplete: callback
});
}
render() {
return (
<div>{this.props.text}</div>
)
}
}
What happens when state.text changes?
New Block appears, because key is changed; componentWillEnter starts the animation for it. Great.
Old block gets re-rendered and componentWillLeave starts the animation for it.
When first animation finishes re-render happens again.
The issue is the step no 2: old element should disappear with the old data, but due to re-render it changes his content to a new one from store, so user see this:
store.text = 'Foo'. User see one Block with text 'Foo' on the screen.
store.text = 'Bar'. User see two Blocks, both with text 'Bar' on the screen. One block is disappearing.
Animation finishes, user see one Block with text Foo on screen.
I believe using transitions is pretty common nowadays and this should be a common issue, but I was surprised I couldn't find anything related.
Best idea I can think is to "freeze" props on the element when it's about to leave (or passing previous store, so it re-renders with previous data), but it feels hacky to me.
What's the best way to solve this problem?

We met the same problem with redux store, since when data got removed, the props contains nothing, thus, the UI will show no data when unmounting animation is happening.
I think it is hacky to use the old store or state (break the convention of React life Cycle), you can use loading placeholder if no data is available, like
if (!this.props.text){
return <EmptyPlaceholder />
}
also the animation duration is small like 300 milliseconds, the user experience won't be bad.
Alternatively, you need to define a class instance variable like:
componentWillMount(){
if(this.props.text){
this.text = this.prop.text;
}
}
Then render text like
<Block key={this.props.text || this.text}/>
Then the old text will always be there when unmounting animation happened. I tested on my project, it worked very well. Hopefully it will help u, if not please feel free to msg me.

Related

MobX observable boolean does not re-render component, but observable number does

I'm getting started with a new create-react-app application using TypeScript, hooks, and mobx-react-lite. Despite having used MobX extensively in a React Native app in the past, I've run into an issue that doesn't make any sense to me.
I have a store with two observables: one number and one boolean. There is an initialize() method that runs some library code, and in the success callback, it sets the number and the boolean to different values (see Line A and Line B below).
The issue: my component ONLY re-renders itself when Line A is present. In that case, after the initialization is complete, the 'ready' text appears, and the button appears. If I delete Line B, the 'ready' text still appears. But if I delete Line A (and keep Line B), the button never renders. I've checked things over a hundred times, everything is imported correctly, I have decorator support turned on. I can't imagine why observing a number can trigger a re-render but observing a boolean cannot. I'm afraid I'm missing something horribly obvious here. Any ideas?
The relevant, simplified code is as follows:
// store/app.store.ts
export class AppStore {
#observable ready = false
#observable x = 5
initialize() {
// Takes a callback
ThirdPartyService.init(() => {
this.ready = true
this.x = 10
})
}
}
// context/stores.ts
const appStore = new AppStore()
const storesContext = React.createContext({
appStore
})
export const useStores = () => React.useContext(storesContext)
// App.tsx
const App = observer(() => {
const { appStore } = useStores()
useEffect(() => {
appStore.initialize()
}, [appStore])
return (
<div>
{ appStore.x === 10 && 'ready' } // <-- Line A
{ appStore.ready && <button>Go</button> } // <-- Line B
</div>
)
}
EDIT: A bit more information. I've added some logging statements to just before the return statement for the App component. I also refactored the button conditional to a const. This may provide more insight:
const button = appStore.ready ? <button>Go</button> : null
console.log('render', appStore.ready)
console.log('button', button)
return (
<div className="App">
<header className="App-header">{button}</header>
</div>
)
When appStore.ready is updated, the component does re-render, but the DOM isn't updated. The console shows 'render' true and shows a representation of the button, as it should, but inspecting the document itself shows no button there. Somehow, though, changing the condition from appStore.ready to appStore.x === 10 does update the DOM.
Turns out I didn't quite give complete information in my question. While I was creating a minimal reproduction, I decided to try dropping the top-level <React.StrictMode> component from index.tsx. Suddenly, everything worked. As it happens, mobx-react-lite#1.5.2, the most up-to-date stable release at the time of my project's creation, does not play nice with Strict Mode. Until it's added to a stable release, the two options are:
Remove strict mode from the React component tree
Use mobx-react-lite#next

React: Can I return the result of previous render?

I have the following situation:
In order to show a modal dialog, I need to first gather some information and store it in redux.
The pattern library I need to use uses a wrapper portal component for animation support. This is always in the react tree.
The modal, however is only in the react tree when I have isOpen = true
The opening animation works fine, but since I render the modal conditionally, the closing animation breaks, since the modal is gone after close.
I am not sure if my idea to solve this is an acceptable method:
const RenderComponent = (props) => {
const content = useRef(null);
if (props.isOpen) {
content.current = props.render();
}
return content.current;
};
This returns null if the modal has not been opened yet (opened means all data is available). This is okay, since the animation only starts when it gets opened.
Once it has been opened, it will render the content into a reference and return that. Storing it in a reference allows me to also return it when isOpen turns false, since it still needs to be rendered for the animation.
So my question is: Is it okay to return the result of a previous render or will this cause unpredictable behavior?
Edit: A bit more detail:
The (company-wide) pattern library has two components: ModalPortal and Modal
I use the above components like this (simplified code):
const node = (
<ModalPortal isOpen={isOpen} onClose={closeModal}>
<RenderComponent
isOpen={isOpen}
render={() => (
<Modal title={data.title} onClose={closeModal}>
Content
</Modal>
)}
/>
</ModalPortal>
);
I think it's best to go from a functional component to a class based one so that you can have more control over the states and what you want happening in each.
An option could be to keep in mind three states for the modal instead of just two: "open", "closing", "closed". Since I don't know about the rest of your code, I am going to write some very generic code here to help illustrate my suggestion. It would be best to share some more in your question, since it's easier to help out with more info.
export const ModalStates = {
"OPEN" : 1,
"CLOSING" : 2,
"CLOSED" : 3
};
export default class ModalComponent {
constructor(props){
super(props);
this.state = {
'modal_state' : ModalStates.CLOSED
};
}
render () {
if(this.state.modal_state != ModalStates.CLOSED)
return <div class="moda">Your modal HTML here</div>;
}
}
This would of course need to be expanded. Here you would need the logic that changes the modal to open and to the other states. The idea is that when you want the modal closed the steps could be:
Change modal state to closing
Carry out animation
When animation is finished, modal state's should change to closed so that it doesn't render any more.
Hope this helps guide you.

React: Updating state in a conditional setTimeout inside componentDidMount does not reload the component

EDIT: I figured out the problem shortly after posting this. See my answer below.
I'm really stumped as to why my component won't reload after updating the state. This is the code for the component in full (minus the imports/export), with annotations:
class About extends Component {
state = {
introIsRunning: true,
animationStep: 1,
indent: 0,
steps: {
1: ["placeholder"],
2: ["placeholder"],
3: ["placeholder"],
4: ["placeholder"],
5: ["placeholder"],
6: ["placeholder"],
},
content: [],
};
My goal is to show a new line of content every five seconds until I reach the end of the steps, at which point I stop.
Before I make any state changes, I check to make sure if introIsRunning is true. Then I check to see if we've reached the end of the steps; if so, I set introIsRunning to false which will prevent anymore state changes.
(Looking at this again, I see it's pretty redundant to have the boolean and the animationStep check when I can just check for the animationStep and leave it at that. I can't see why that would be the problem though, so moving on...)
componentDidMount = () => {
if (this.state.introIsRunning) {
if (this.state.animationStep > 6 {
this.setState({ introIsRunning: false });
}
If this is the first step, I don't want any delay before displaying the first line. But if at least the first line has already been returned, I want to wait 5 seconds before adding the next. However, because zero delay would trigger an immediate state change which could potentially cause issues, I've added a 5 second delay to the first line rendering just to make sure that wasn't causing my issue.
let animationDelay = 5000;
if (this.state.animationStep > 1) {
animationDelay = 10000;
}
console.log('state before timeout:', this.state);
The 'state before timeout' console log shows animationStep: 1, and content is an empty array. As expected.
After the prescribed delay, I update the state with the correct step's content and increment the step so that on the next iteration, it'll load step 2 and advance to 3, etc.
setupContent just adds a new object with the appropriate content to the state's content array, which gets used in the render method to build out the actual component.
setTimeout(() => {
this.setState({
content: this.setupContent(this.state.steps[this.state.animationStep]),
animationStep: this.state.animationStep + 1,
});
}, animationDelay);
// debugging
setTimeout(() => {
console.log('why am I still here? state is:', this.state);
}, 15000);
}
}
setupContent = content => {
let updatedContent = [...this.state.content];
const id = "line_" + this.state.animationStep;
updatedContent.push({
id: id,
number: this.state.animationStep,
indent: this.state.indent,
content: content,
});
}
The 'why am I still here?' console log actually shows the correctly updated state. animationStep is now 2, and the content array contains one object built to setupContent's specifications.
render() {
return (
<div className="container">
{this.state.content.map(line => (
<Line
key={line.id}
id={line.id}
number={line.number}
indent={line.indent}
>
{line.content}
</Line>
))}
</div>
);
}
}
This correctly turns my content object into a functional Component which is rendering how I want it to. However, it just never advances to the next step. After correctly, verifiably updating the state, it just sits here. I've searched around and done as much debugging as I can think to do but I really have no earthly idea why the state would update without reloading the component.
Wow, I feel really dumb for figuring this out minutes after posting the question, but the problem — of course — was that I was making all those checks in componentDidMount, which only happens once. I moved all the code out of componentDidMount and into its own function, which I now call in componentDidMount and componentDidUpdate. Now it's working as expected. I should really get a rubber duck...

Best practice for emitting an event to another component in the same container?

So I have a button that lives in a container and uses a callback on it's onClick event to update state in the container, fairly basic stuff. But now I want that container to let a different child know that the button was clicked so it can trigger the appropriate response. (and it should only let the child know once so the response isn't being triggered a million times)
The way I solved this looks and feels and quacks like a code smell, so I thought I'd ask you guys if there is a better way to do it. Here is what I did:
class myContainer extend Component {
constructor(){
super()
state= { triggered: false }
}
componentWillUpdate(nextProps, nextState){
this.hasTriggered = this.state.triggered !== nextState.triggered
}
triggerResponse = () => this.setState({...this.state, !this.state.triggered})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
Now this seems to work perfectly fine, and maybe this is what I should do, but it just feels like there has to be a neater way of sending a simple message of "I have been clicked" to a component in the same container.
One major red flag for me is that "triggered" is a boolean, but it doesn't matter if it is true or false, so if triggered is false, it means nothing, all that matters if it was the other boolean last round. This seems like a violation of good practices to me.
*Summary: What I'm looking for is a snappy way to give state a value for just one update cycle and then go back to null or false without having to update it again. Or a different way to get the same result.
I came up with 2 different yet unsatisfying answers:
class myContainer extend Component {
constructor(){
super()
state= { hasTriggered: false }
}
shouldComponentUpdate(nextProps, nextState){
return (!nextState.hasTriggered && this.state.hasTriggered)
}
componentDidUpdate(nextProps, nextState){
if(nextState.hasTriggered)this.setState({hasTriggered: false})
}
triggerResponse = () => this.setState({hasTriggered: true})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.state.hasTriggered}/>
)
}
}
This is unsatisfying because it is a lot of code for a very simple button click. I am setting state, sending it down, resetting state and then ignoring the next render call all to accommodate a lousy button click. The good news is that I'm no longer misusing a boolean, but this is definitely too much code.
class myContainer extend Component {
componentDidUpdate(){
this.hasTriggered = false
}
triggerResponse = () => {
this.hasTriggered = true
this.forceUpdate()
}
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
I find this method unsatisfying because I no longer have the shared state in state. Before I also did this by comparing new and old state and making a variable carry the result over to my component, but at least then I could look at my state and see that there is a state variable that has to do with this button. Now there is local state in my component that is in no way linked to the actual state, making it harder to keep track of.
After thinking about this all day I've come to the conclusion that #ShubhamKhatri was on the right track in his comment, I thought I was pulling up state to my container by using a callback and passing state down, but clearly there is too much logic being executed in my component if it's handling a click event. So my new rule is that in this kind of scenario you should just pull up whatever state you need to execute the onClick inside the container. If your dumb components are executing anything other than a callback it is a mistake.
The reason I was tempted to do the onClick logic in my presentational component is because I was using a third party library(d3) to handle the graphics and so I didn't consider that the state I wasn't pulling up was the d3 state, if I move that up, change it when the button is clicked and then pass it down to my component it works beautifully.
Now this means I need to import d3 in two places and I did have to write a bit more code than before, but I think the separation of concerns and the overall cleanliness of my code is well worth it. Also it made my child component a lot easier to maintain, so that's nice.

React/Redux animations based on actions

Having more or less completed my first React+Redux application, I have come to the point where I would like to apply animations to various parts of the application. Looking at existing solutions, I find that there is nothing even close to what I would like. The whole ReactCSSTransitionGroup seems to be both an intrusive and naive way to handle animations. Animation concerns bleed out of the component you want to animate and you have no way to know anything about what happens in the application. From my initial analysis I have come up with the following requirements for what I would consider a good animation API:
The parent component should be ignorant of how the child component fades in/out (maybe with the exception of staggered animations).
The animations should integrate with React such that components are allowed to fade out (or otherwise complete their animations) before they are removed.
It should be possible to apply an animation to a component without modifying the component. It is ok that the component is styled such as to be compatible with the animation, but there should be no props, state, contexts or components related to the animation, nor should the animation dictate how the component is created.
It should be possible to perform an animation based on an action and the application state - in other words, when I have the full semantic context of what happened. For example, I might fade in a component when a thing has been created, but not when the page loads with the item in it. Alternatively, I might select the proper fade out animation, based on the user's settings.
It should be possible to either enqueue or combine an animation for a component.
It should be possible to enqueue an animation with a parent component. For example, if a component has two sub components and opening one would first trigger the other to close before opening itself.
The specifics of enqueuing animations can be handled by an existing animation library, but it should be possible to tie it in with the react and redux system.
One approach I have tried out is to create a decorator function like this (it is TypeScript, but I don't think that matters too much with regards to the problem):
export function slideDown<T>(Component: T) {
return class FadesUp extends React.Component<any, any> {
private options = { duration: 0.3 };
public componentWillEnter (callback) {
const el = findDOMNode(this).childNodes[0] as Element;
if (!el.classList.contains("animated-element")) {
el.classList.add("animated-element");
}
TweenLite.set(el, { y: -el.clientHeight });
TweenLite.to(el, this.options.duration, {y: 0, ease: Cubic.easeIn, onComplete: callback });
}
public componentWillLeave (callback) {
const el = findDOMNode(this).childNodes[0] as Element;
if (!el.classList.contains("animated-element")) {
el.classList.add("animated-element");
}
TweenLite.to(el, this.options.duration, {y: -el.clientHeight, ease: Cubic.easeIn, onComplete: callback});
}
public render () {
const Comp = Component as any;
return <div style={{ overflow: "hidden", padding: 5, paddingTop: 0}}><Comp ref="child" {...this.props} /></div>;
}
} as any;
}
...which can be applied like this...
#popIn
export class Term extends React.PureComponent<ITermStateProps & ITermDispatchProps, void> {
public render(): JSX.Element {
const { term, isSelected, onSelectTerm } = this.props;
return <ListItem rightIcon={<PendingReviewIndicator termId={term.id} />} style={isSelected ? { backgroundColor: "#ddd" } : {}} onClick={onSelectTerm}>{term.canonicalName}</ListItem>;
}
}
Unfortunately it requires the component to be defined as a class, but it does make it possible to declaratively add an animation to a component without modifying it. I like this approach but hate that I have to wrap the component in a transition group - nor does it address any of the other requirements.
I don't know enough about the internal and extension points of React and Redux to have a good idea how to approach this. I figured thunk actions would be a good place to manage the animation flows but I don't want to send the action components into the actions. Rather, I would like to be able to retrieve the source component for an action or something like that. Another angle could be a specialized reducer which passes in both the action and the source component, allowing you to match them somehow and schedule animations.
So I guess what I'm after is one or more of the following:
Ways to hook into React and/or Redux, preferably without destroying performance or violating the basic assumptions of the libraries.
Whether there are any existing libraries that solves some or all of these issues and which would be easy to integrate into the application.
Techniques or approaches to achieve all or most of these goals by either working with the normal animation tools or integrating well into the normal building blocks.
I hope I understood everything right… Dealing with React + Redux means, that in best case your components are pure functional. So a component that should be animated should (IMHO) at least take one parameter: p, which represents the state of the animation. p should be in the interval [0,1] and zero stands for the start, 1 for the end and everything in between for the current progress.
const Accordion = ({p}) => {
return (
…list of items, each getting p
);
}
So the question is, how to dispatch actions over time (what is an asynchronous thing), after the animation started, until the animation is over, after a certain event triggered that process.
Middleware comes in handy here, since it can »process« dispatched actions, transform them into another, or into multiple
//middleware/animatror.js
const animator = store => next => action => {
if (action.type === 'animator/start') {
//retrieve animation settings
const { duration, start, … } = action.payload;
animationEngine.add({
dispatch,
action: {
progress: () => { … },
start: () => { … },
end: () => { … }
}
})
} else {
return next(action);
}
}
export default animator;
Whereby the animationEngine is an Instance of AnimatoreEngine, an Object that listens to the window.requestAnimationFrame event and dispatches appropriate Actions. The creation of the middleware can be used the instantiate the animationEngine.
const createAnimationMiddleware = () => {
const animatoreEngine = new AnimatorEngine;
return const animator = store => next => action => { … }
}
export default createAnimationMiddleware;
//store.js
const animatorMiddleware = createAnimationMiddleware();
…
const store = createStore(
…,
applyMiddleware(animatorMiddleware, …)
)
The basic Idea is, to »swallow« actions of type animator/start, or something else, and transform them into a bunch of »subactions«, which are configured in the action.payload.
Within the middleware, you can access dispatch and the action, so you can dispatch other actions from there, and those can be called with a progress parameter as well.
The code showed here is far from complete, but tried to figure out the idea. I have build »remote« middleware, which handles all my request like that. If the actions type is get:/some/path, it basically triggers a start action, which is defined in the payload and so on. In the action it looks like so:
const modulesOnSuccessAction => data {
return {
type: 'modules/listing-success',
payload: { data }
}
}
const modulesgetListing = id => dispatch({
type: `get:/listing/${id}`,
payload: {
actions: {
start: () => {},
…
success: data => modulesOnSuccessAction(data)
}
}
});
export { getListing }
So I hope I could transport the Idea, even if the code is not ready for Copy/Paste.

Categories