Transition in React is not working like expected - javascript

From the website and some articles, i have learned that any changes made to the app will first be reflected in the Virtual DOM. and then to the Real DOM after diffing both trees. the lifecycle hook useEffect will fire once the Real DOM is populated.
Problem
import React from 'react';
import { useEffect, useRef } from 'react';
export function App(props) {
const ref = useRef();
useEffect(() => {
const ref2 = ref.current;
const div = document.createElement('div')
div.innerHTML = "ASDF"
div.style.position = "absolute";
div.style.top = 0;
div.style.transition = "all 5s"
ref2.appendChild(div);
// this should work theoretically and my div should get a transition
div.style.top = "100px"
}, [])
console.log("render")
return (
<div ref={ref} className='App'>
<h1>Hello React.</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
// Log to console
console.log('Hello console')
Running the above code gives a static div with top as 100px instead of a transition.
So, I tried the following code
import React from 'react';
import { useEffect, useRef } from 'react';
export function App(props) {
const ref = useRef();
useEffect(() => {
const ref2 = ref.current;
const div = document.createElement('div')
div.innerHTML = "ASDF"
div.style.position = "absolute";
div.style.top = 0;
div.style.transition = "all 5s"
ref2.appendChild(div);
// The following three lines is the only change
setTimeout(()=>{
div.style.top = "100px"
}, 0)
}, [])
console.log("render")
return (
<div ref={ref} className='App'>
<h1>Hello React.</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
// Log to console
console.log('Hello console')
and this gives me the result i wanted. where initially the div is positioned at the top and animates to 100px.
**Why does a 0 second setTimeout work? Does it have something to do with the event loop? **
I can't use useTransition, animations API, framer-motion or any other animation method. I need the transition to happen this way only. Thank you for your patience!

I've spent the night looking into this problem. Check out this code:
function tellMeWhatsPassed(thing){
console.log(thing)
}
function createThing(){
const div = {}
div.innerHTML = 'original'
tellMeWhatsPassed(div)
div.innerHTML = 'updated'
tellMeWhatsPassed(div)
}
createThing()
// {innerHTML: 'original'}
// {innerHTML: 'updated'}
Since JS executes sequentially we expect that since we are calling the tellMeWhatsPassed with two different arguments we get two different console logs.
Now watch what happens when we update the code and instead of creating an object we create a html div element.
function tellMeWhatsPassed(thing){
console.log(thing)
}
function createThing(){
const div = document.createElement('div')
div.innerHTML = 'original'
tellMeWhatsPassed(div)
div.innerHTML = 'updated'
tellMeWhatsPassed(div)
}
createThing()
// <div> updated </div>
// <div> updated </div>
We've got two things going on: the DOM API is being executed off the main stack and is reintroduced through the event loop AND there's a quirk with console logging that shows us only the latest when passing a reference compared to a primitive.
To prove this last point convert the console.log(thing) to console.log(thing.innerHTML) and you can see the latest updated innerHTML property. Alternatively you could pass the div.innerHTML to tellMeWhatsPassed
This isn't exactly an answer but I hope my findings at least help you!

You are right... It will delay the execution till the executing code must complete.
Putting some delay for witnessing UI changes is helpful.
This is because even though setTimeout was called with a delay of zero, it's placed on a queue and scheduled to run at the next opportunity; not immediately. Currently-executing code must complete before functions on the queue are executed, thus the resulting execution order may not be as expected.

Related

Why is the value on display not in sync with value logged to console?

Can anyone tell me why at button click, the value outputted to the console is always one unit smaller than displayed on the screen?
The values are not in sync as expected.
Example below in React
In Child:
import React, {useState } from "react";
export const ChildComp = ({getNumProps}) => {
const [num, setNum] = useState(0);
const onPlusClick = () => {
if (num< 12) {
setNum(num + 1);// num does not increase immediately after this line, except when focus reenters here on second method call
}
getNumProps(num);
}
return(
<div>
<button onClick={onPlusClick}>
Click to increment
</button>
{num}
</div>
);
}
In parent
import { ChildComp } from "./ChildComp"
export const ParentComp = () => {
const getNum= (num) => {
console.log(num);
}
return (<ChildComp getNumProps={getNum}/>)
}
The page initially shows 0
When I click once the number increments to 1, but console displays 0
When I click once the number increments to 2, but console displays 1
I should see in the console the same as the page display
Appreciate if you can leave a commen on how the question can be improved.
This is a child to parent communication example. Also, any objections about standards used, please let me know.
Thanks.
Update: I notice the values would be in sync if
instead of getNumProps(num);
I did getNumProps(num + 1); but that doesn't change the fact that previously on this line
setNum(num + 1);, as already pointed out in the comment, num does not increase immediately after this line, except when focus reenters here on second method call. Not sure why.
The prop function getNumProps is a side effect, and should be put into a hook, instead of inside of that onPlusClick function.
Instead, do this:
useEffect(() => {
getNumProp(num);
}, [num]);
Alternatively, to avoid the error: "React Hook useEffect has a missing dependency: 'getNumProps'. See this doc on using the useCallback hook
const callback = useCallback(() => {
getNumProp(num);
}, [num]);
function onPlusClick(...) {
...
callback();
}
The change to the state of num will cause a re-render of the child component, not the parent.

ReactJS 17.0.2 seems to call function again when putting value into DOM

There is a strange behavior when running ReactJS 17.0.2.
I have a function that generates a random number outside of a component. I assign the return value of this function to a constant inside the component and afterwards console.log it and display in the DOM. Strangely the two are different. This effect is not happening in ReactJS 18.0 and above but I still want to understand what is happening here.
const randomNum = () => {
return 0.5 - Math.random() * 100;
};
export default function App() {
const randN = randomNum();
console.log(randN);
return (
<div className="App">
<p>Random number is: {randN}</p>
</div>
);
}
I would expect the console.log and the DOM to show the exact same values, but that is not the case.
Here is a sandbox that shows this behavior.
<StrictMode> deliberately renders the component twice, and the version of react you're using also secretly overwrites console.log during the second render to silence the second log. So you're seeing the log from the first render, and the value from the second render.
To see the second log statement, which will match with what's on the dom, you can do this:
import "./styles.css";
const randomNum = () => {
return 0.5 - Math.random() * 100;
};
const log = console.log;
export default function App() {
const randN = randomNum();
log(randN);
return (
<div className="App">
<p>Random number is: {randN}</p>
</div>
);
}
Later versions of react no longer mess with console.log, since it was causing confusions like yours.

How to force new state in React (Hooks) for textarea when clicking on another element?

codesandbox.io sandbox
github.com repository
I am creating this small Wiki project.
My main component is Editor(), which has handleClick() and handleModeChange() functions defined.
handleClick() fires when a page in the left sidebar is clicked/changed.
handleModeChange() switches between read and write mode (the two icon buttons in the left sidebar).
When in read mode, the clicking on different pages in the left sidebar works properly and changes the main content on the right side.
However, in write mode, when the content is echoed inside <TextareaAutosize>, the content is not changed when clicking in the left menu.
In Content.js, I have:
<TextareaAutosize
name="textarea"
value={textareaValue}
minRows={3}
onChange={handleMarkdownChange}
/>
textareaValue is defined in Content.js as:
const [textareaValue, setTextareaValue] = useState(props.currentMarkdown);
const handleMarkdownChange = e => {
setTextareaValue(e.target.value);
props.handleMarkdownChange(e.target.value);
};
I am unsure what is the correct way to handle this change inside textarea. Should I somehow force Editor's child, Content, to re-render with handleClick(), or am I doing something completely wrong and would this issue be resolved if I just changed the definition of some variable?
I have been at this for a while now...
You just need to update textareaValue whenever props.currentMarkdown changes. This can be done using useEffect.
useEffect(() => {
setTextareaValue(props.currentMarkdown);
}, [props.currentMarkdown]);
Problem:
const [textareaValue, setTextareaValue] = useState(props.currentMarkdown);
useState will use props.currentMarkdown as the initial value but it doesn't update the current state when props.currentMarkdown changes.
Unrelated:
The debounce update of the parent state can be improved by using useRef
const timeout = useRef();
const handleMarkdownChange = (newValue) => {
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(function () {
console.log("fire");
const items = [];
for (let value of currentData.items) {
if (value["id"] === currentData.active) {
value.markdown = newValue;
value.unsaved = true;
}
items.push(value);
}
setCurrentData({ ...currentData, items: items });
}, 200);
};
using var timeout is bad because it declares timeout on every render, whereas useRef gives us a mutable reference that is persisted across renders

Click handler, in react, doesn't return latest change in state when it executes a 2nd time

This takes place in a functional component:
import {useEffect} from 'react';
let [clickedOnPiece, setClickedOnPiece] = useState(false);
let [testRender, setTestRender] = useState(false);
useEffect(() => {
testRenderFunction();
}, [])
function testRenderFunction() {
let el = <div onClick={onClickHandler}>Click me</div>;
setTestRender(el);
}
function onClickHandler() {
if (clickedOnPiece) {
console.log("already clicked")
return
}
console.log(clickedOnPiece); //returns false the 1st & 2nd time.
setClickedOnPiece("clicked");
}
return (
<>
{testRender}
</>
)
When I click on div for the first time, I wait until setClickedOnPiece("clicked") successfully updates clickedOnPiece to "clicked". (I check this with React Developer Tools).
When I click div the 2nd time, it doesn't log the new change in state. It still logs clickedOnPiece as false. Why is this?
Okey this problem is because useState is asyncronus. u can read more about this useState set method not reflecting change immediately.
I think the solution is add useEffect like this.
useEffect( () => {
console.log(clickOnPiece);
}
, [clickOnPiece])
If you want to toggle the state, you could do something like this:
let [clickedOnPiece, setClickedOnPiece] = useState(false);
const onClickHandler = () => {
// set the value of clickedOnPiece to the opposite of what it was
// i.e. if it was 'true', set it to 'false'.
setClickedOnPiece(!clickedOnPiece);
console.log(clickedOnPiece);
}
// call the onClickHandler on click
<div onClick={()=>onClickHandler()}>Click me</div>
Looks like you are toggling
let [clickedOnPiece, setClickedOnPiece] = useState(false);
const onClickHandler = () => {
console.log(clickedOnPiece);
setClickedOnPiece(!clickedOnPiece);
}
console.log(clickedOnPiece);
<div onClick={onClickHandler}>Click me</div>
After setting state, don't console immediately because state is an asynchronous.
onClickHandler references the old, previous variable, clickedOnPiece. I believe this is because onClickHandler is not defined in the return statement part of the functional component which would have allowed it a new onClickHandler body to be created each time. Instead, we have the old onClickHandler continually referencing the old clickedOnPiece.
This problem is known as 'stale closures' - a concept I found discussed well at the bottom of this article

Expand animation with requestAnimationFrame and React doesn't work sometimes

I'm trying to implement some single input form with easy "expand" animation to when going to/from edit mode.
Basically I created a ghost element containing value, next to this element is icon button working as edit/save. When you click on the edit button, input with value should appear instead of ghost element and width of input should expand/decrease to constant defined.
I have so far this piece of code, which mostly works fine, but for expand it sometimes doesn't animate and I don't know why.
toggleEditMode = () => {
const { editMode } = this.state
if (editMode) {
this.setState(
{
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
setTimeout(() => {
this.setState({
editMode: false
})
}, 150)
})
}
)
} else {
this.setState(
{
editMode: true,
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
this.setState({
inputWidth: INPUT_WIDTH
})
})
}
)
}
}
You can take a look on example here. Could someone explain what's wrong or help me to find solution? If I'll add another setTimeout(() => {...expand requestAnimationFrame here...}, 0) in code, it starts to work, but I don't like the code at all.
This answer explains what's going on in detail and how to fix it. However, I wouldn't actually suggest implementing it.
Custom animations are messy, and there are great libraries that handle the dirty work for you. They wrap the refs and requestAnimationFrame code and give you a declarative API instead. I have used react-spring in the past and it has worked very well for me, but Framer Motion looks good as well.
However, if you'd like to understand what's happening in your example, read on.
What's happening
requestAnimationFrame is a way to tell the browser to run some code every time a frame is rendered. One of the guarantees you get with requestAnimationFrame is that the browser will always wait for your code to complete before the browser renders the next frame, even if this means dropping some frames.
So why doesn't this seem to work like it should?
Updates triggered by setState are asynchronous. React doesn't guarantee a re-render when setState is called; setState is merely a request for a re-evaluation of the virtual DOM tree, which React performs asynchronously. This means that setState can and usually does complete without immediately changing the DOM, and that the actual DOM update may not occur until after the browser renders the next frame.
This also allows React to bundle multiple setState calls into one re-render, which it sometimes does, so the DOM may not update until the animation is complete.
If you want to guarantee a DOM change in requestAnimationFrame, you'll have to perform it yourself using a React ref:
const App = () => {
const divRef = useRef(null);
const callbackKeyRef = useRef(-1);
// State variable, can be updated using setTarget()
const [target, setTarget] = useState(100);
const valueRef = useRef(target);
// This code is run every time the component is rendered.
useEffect(() => {
cancelAnimationFrame(callbackKeyRef.current);
const update = () => {
// Higher is faster
const speed = 0.15;
// Exponential easing
valueRef.current
+= (target - valueRef.current) * speed;
// Update the div in the DOM
divRef.current.style.width = `${valueRef.current}px`;
// Update the callback key
callbackKeyRef.current = requestAnimationFrame(update);
};
// Start the animation loop
update();
});
return (
<div className="box">
<div
className="expand"
ref={divRef}
onClick={() => setTarget(target === 100 ? 260 : 100)}
>
{target === 100 ? "Click to expand" : "Click to collapse"}
</div>
</div>
);
};
Here's a working example.
This code uses hooks, but the same concept works with classes; just replace useEffect with componentDidUpdate, useState with component state, and useRef with React.createRef.
It seems to be a better direction to use CSSTransition from react-transition-group, in your component:
function Example() {
const [tr, setIn] = useState(false);
return (
<div>
<CSSTransition in={tr} classNames="x" timeout={500}>
<input
className="x"
onBlur={() => setIn(false)}
onFocus={() => setIn(true)}
/>
</CSSTransition>
</div>
);
}
and in your css module:
.x {
transition: all 500ms;
width: 100px;
}
.x-enter,
.x-enter-done {
width: 400px;
}
It lets you avoid using setTimeouts and requestAnimationFrame and would make the code cleaner.
Codesandbox: https://codesandbox.io/s/csstransition-component-forked-3o4x3?file=/index.js

Categories