What happens when React rerenders? - reactjs

I have the following code and you can view it on codeSandbox:
import "./styles.css";
import lottie from "lottie-web";
import { useEffect, useRef } from "react";
import logo from "pic";
export default function App() {
const el = useRef(null);
useEffect(() => {
if (el != null) {
lottie.loadAnimation({
container: el,
renderer: "svg",
loop: true,
autoplay: true,
animationData: logo
});
}
}, []);
return (
<div className="App">
<div ref={el}></div>
</div>
);
}
Lottie is just some library to render animation. The problem I have with it is that while I am in the developing phase, if I make some modification to the second div tag, then React rerenders without destroying the previous animation and in the meanwhile create a new animation below the original one. I am aware that one has to add some effect clearing logic in useEffect but I just do not see why.
I am new to React and only have a very basic understanding of how React works in the background, here is what I think: basically when I change the second div tag, the diff algorithm notices that it has been altered, so it goes ahead and tries to modify the dom associated with it. In my opinion it can 1). either update the current dom 2). delete it and append a new dom. In the first situation, shouldn't it leave the first animation unchanged without adding a new animation? In the second situation, if it gets rid off the current dom, why would the original animation be kept?

React can do both scenarios you mentioned: update existing and destroy & re-create. And React uses its own algorithm to decide when to do which.
You can force React to use destroy & re-create strategy using a special props called key.
Here is an example: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
Quoted from the link above:
When a key changes, React will create a new component instance rather than update the current one
I couldn't run the example you provided via code sandbox and just by reading your code, I can see that your App component has no props and no state. Thus, there is no way for you to update it.
Your code said: when componentDidMount use lottie to do some animation inside the second div. So, chances are the animation issue you're having is probably coming from lottie not React.

Related

How can i write a test using React Testing Library to check if hover changes the components style?

I am testing my application, and encountered a problem. When trying to test whether a row in my Dropdown component applies an effect on hover, I noticed I was not able to check elements for their background color, which I find odd.
Trying to use the jest-dom matcher "toHaveStyle()", the following is an example where I cannot for the life of me get it to work.
dropdown.test.tsx
test('Should contain clickable elements that change style when hovered', () => {
const dropElement1 = screen.getByLabelText('testLabel1');
expect(dropElement1).toHaveStyle('background: white');
});
Error
I have also tried this by using 'background-color', by using the hex value (another interesting bug is that PrettyDom converts hex to RGB), or by adding ; to the declaration in toHaveStyle().
I am certain that the element is indeed white, and I can't understand what is going wrong. If my approach is bad practice and you have a better idea of how to check this, or you have a solution to my problem, please, let me know!
Your testing case can't find the dropElement1 styles because it's a drop-down menu and not opened since you just render the Dropdonw component.
You need to simulate a mouse hover or clicking action on the DropDown menu and then expect to have styles property for it.
import React from "react";
import { render, screen, fireEvent, waitFor } from "#testing-library/react";
import { Dropdown } from "./Dropdown";
test('Should contain clickable elements that change style when hovered', async () => {
render(<Dropdown />);
fireEvent.mouseEnter(screen.getByText('drop-down-btn'));
await waitFor(() => screen.getByTestId('dropdown-menu'))
expect(screen.getByLabelText('testLabel1')).toHaveStyle('background: white');
});
Note: as you have not posted the Dropdown component, I put some sample names for getting your toggles and drop-down menu. also, you can read about the mouse events on the react-testing-library. you can also use mouseOver but it depends on your drop-down menu implementation.

Why does using React Context with Framer Motion not work?

