prevent component from unmounting when displayed in different parts of the DOM - javascript

The layout of my app changes depending on some choices made by the user so the same component gets hung under different nodes in the DOM. Unfortunately, React unmounts and re-mounts the component. As a result, my component loses the state is has accumulated. I have tried to add a key property to convey the information that this is the same instance but I got the same results. The below is obviously a SSCCE. Every time the user clicks the button, component A gets unmounted and re-mounted:
class A extends React.Component {
componentWillUnmount = () => {
console.log('map::componentWillUnmount()');
}
componentDidMount = () => {
console.log('map::componentDidMount()');
}
render() {
return (
<div>
this is component A
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state={i:0};
}
increment = () => {
this.setState({i: this.state.i+1});
}
render() {
console.log('app::render');
if (this.state.i % 2 === 0)
return (
<>
<div>
<div>
<A key={42}/>
</div>
</div>
<button onClick={this.increment}>re-render</button>
</>
);
else return (
<>
<div>
<A key={42}/>
</div>
<button onClick={this.increment}>re-render</button>
</>
)
}
}
Just to clarify, my code isn't trying to achieve anything except reproduce the issue, like I said, it's a SSCCE. Surely there are apps where they user can change the layout from a "preferences" menu so that a component ends up in a different place in the DOM depending on the user's preferences. I can't imagine its acceptable to lose the state in such a situation. What is the proper way to deal with this kind of situations?

This is because the conditional rendering is returning two different tree structures where the path to A is different.
React.Fragment > div > div > A
React.Fragment > div > A
Imagine if we're mimicking React by using plain JS to do the mounting and unmounting manually, we will have to:
Store <A/> as a variable
Remove the inner <div/> (<A/> will automatically be removed as well)
Then append the previously stored <A/> into the outer <div/> (which actually is not a good approach either because this assumes <A/> will never need to update itself once it's mounted, even if the props change)
So long as there's a remove and append, it's quite equal to a React component being unmounted and then mounted again.
The code is a bit vague and I cannot tell what it's trying to achieve by having 2 <div/>s in the first condition and only 1 <div/> in the second. But perhaps you can use ternary operators to conditionally render just what you need and leave <A/> alone (and may be a bit of CSS to make the inner <div/> appear as if it is nested inside another <div/>). This way, <A/> will not unmount when <App/> changes state.
return (
<>
<div>
<A />
{condition ? // where `condition` is a boolean
<div style={{
position: 'absolute'
// and may be other styles to achieve the visual trickery
}}>
{/* your content here */}
</div>
:
null
}
</div>
<button onClick={this.increment}>re-render</button>
</>
)
By the way, they say 42 is the answer to everything, but I think not for this case. Setting the key to 42 won't help. 😅

Related

How can I use React Material UI's transition components to animate adding an item to a list?

I have this class.
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.add = this.add.bind(this);
this.clear = this.clear.bind(this);
}
add() {
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
}
clear() {
this.setState({ items: [] });
}
render() {
return (
<div>
<div>
<button onClick={this.add}>Add</button>
<button onClick={this.clear}>Clear</button>
</div>
{/* This is wrong, not sure what to do though... */}
<Collapse in={this.state.items.length > 0}>
<ul>{this.state.items}</ul>
</Collapse>
</div>
);
}
}
Sandbox link: https://codesandbox.io/s/material-demo-ggv04?file=/Demo.js
I'm trying to make it so that every time I click the "add" button, a new item gets animated into existence at the top of the list and the existing items get pushed down. Not sure how to proceed though.
Extra Resources
Example of what I'm trying to achieve: https://codeburst.io/yet-another-to-do-list-app-this-time-with-react-transition-group-7d2d1cdf37fd
React Transition Group Transition docs: http://reactcommunity.org/react-transition-group/transition (which seem to be used internally by Collapse)
I updated your Sandbox code to achieve what you wanted, but I don't think MaterialUI is the best library for that (I could be missing a better way to do it).
The challenge is that when you add a new item, that doesn't exist in the DOM yet. And most of those animation libraries/components require the element to be in the DOM and they just "hide" and "show" it with a transition time.
I had a similar situation and after some research, the better library I found that can handle animation for elements that are not yet in the DOM, was the Framer Motion. (You can check their documentation for mount animations)
Anyway, here is the link for the new Code Sandbox so you can take a look. The changes I made:
Removed random key
In the map function that creates your list using the <Collapse /> component, there was a function to get a random integer and assign that as a key to your component. React needs to have consistent keys to properly do its pretenders, so removing that random number fixes the issue where your "Toggle" button wasn't animating properly. (If your list of items doesn't have an unique ID, just use the index of the map function, which is not a good solution, but still better than random numbers).
<Collapse key={i} timeout={this.state.collapseTimeout} in={this.state.open}>
{it}
</Collapse>
Added a new function to control the toggle
The approach here was: add the item in your list and, after the element is in the DOM, close the <Collapse />, wait a little bit and open it again (so you can visually see the animation). In order to do that, we needed a new "toggle" function that can explicit set the value of the collapse.
toggleValue(value) {
this.setState(() => {
return {
open: value
};
});
}
Added a variable timeout for the collapse
The last issue was that, closing the <Collapse /> when the new item is added, was triggering the animation to close it. The solution here was to dynamically change the timeout of the collapse, so you don't see that.
setCollapseTimeout(value) {
this.setState(() => {
return {
collapseTimeout: value
};
});
}
When adding the element to the list, wait to trigger the animation
Again, to work around the issue with elements not yet in the DOM, we need to use a setTimeout or something to wait to toggle the <Collapse />. That was added in your add() function.
add() {
this.toggleValue(false);
this.setCollapseTimeout(0);
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
setTimeout(() => {
this.setCollapseTimeout(300);
this.toggleValue(true);
}, 100);
}
Again, this is a hacky solution to make <Collapse /> from MaterialUI work with elements that are not yet in the DOM. But, as mentioned, there are other libraries better for that.
Good luck :)
Ended up here earlier on and then came back to create a sandbox showing hopefully a simple method for this scenario. The material-ui docs are a bit (lot) light in this area and I was fighting with a very similar situation, but I tried something with TransitionGroup from react-transition-group, crossed my fingers and it seemed to work.
Forked CodeSandbox with TransitionGroup
The gist is that you
wrap all of the components you want to transition in the <TransitionGroup> component
Inside the TransitionGroup, put in the "condition" (logic or loop output) for the data you want to render
Wrap the individual components you want to transition with transition component of your choice - <Collapse> in this example
e.g. In its most simple setup where "items" is an array of unique numbers coming from either props, state or a redux store
<TransitionGroup>
{items.map(item => (
<Collapse key={item}>
I am item {item}
</Collapse>
))}
</TransitionGroup>
With this setup I have found that I didn't need to put any props on the TransitionGroup or Collapse, and the TransitionGroup handled all the mounting and unmounting in the loop rendering. Material UI doesn't produce the lightest of HTML output, but I guess it's all rendered on the fly so maybe that makes it better (unless you have thousands of elements, then things start to drag).
You can even go a step further and wrap the whole thing in another TransitionGroup to cover situations where you want to remove the whole thing without transitioning all of the individual items - in this instance I switched it to a <Slide>. I was absolutely certain that this wouldn't work, but it seemed to not care. You can also try and be semantic and use the "component" property rather than wrapping in another element e.g.
{items.length > 0 && (
<TransitionGroup>
<Slide>
<TransitionGroup component="ul">
{items.map((item) => (
<Collapse component="li" key={item}>I am item {item}</Collapse>
))}
</TransitionGroup>
</Slide>
</TransitionGroup>
)}
I have changed the sandbox in the following ways
Included TransitionGroup from react-transition-group
Changed the "add" logic so that the components aren't part of the "items" array - the array only contains the data required to render the components
I have added a simple "count" and pushed that to the array to give the items a unique index (had originally used Math.random, but I wanted a "prettier" output). Generally your items will probably be coming from a database somewhere where a unique id will already be set.
Rendered the components in a loop based on the data in the array (this could be done in a separate function, but the gist is that the components aren't being stored in the array)
added a "delete" function to show the removal of single items
wrapped the whole group in a second <TransitionGroup> to show that the unmounting can happen in a group level
Put in some simple styling to get a better idea of the effect. You could use Material UI components here, but just wanted to keep it simple.
Hope this helps someone in the future.

Using the hidden attribute vs conditionally rendering a component

I have recently discovered an alternative to conditionally rendering a component in JSX, which is to use the hidden HTML attribute.
Example
function Parent() {
return {!hideChild && <Child />}
}
vs
function Parent() {
return <Child hidden={hideChild} />
}
function Child({ hidden }) {
return (
<div hidden={hidden} >
//my content
</div>
)
}
So far I have not noticed any performance or alike issues when using hidden. In saying that, are there any downsides to have lots of HTML on the page that is hidden?
For me, this approach has served well when I want to retain the component state and have the functionality of toggling the visibility of the components UI.
Is this bad practice? Should we be conditionally rendering components instead?
The difference is that when using conditional rendering, the logic inside the conditionally rendered UI will not be executed if the condition fails.
But using the hidden attribute will execute the logic but only hides the UI.
Example:
import React from 'react';
const A = () => {
console.log('A rendrerd');
return <h1>A</h1>;
};
const B = ({ hidden }) => {
console.log('B rendrerd');
return <h1 hidden={hidden}>B</h1>;
};
const Test = () => {
return (
<div>
{false && <A />}
<B hidden={true} />
</div>
);
};
export default Test;
A will never call its console.log statement.
B is hidden but it will log B rendered.
I think this is worth mentioning. hidden attribute acts more or less like display: none with css. The truth is "the component will be rendered but only hidden from your display."
Just to illustrate, below is a photo of some html and their output. you realize that the output doesn't show the <p> that is decorated with hidden attribute but if when you inspect the rendered code, you realize that <p> was actually renderd. So you can image have multiple components in react where they will all be rendered but only displayed based on that hidden attribute.
Well I may not be sure of the performance involved but it's obvious that doign a simple if() to condition render a component will depending on the size of your components be much quicker than rendering everything and only relying on their hidden attribute decoration. And this also means that a user can just Inspect element and remove hidden attribute to display that component which is intended to be hidden.

React - Does changing an unused prop rerender a component?

I'm new to React and I have a doubt about rerendering and I haven't found answer regarding this specific case.
Let's assume I have a <Parent /> component and a <Child myProp='10' /> component and that the Child component does not use that myProp prop in the return statement, nor anything related to that prop, (in other words, the child will always return the same html elements independent of whatever that number prop is), I just want to use that number prop to identify that specific component to pass it back to the parent in an event handler. If that number prop changes, will the Child component rerender? Here is a simplified code example of what I mean:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
number1: 5,
number10000: 20
};
this.eventHandler=this.eventHandler.bind(this);
};
eventHandler(argGivenByChild){
//setState depending on which Child was clicked, argGivenByChild will be the identifier passed to the Child.
}
render() {
return (
<div>
<Child identifier={this.state.number1} clickHandler={this.eventHandler}/>
<Child identifier={this.state.number10000} clickHandler={this.eventHandler}/>
</div>
)
}
}
function Child(props) {
return (
<div onClick={()=>props.clickHandler(props.identifier)}>
<h1>Some heading</h1>
<p>Some paragraph</p>
</div>
)
}
Whenever I click any of the Child, the identifier prop will change, but the Child never uses it to visually render anything anyways, it's only passed to the event handler when it's clicked on. So, in that case, does React rerender the Child to account for the prop change, even though nothing changed on screen?
On one hand, I think there's nothing to rerender, as both the html and css remain the same (the Virtual and real DOM remain the same, don't they?), but on the other hand, internally the component is being modified, so maybe it has to rerender to reflect that change to the component, particularly in the event handler? What exactly happens in this case?
Thanks in advance

