I have a react component:
React.createComponent({
render: function () {
return (<div className="some-component"></div>);
}
});
Some seconds after it renders, I want it to add a class from within the component. The class is for the purposes of revealing the component with an animation. I don't regard it as a real change of state, as it has no affect on the application other than to give the component an animated introduction, so I'm loathed to initiate it from outside of the component via a change of store/state.
How would I do this in simple terms? Something like:
{setTimeout(function () {
this.addClass('show'); //pseudo code
}, 1000)}
Obviously I could use jQuery, but that feels anti-React, and prone to side-effects.
I don't regard it as a real change of state, as it has no affect on the application other than to give the component an animated introduction
A change of state in the component seems the natural and proper solution for this scenario. A change in a component state triggers a re-render, which is exactly what you need here. Consider that we're talking about the state of your component, not of your application here.
In React, you don't deal with the DOM directly (e.g. by using jQuery), instead your component state should "drive" what's rendered, so you let React "react" to changes in state/props and update the DOM for you to reflect the current state:
React.createComponent({
componentDidMount () {
this.timeoutId = setTimeout(function () {
this.setState({show: true});
}.bind(this), 1000);
}
componentWillUnmount () {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
render: function () {
return (<div className={this.state.show ? 'show' : null}></div>);
}
});
When using setTimeout in React you need to be careful and make sure to cancel the timeout when the component gets unmounted, otherwise your timeout callback function will run anyway if the timeout is still pending and your component gets removed.
If you need to perform an initial mount animation or more complicated animations, consider using ReactCssTransitionGroup, which handles timeouts and other things for you out of the box: https://facebook.github.io/react/docs/animation.html
Related
I tried to make...
There is no input on the screen at the first rendering. When I click the button, input appears. And I want to set focuse on the input at the same time.
Let me explain what i made.
At first, the input is not visible on the screen.
Because the display property of the Box(the div tag), which is the parent component of the input, is none.
But when i click the button, the display property of the Box changes to block.
And here is what i want to do.
i'm going to set focus on the input on the screen.
In the function called when the button is clicked, I wrote a code that changes the css code and sets the focus on the input.
But it didn't work.
Please take a look at the following code.
const [inputDisplay, setInputDisplay] = useState("none");
const refInput = useRef(null);
const HandleShowInput = () => {
setInputDisplay("block");
refInput.current.focus();
};
return (
<>
<Box theme={inputDisplay}>
<Input ref={refInput}/>
<Box/>
<Button onClick={HandleShowInput}/>
</>
)
Below is the code that is dynamically changing the css of the Box component.
import styled, { css } from "styled-components";
const Box = ({ children, ...props }) => {
return <StBox {...props}>{children}</StBox>;
};
const StBox = styled.div`
${({ theme }) => {
switch (theme) {
case "block":
return css`
display: ${theme} !important;
`;
default:
break;
}
}}
`;
export default Box;
But this below code is worked. I separated the code by putting it in useEffect.
const [inputDisplay, setInputDisplay] = useState("none");
const refInput = useRef(null);
const HandleShowInput = () => {
setInputDisplay("block");
};
useEffect(() => {
refInput.current.focus();
}, [inputDisplay]);
return (
<>
<Box theme={inputDisplay}>
<Input ref={refInput}/>
<Box/>
<Button onClick={HandleShowInput}/>
</>
)
I want to know why the upper case not works and the lower case works. I don't know if I have lack react knowledge or css knowledge. I would be very grateful if you could help a beginner in react. Also, please understand if there are any unnatural sentences because i'm not good at English. thank you.
When you are trying to focus on the input element by HandleShowInput this function.Here two things are happening your changing the state and focus of input.It will focus the input but time will be so less that we can't see on the ui.And also due to the state change render will happen and again ref will get the input element. Thus you are not able to see this focussed input
But in case of useEffect this will happen after the render. After this no rendering. So we can see the focussed input
The way of thinking about React is a little different from Javascript.
You may expect the below two run in the same way.
setInputDisplay("block");
refInput.current.focus();
and
document.querySelector('#canFocus').style.display='block'
document.querySelector('#canFocus').focus();
NO~ It's not.
JS block the Dom and then focus it, it works well.
But React works like the code below.
setTimeout(()=>{
// next react render cycle callback
document.querySelector('#canNotFocus').style.display='block'
}, 1000)
document.querySelector('#canNotFocus').focus();
While focus method is called, the dom is display as none;
You set state in react, ReactDom will make it as a display block in the next life cycle of function component.
demo here : https://codesandbox.io/s/confident-wilson-q01ktj?file=/index.html
useEffect(() => {
refInput.current.focus();
}, [inputDisplay]);
is a watching function. While inputDisplay changed, the function inside will be called.
you set state to block
react re-render the component as a newer state
render function called, and dom is block
Effect watching function is called and the focus() called.
I'm using Framer Motion's useViewportScroll hook to track the user's scroll on my page, but it isn't updating. I came across this article, which explains how Framer Motion uses document.body.clientHeight and window.innerHeight to calculate the viewport scroll.
The article explains how some CSS can break this, specifically if document.body.clientHeight - window.innerHeight <= 0.
I can't seem to figure out how this wouldn't be true. Even with no CSS at all in my react app, that expression evaluates to true. You can see it in this example on CodeSandbox.
Thanks!
I think the reason you're not seeing updates is not because of any css.
Framer seems to calculate it's state standalone from react. So when Framer updates its values internally. react won't necessarily trigger a re-render with the updated values. (probably this is done for performance).
You can hook into the state changes by subscribing to an onChange handler that Framer is exposing and set the state you're interested in.
For example, the onChange on scrollYProgress will call setState, triggering react to re-render.
import React from "react";
import { useViewportScroll } from "framer-motion"
const FramerPostionHook = () => {
const { scrollYProgress } = useViewportScroll();
const [hookedYPostion, setHookedYPosition] = React.useState(0);
React.useEffect(()=>{
// hook into the onChange, store the current value as state.
scrollYProgress.onChange(v=> setHookedYPosition(v));
},[scrollYProgress]); //make sure to re-subscriobe when scrollYProgress changes
return (<>
scrollYProgress.current: {scrollYProgress.current}<br/>
scrollYProgress.hookedYPostion: {hookedYPostion}<br/>
</>)
}
Sandbox example
I am writing a custom component that I would like to define other component dependencies.
The dependencies are different animations types.
Let's say they have the names "animation__x" and "animation__y"
x and y can be any name, so I am looking for something like animation__*
or /animation__\s*/
The only way I have made this work at the moment is either ensuring my component is placed after the animation components on the HTML or alternatively to force update components using this.el.updateComponents()
Neither of these solutions feels right to me.
AFRAME.registerComponent('cool-component', {
dependencies: ['animation'],
update: functions(data){
//detect available animations and do some stuff with them
let animations = Object.keys(components).filter((key) => {
return /(^animation__\w*)/.test(key);
});
//animations results in an empty array
}
});
html that is not working
<a-scene cool-component animation__x="" animation__y="" animation__z=""></a-scene>
html that is working (but its not good as I cant ensure my component is always last in the list
<a-scene animation__x="" animation__y="" animation__z="" cool-component></a-scene>
js that works, but doesnt feel write as I am using the entities internal functions
AFRAME.registerComponent('cool-component', {
dependencies: ['animation'],
update: functions(data){
this.el.updateComponents(); //<-- I DONT LIKE THIS BUT IT WORKS
//detect available animations and do some stuff with them
//now all animations are available as this.el.components
let animations = Object.keys(components).filter((key) => {
return /(^animation__\w*)/.test(key);
});
}
});
Three options:
Depend on the specific component names: dependencies: ['animation__xxx']
Make cool-component set those animations:
AFRAME.registerComponent('cool-component', {
init: functions(data){
this.el.setAttribute('animation__xxx', {...});
}
});
You can also defer cool-component logic until the entity has loaded and all the components have initialized:
init: function () {
this.el.addEvenListener(‘loaded’, this.doStuffAferComponentsLoad.bind(this));
}
More details in what cool-component is trying to accomplish will help to get a more precise answer.
Here's a fiddle of what I want to do: https://jsfiddle.net/s7s07chm/7/
But I want to do this with react instead of jquery. Basically, I put the className of the element in the state, and on componentDidMount I update the className to initiate the transition.
But this isn't working. The component is just rendering with the transitioned state. In other words, instead of sliding down, it appears at the bottom from the beginning
Am I doing this wrong?
If so, is there another way to accomplish this?
here's the actual code
getInitialState: function() {
return {
childClass: 'child'
};
},
componentDidMount: function() {
this.setState({
childClass: 'child low'
});
},
the reason that won't work is because your DOM won't be updated until the component is mounted. So the class you're assigning with getInitialState will never appear in the DOM, but the one you set with componentDidMount will. As Ray mentioned, you should take a look at ReactCSSTransitionGroup.
componentDidMount is called after React hands them off to the browser, but the browser might not have finished drawing.
You can however use requestAnimationFrame to ensure setState is called after the browser has finished drawing the component.
componentDidMount() {
requestAnimationFrame(() => this.setState({
childClass: 'child low'
}))
}
Has somebody found a good way to animate state transitions?
The router immediately removes the view from the DOM. The problem with that is that I can't defer that until the end of the animation. Note: I'm using v1.0.0-pre.4.
Billy's Billing just released an Ember module that supports animated transitions.
I'll expand on Lesyk's answer. If you need to apply it to multiple views in a DRY way, you can create a customization class like this:
App.CrossfadeView = {
didInsertElement: function(){
//called on creation
this.$().hide().fadeIn(400);
},
willDestroyElement: function(){
//called on destruction
this.$().slideDown(250);
}
};
And then in your code you apply it on your various view classes. As Ember depends on jQuery you can use pretty much any jQuery animation.
App.IndexView = Ember.View.extend(App.CrossfadeView);
App.PostView = Ember.View.extend(App.CrossfadeView);
I know this is pretty old, but the best solution for this context-specific animation today is probably ember liquid fire.
It allows you to do things like this in a transition file:
export default function(){
this.transition(
this.fromRoute('people.index'),
this.toRoute('people.detail'),
this.use('toLeft'),
this.reverse('toRight')
);
};
Ran into this same requirement on my app. Tried Ember Animated Outlet, but didn't give the granularity I needed (element specific animations).
The solution that worked for me was as follows --
Change linkTo to be an action
{{#linkTo "todos"}}<button>Todos</button>{{/linkTo}}
Becomes...
<a href="#/todos" {{action "goToTodos"}}><button>Todos</button></a>
Create Method for goToTodos in current controller
App.IndexController = Ember.Controller.extend({
goToTodos: function(){
// Get Current 'this' (for lack of a better solution, as it's late)
var holdThis = this;
// Do Element Specific Animation Here
$('#something').hide(500, function(){
// Transition to New Template
holdThis.transitionToRoute('todos');
});
}
});
Finally -- To animate in elements on the Todos Template, use didInsertElement on the view
App.TodosView = Ember.View.extend({
didInsertElement: function(){
// Hide Everything
this.$().hide();
// Do Element Specific Animations Here
$('#something_else').fadeIn(500);
}
});
So far, this is the most elegant solution I've found for element specific animations on transition. If there is anything better, would love to hear!
I've found another drop-in solution that implements animations in Views: ember-animate
Example:
App.ExampleView = Ember.View.extend({
willAnimateIn : function () {
this.$().css("opacity", 0);
},
animateIn : function (done) {
this.$().fadeTo(500, 1, done);
},
animateOut : function (done) {
this.$().fadeTo(500, 0, done);
}
}
Demo: author's personal website
App.SomeView = Ember.View.extend({
didInsertElement: function(){
//called on creation
this.$().hide().fadeIn(400);
},
willDestroyElement: function(){
//called on destruction
this.$().slideDown(250)
}
});