I have a version of a slideshow where the state is being stored locally, you can see that the slideshow works great and the slide components only are unmounted once the animation is complete. https://stackblitz.com/edit/react-framer-motion-slideshow-official?file=src%2FSlideShow.js
Once I added the context to handle the values, the animation sliding still works but the exiting component is replaced with the new slide content when the animation begins, which looks really strange. Also the custom value for the slide directions seems to be broken. https://stackblitz.com/edit/react-framer-motion-slideshow-official-context?file=src%2FSlideShow.js
Do you have any ideas how I can get the animation to work correctly again when using context?
Everything that consumes a context re-renders every time that context’s state changes. So the children of your Slides component
rerender
see that the new variant = to the next state
appear at the destination
If I were you I wouldn't use context. If you really want to not explicitly pass the same props over and over you can do
{[
MainSettingsSlide,
ChangeLanguageSlide,
LanguageDetailsSlide,
BlockedSitesSlide
].map((Component, i) => (
<Component
activeSlideName={activeSlideName}
onNavigateSidebar={onNavigateSidebar}
key={i}
/>
))}
Sorry for the indirect answer :)
Edit two days later
In rereading your question I realize there are some other problems
You need to always conditionally render based on props not context
const Slide = ({ children, slideName, className, activeSlideName }) => {
// This context will update outside of framer-motion
// framer-motion animating something in while it is animating something out is
// predicated on you giving it control by using props
// const { activeSlideName } = useSlideShowContext();
// console.log('activeSlideName in Slide', activeSlideName);
// console.log('---------------------');
if (activeSlideName !== slideName) {
return null;
}
Your onNavigateSlideShow was using slideDirection instead of direction
const onNavigateSlideShow = ({ slide, direction = 'forward' }) => {
// const onNavigateSlideShow = ({ slide, slideDirection = 'forward' }) => {
console.log('ccc', direction);
setActiveSlide([slide, direction]);
};
I still can't get the directions to go in the right direction
I think this is due to a race condition between the direction being set and when the animation is kicked off
If you click the back button before the animation completes it works as expected.
Here is where I got to: https://stackblitz.com/edit/react-framer-motion-slideshow-official-context-dftoab?file=src/SlideShow.js
Sorry that this is again not a complete answer. I think I am coming to the same conclusion as before that the two apis probably shouldn't be mixed. Especially due to edge cases like this one.
There have been a decent number of questions recently about context and AnimatePresence so made sandbox for most of the cases that I could think of: https://codesandbox.io/s/framer-motion-using-context-with-animate-presensce-nuj0m

reactJS adding an event listener to a navbar tile

I am running the current version of reactJS with hooks. I have three code modules in my app: header.js which creates a navbar and exports it to app.js which adds some other objects and exports all of this to index.js.
I am trying to add an event listener to the individual tiles in the navbar so that I can redirect to the appropriate page.
code
var listenerElement = document.getElementById("Tile1");
if (listenerElement !== null) {
listenerElement.addEventListener("click", navbarClicked) ;
console.log(listenerElement);
} else {
console.log("Element with ID=Tile1 not found");
}
<div id="Tile1" className="linkcontainer">Home</div>
/code
However, I cannot find an appropriate place to add the event-listener and the element with ID "Tile1" is never found - perhaps because it hasn't been rendered as yet?
The element in question is only rendered by index.js but I can't add a function after the reactDOM.render block in index.js - I get an error "not a react function"
Any suggestions would be much appreciated :-)
For react, you should use "refs" to link to a specific aspect of the navabar.
But as you are just trying to have something affect the navbar, you should use the "onClick" property for the div or iconButton or similar.
for example in a function component:
function handleClick() {
console.log("clicked");}
<nav> <iconButton onClick={handleClick} > button </iconButton> </nav>
https://reactjs.org/docs/handling-events.html
In react, you don't work with the DOM directly. React makes a copy of the DOM called virtual DOM and then compares them to update the DOM. You should add your event listener using props.
So instead of:
document.getElementById("Tile1").addEventListener("click", navbarClicked);
you should do <div onClick={navbarClicked} id="Tile1" className="linkcontainer">Home</div>

Newly created preact Component retains UI state of old component (different from react)

I have a simple Preact component which contains a checkbox:
class Cb extends Component {
render() {
return (<div>Checkbox: <input type="checkbox" /></div>);
}
}
In a parent Component, this Cb is conditionally added like this:
{ this.state.show ? <Cb /> : <div>Nothing</div> }
Now for the strange part: If you follow these steps:
Check the checkbox
Toggle state.show in the parent Component, removing the Cb
Toggle state.show in the parent Component again, creating a new Cb
Then the newly created checkbox is still checked!
How is this possible? The checkbox is truely removed and a completely new Cb instance is created (I checked using log messages in the constructor). Where is this state stored?
Additional weirdness: The behavior is different in React (there, the newly created checkbox is not checked).
Here are two Codepens with the same code in Preact and React, where you can compare the behaviors.
This has to do with how Preact recycles components. There is a GitHub issue along the same lines as what your question is. Now to fix the problem you will have to reset the checked value in componentWillUnmount
componentWillUnmount () {
this.cb.checked = false;
}
Where this.cb is a ref to the checkbox.
Working Codepen with the modification.
I made the checkbox a ref because there is another issue with componentWillUnMount and using querySelector (and its also shorter to type)
Edit: As commented this is only valid for preact 8.x. preact X removes component recycling.

React: empty placeholder div gets re-rendered and thereby removes children (asynchr attached chart)

I am working on a widget dashboard which has DC.JS charts as content for each widget.
Widgets are created/ removed using react-grid-layout which creates an empty placeholder node like this:
<div id={"content_" + this.props.id} className="widgetContent"> /* chart is later drawn here */ </div>)
DC.JS later selects the div by Id and attaches its SVG chart as a child.
The problem is that for some events (like toggling static or changing Ids of the widgets), react re-renders the widgets and thereby "overwrites" the existing charts (children) with a brand new empty placeholder div as above.
My question is if that issue can be solved by React-techniques (can I prevent a div from ever being re-rendered?) or if this is an issue with the library itself.
Very similar code can be found here. The code in action is here. Imagine the snippet line above (the empty chart placeholder where a chart is attached later) in line 44.
The common solution here is to wrap this chart in a component where shouldComponentUpdate is set to false. That way react will never alter the element which your charting library modifies. An example wrapper component can be found here (including below)
var React = require('react/addons');
var ReactIgnore = {
displayName: 'ReactIgnore',
shouldComponentUpdate (){
return false;
},
render (){
return React.Children.only(this.props.children);
}
};
module.exports = {
Class: ReactIgnore,
Component: React.createClass(ReactIgnore)
};

Resources