React: change modal's content without passing entire DOM elements as a props?

I am new to React and currently I am trying to build a Two-Fact-Auth Modal-like UI in my project.
The Modal looks relatively sample as well.
Title
A message
Modal Content(which can be a inputbox, or one or more drop-down selections, or just displaying string)
A Button
Imagine there are some modals: First one ask you to enter your phone number. After you typed your phone number, it get direct to second modal and second modal will display the phone number you typed and ask you to confirm, and third modal will display other things and so on.
My approach to this is to build a my own modal component using ReactStrap.
export default class ModalControl extends React.Component{
render(){
return(
<div>
<Modal isOpen={this.props.isOpen}>
<ModalHeader >{this.props.title}</ModalHeader>
<ModalBody>
<p>{this.props.message}</p>
<p>{"Content that is change dynamically"}</p>
</ModalBody>
<ModalFooter>
<Button color="info" onClick={() => this.props.clickAction>{this.props.buttonLabel}</Button>
</ModalFooter>
</Modal>
</div>
)
}
}
However, One of the problem I have is to change the modal content. Since the Modal does not know if itself contains a selection, 'A input box', or 'Just a string', what should i do so the ModalControl can take content that is dynamically changing?
My attempt:
I tried to pass entire DOM elements as a string to modal and parse it in the modalControl. However, I have read a SO post that saying passing Dom Elements is not recommended in React.
In my main, i have something like this, but apparently it is not rendering
<ModalControl
isOpen={true}
title={"Code Authentication"}
message={"For your security, we need to verify your identity by sending a code to your phone number"}
buttonLabel={"Verify Code"}
>
<div className="row">
<div className="col-sm-6">
{this.getPhoneList()}
</div>
<div className="col-sm-6">
{this.getMethodList()}
</div>
</div>
</ModalControl>
What i want to achieve: How do I implement a modal class whose modal content that is dynamically changing? Since I am new to React, I am not sure if this is the best practice. If it is not, is there a better approach?
You have to maintain a state which holds variables that can dynamically change.
First you set the state.
constructor(props) {
super(props);
this.state = {
variableX: "default string"
};
}
You can then use React's setState() method to update the state
this.setState({
variableX: "updated string"
});
In your render method you can then access the variable
render() {
const { variableX } = this.state;
return (
<div>{variableX}</div>
);
}
You can also pass the state as a prop to child components
// Parent component
render() {
const { variableX } = this.state;
return (
<div>
<ChildComponent variableX={variableX} />
</div>
);
}
// Child component
render() {
const { variableX } = this.props;
return (
<div>
{variableX}
</div>
);
}
One thing to note is that you never want to mutate the state. You can search many articles regarding this using the terms "react state immutability"
You should read: https://reactjs.org/docs/state-and-lifecycle.html
In a large application where it's more difficult to maintain a state, and where you don't want to keep passing properties all the time, you can consider using Redux with React:
https://redux.js.org/basics/usagewithreact

How do I test a React component that calls a function on its parent, which changes props on the component?

The problem:
component Child's props are passed down as the values of Parent's state.
Child has a method that calls a method on Parent, which updates the state of Parent.
When Parent's state updates, one of Child's prop values change. as in: <Child prop1={this.state.prop1}>
What is the right way to go about testing that this process is happening as expected?
Here's some example code to make the problem clearer:
//App.js
import React, { Component } from 'react';
import Content from './Content';
class App extends Component {
constructor(props){
super(props)
this.state = {
page: 'home',
}
}
gotoAbout(){
this.setState({
page: 'about',
})
}
render() {
return(
<Content page={this.state.page} gotoAbout={this.gotoAbout.bind(this)} />
)
}
}
As you can see, the parent component App passes a prop, and a function that can change the value of that prop to its child component, Content.
The Content component would then do something like this:
//Content.js
import React, { Component } from 'react';
class Content extends Component {
constructor(props){
super(props)
}
gotoAbout() {
this.props.gotoAbout()
}
render(){
if(this.props.page = 'home'){
return(
<div>
<p>this is the home content</p>
<button onClick={this.gotoAbout}></button>
</div>
)
} else {
return(
<p>this is the about content</p>
)
}
}
}
The above is a simplified example, but I think it gets the point across. What would be the best way to write a test for this kind of component-prop flow?
I generally first start testing the components in isolation using shallow rendering with its expected functionality and then test components with composition.
e.g To test Content component
1.test whether it behaves correctly for props or state changes
2.test if it performs event's correctly such as button click or any other events through simulation
const wrapper
= shallow(
<Content
page={"home"}
gotoAbout={()=>{ console.log("fake goAbout")}}
/>
);
Now Check if rendered structure is matched as expected for prop page={"home"}
expect(wrapper.containsMatchingElement(
<div>
<p>this is the home content</p>
<button onClick={this.gotoAbout}></button>
</div>
)).to.equal(true);
Similarly test for other prop page={"about"} whether content renders correctly or not.
wrapper.setProps({ page: 'about' });
expect(wrapper.containsMatchingElement(
<p>this is the about content</p>
)).to.equal(true);
After that you can test for button click events
clickCount = 0;
const wrapper
= shallow(
<Content
page={"home"}
gotoAbout={()=>{ clickCount = clickCount + 1;}}
/>
);
Now you can check whether clickCount is greater than zero, after simulating the click event.
wrapper.find('button').simulate('click');
After that you may start testing the App component.
const wrapper = shallow(<App />);
const childWrapper = wrapper.find('Content');
After that you may create another Content component separately via shallow rendering and match those two for equal html structure, props, states etc.
const twrapper
= shallow(
<Content
page={"home"}
gotoAbout={()=>{ console.log("fake goAbout")}}
/>
);
expect(twrapper.html()).to.equal(childWrapper.html());
You can also check whether prop is passed correctly to rendered child elements -
expect(childWrapper.prop('page')).to.equal("home");
There may other better way available as well for testing the react components and these are just simple test examples.
Enzyme provides lots of ways to test your components and there is no hard and fast rule I guess. But you should at least test the expected functionality and features of your component.
Also your test cases make sure that any new changes being made to the component don't break your test specification.

Categories