React Component Lifecycle - javascript

To my knowledge, when some events happen, react creates a virtual-DOM from scratch and compares it to the old virtual-DOM. If that is the case, when the render method is called, react should create components and return them to compare.
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Demo extends Component {
constructor(props) {
console.log("constructor called");
super(props);
this.state = { dummy: 1, };
}
render = () => <button onClick={this.props.onChange}> button </button>;
}
class App extends Component {
state = { num: 0, };
onChange = () => {
this.setState({ num: this.state.num+1 }); // mutate state to trigger a render
}
render() {
console.log("rendered");
return (
<Fragment>
<Demo onChange={this.onChange} />
</Fragment>
);
}
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
A quick console log reveals that the constructor is called the first time to mount the Demo component onto the page. However, subsequent renders do not create new Demo objects that are compared with the old virtual-DOM (since the constructor is not called).
At first, my theory was that when the constructor of Demo is called, it then calls the super constructor to check if a similar object with the same props already exists. But that was not the case as proven by moving console.log("constructor called"); before calling the parent constructor.
So my question is how does react know not to create another object?

The key here is that Demo is not unmounted. When you first render the app, it renders and mounts the Demo component and passes the onChange prop. But, when the callback is invoked from Demo it sets the state on App. Calling setState in App doesn't unmount the Demo component, so there's no need to mount it again. When the component is mounted initially is when the constructor runs. If you had a toggle on the App component that would only show the component within render if a certain condition is true, that would trigger the component to unmount.
Check out this codesandbox and play around a little: https://codesandbox.io/s/lifecycle-methods-vivn6?file=/src/Lifecycle.js.
Also, this is a cool diagram to get a sense of what's happening: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
The main key is that the constructor runs when the component mounts. It will only run again if the component is unMounted from the DOM and then remounted later.

Related

Re-rendering in React functional component - why this behavior?

This question concerns the definition of callback functions inside functional components, with the callback functions being passed down as props to child functional components.
I am aware that when defining functions inside functional components and those functions are being passed on as props to child components we should use useCallback. In this example I am deliberately not using it.
I have a dummy React app as below:
App.jsx
import { useState, useCallback } from 'react';
import './App.css';
import TestComponent from './components/TestComponent';
import TestComponent2 from './components/TestComponent2';
function App() {
const [message, setMessage] = useState("");
console.log('Defining myArrowfunc');
const myArrowFunc = () => console.log('Hello!');
console.log('myArrowfunc defined');
return (
<>
<TestComponent message={message} myArrowFunc={myArrowFunc} />
<TestComponent2 myArrowFunc={myArrowFunc} />
<input
type="text"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button
type="button"
onClick={() => {
console.log('Setting message');
setMessage("x");
}}>Set to x</button>
</>
);
}
export default App;
TestComponent.jsx
import { useEffect } from 'react';
function TestComponent(props) {
useEffect(() => {
console.log(`TestComponent.useEffect, props.message = ${props.message}`);
}, [props]);
return <h2>TestComponent - {props.message}</h2>;
}
export default TestComponent;
TestComponent2.jsx
import { useEffect } from 'react';
function TestComponent2(props) {
useEffect(() => {
console.log(`TestComponent2.useEffect`);
}, [props.myArrowFunc]);
return <h2>TestComponent2 - Placeholder</h2>;
}
export default TestComponent2;
Starting the app, the page loads and we are here:
In the console window everything looks like expected. The console.log statements Defining myArrowFunc and myArrowFunc defined ran, and we can see the console.log statements from the useEffect hook within TestComponent and TestComponent2 which ran after they were rendered.
Now we click on the button Set to x, which invokes the callback function attached to the onClick event of the button. This callback calls setMessage("x") which updates the state of the App component and consequently triggers a re-render of the component.
In the console window we can see that the App functional component ran (e.g. from the console.log("Defining myArrowFunc)) and one can also see that the useEffect hooks of the child components ran after they were re-rendered.
Now, that TestComponent re-rendered is of course understandable. message state is a prop on TestComponent which would cause a re-render of TestComponent. Also, in the dependency array of TestComponent is specified props (not props.message), but props is a new object on every render (right?) and hence the reference comparison tells us props has changed and so useEffect runs.
That the useEffect hook of TestComponent2 ran can be understood (right?) from the fact that myArrowFunc is re-defined on each render of App, and so the prop passed to TestComponent2 has in fact changed (reference comparison).
Here comes the part where I become confused: the message state in App now holds "x", and so additional clicks to the button will not change the state, and so should not trigger a re-render of App (and any child components dependant). When I click the button the following output happens:
The statements console.log('Defining myArrowfunc'); and console.log('myArrowfunc defined'); ran. What does this mean? Did the component re-render? If the App function ran, then it should have defined a new myArrowFunc (right?), and since TestComponent2 takes that as a prop, it should have re-rendered and run its useEffect hook?
What's interesting as well is that when I again click the button, it does not look like App ran, the output becomes only "Setting message".
Would very much appreciate an outline / explanation of what exactly is going on here.
When you click the button, a console log with setting message will run, and then a setState with will make a re-render.
At the following click, react is clever enough to see that the state you are setting is the same as before so it doesn't re-render
If you change then, the value and click the button, it will re-render, but not for the following cases with the same value.
To sum up, the first's console.log won't run if there's not a re-render, and the procedure of react--if default--memoizing will prevent it

React | Having trouble enacting state changes in child component

I am coding a checkers web app and have set up user interaction by placing onclick functions on the components. I have two different components pieces and spaces. Ideally, the onclick functions are linked to methods in the parent component which interpret the state of the component being clicked and responds by returning data back to the component or a separate component. The issue is, I do not understand how to send data to child components from the parent following the mounting of said child component. I understand you can use props to initialize the state in a child component, but is there anyway I can update the child component from the parent following this initialization? I am new to react so I'm not exactly sure how inter component communication works quite yet.
You can pass update function to child, I hope this helps.
Parent
import React, { Component } from 'react'
export default class Parent extends Component {
constructor(props) {
super(props);
this.state={
something:""
}
}
updateSomething = (anotherThing) => {
this.setState({something:anotherThing})
}
render() {
return (
<div>
{/* we are passing updateSomething function in props*/}
<ChildComponent updateSomething={(anotherThing)=>this.updateSomething(anotherThing)}/>
</div>
)
}
}
Child
import React, { Component } from 'react'
export default class ChildComponent extends Component {
render() {
return (
<div>
{/* we are using updateSomething function to update parents state*/}
<button onClick={()=>this.props.updateSomething("anotherThing")}>Update Parents state</button>
</div>
)
}
}

setting a key in a component does not re-render the component on state updates

I have a component that has two children:
A component where the user inputs and select a topic
A component that renders results based on the topic selected in the previous component
So this is the code of the parent component:
import SearchBox from "../../components/SearchBox";
import Results from "../../components/Results";
class Home extends Component {
constructor(props) {
super(props);
this.state = {
selectedTopic: null,
};
}
onSelectTopic = (topic) => this.setState({
selectedTopic: topic,
});
render() {
return (
<div>
<SearchBox
onSelectTopic={this.onSelectTopic}
/>
<Results
key={this.state.selectedTopic && this.state.selectedTopic.id}
selectedTopic={this.state.selectedTopic}
/>
</div>
);
}
}
export default Home;
Basically, the SearchBox component updates the state of the parent component that propagates the selection to the Results component.
The Results component has its own state, that I want to clear when a new selection is passed. Instead of using getDerivedStateFromProps in Results to clear its state I have added a key to <Results key={...}/> as in the snippet above.
I was expecting that every time the state in the parent component updates (ie, the onSelectTopic method is called above), the Results component will be recreated, but instead the component does not update at all. Am I missing something? If I remove its key property, Results updates as expected (ie without clearing its internal state, which is what I want to achieve by using the key)
Btw, I'm using preact instead of react, but my understanding is that the behaviour regarding the key property on components is the same.

React componentDidMount timing

Is componentDidMount lifecycle method independent from sibling components? From example below it seems like it is invoked only after all sibling components have been mounted.
Let's say we have a top level component which renders 2 child components, first having simple render() and another having relatively slower render().
Sample to reproduce: https://codesandbox.io/s/j43klml9py?expanddevtools=1
TL;DR:
class SlowComponent extends Component {
componentDidMount() {
// perf mark
}
render() {
// Simulate slow render
// Takes 50ms
return <h3>Slow component</h3>;
}
}
class FastComponent extends Component {
componentDidMount() {
// perf mark
}
render() {
return <h3>Fast component</h3>;
}
}
class App extends Component {
constructor(props) {
super(props);
// perf mark start
}
componentDidMount() {
// perf mark
// measure all marks and print
}
render() {
return (
<div>
<FastComponent />
<SlowComponent />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
I expect componentDidMount timings to be like this:
FastComponent 10 ms
SlowComponent 50 ms
App 52 ms
But in reality what I get is both fast and slow component componentDidMount callbacks are fired at the same time, i.e.
FastComponent 50 ms
SlowComponent 51 ms
App 52 ms
Current sample and repro code uses mount callback but same applies to componentDidUpdate.
Full source:
import ReactDOM from "react-dom";
import React, { Component } from "react";
class SlowComponent extends Component {
componentDidMount() {
performance.mark("slow-mounted");
}
render() {
// Simulate slow render
for (var i = 0; i < 10000; i++) {
for (var j = 0; j < 100; j++) {
const b = JSON.parse(
JSON.stringify({
test: "test" + i,
test1: i * i * i
})
);
}
}
return <h3>Slow component</h3>;
}
}
class FastComponent extends Component {
componentDidMount() {
performance.mark("fast-mounted");
}
render() {
return <h3>Fast component</h3>;
}
}
class App extends Component {
constructor(props) {
super(props);
performance.mark("init");
}
componentDidMount() {
performance.mark("app-mounted");
performance.measure("slow", "init", "slow-mounted");
performance.measure("fast", "init", "fast-mounted");
performance.measure("app", "init", "app-mounted");
console.clear();
console.log(
"slow",
Math.round(performance.getEntriesByName("slow")[0].duration)
);
console.log(
"fast",
Math.round(performance.getEntriesByName("fast")[0].duration)
);
console.log(
"app",
Math.round(performance.getEntriesByName("app")[0].duration)
);
performance.clearMarks();
performance.clearMeasures();
}
render() {
return (
<div>
<h1>Demo</h1>
<FastComponent />
<SlowComponent />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
I assume you are using the latest React version (16.5.0).
Assuming two components, one slow and one fast wrapped in a parent component the execution flow will be the following:
Parent component calls its UNSAFE_componentWillMount method
Iterates all children with the order they are defined in its render method and calls their UNSAFE_componentWillMount method
Iterate all children and call their render method(here your slow component expensive render method will kick-in)
Iterate all children and call their componentDidMount method
Parent component calls its componentDidMount method
This whole process is being done synchronously.
Going back to your example, this is why you see almost the same timing to both your components, because all component lifecycle methods are called as soon as the work is completed for all components.
In general, conceptually React consists of 2 phases, "render" phase and "commit" phase.
The "commit" phase is when React applies any changes, and in your example the lifecycle method componentDidMount is called in this phase.
You can follow this flow by debugging React, and viewing the stack trace which is the following:
componentDidMount
commitLifeCycles
commitAllLifeCycles
callCallback
invokeGuardedCallbackDev
invokeGuardedCallback
commitRoot
The simple answer
All components will fire componentDidMount method after all renders are completed. Because before to mount elements to DOM we need to create them.
As per your life cycle order, your componentDidMount will be called (as the name itself suggests) after your component has been mounted. Which will be after the DOM has been mounted, which in doing so will call your render.
Now assuming if there is no I/O i.e, async event happening in your child component anywhere.
(and even if there are any async event happening, the functions will be executed but the call stack will maintain the execution in the event loop.. but this is a separate discussion)
-- Parent
-- Child-1
-- Child-2
Order of execution
Parent constructor()
Child 1 constructor()
Child 1 render()
Child 1 componentDidMount()
Child 2 constructor()
Child 2 render()
Child 2 componentDidMount()
Parent componentDidMount()
As per the ReactJS docs the mounting triggers in order are
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
Reference of mounting order from react documentation Mounting React Docs
On a side note, another detailed read on the order of execution.
Understanding React — Component life-cycle
Yes, ComponentDidMount lifecycle method is independent from sibling components. It depends on how your render takes time because it invokes after your render complete. But the component mounts in same order as they are in parent component. E.g - In above example first fast component mounts then second component.
And you can't calculate how much time a function takes to iterate a loops. So here your assumption of calculating time of slow component's ComponentDidMount lifecycle method was wrong. You can't expect specific time estimate based on number of iteration without profiling.

Getting a changed variable to show up in `render`

Probably a simple question with a simple answer, but I can't figure this out. Blame the heat. My simulator prints 'this is a test' on the screen, when I want it to say 'changed'. What am I doing wrong?
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Template from './src/components/Template';
export default class App extends React.Component {
constructor(props) {
super(props);
this.foo= "this is a test";
}
changeMe = () => {
this.foo = 'changed';
}
componentDidMount(){
this.changeMe();
}
render() {
return (
<Template foo={this.foo} />
);
}
}
Use state
You are using a class attribute witch will not trigger a re-render when changed. In react a component will re-render when it receives new props or when the state is changed (there's also a way to force it but best not to do that).
Example using the state to trigger a rerender
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Template from './src/components/Template';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state ={foo: "this is a test"};
}
changeMe = () => {
this.setState({foo:'changed'})
}
componentDidMount(){
this.changeMe();
}
render() {
return (
<Template foo={this.state.foo} />
);
}
}
When passing new props to a component it will also re-render (unless you implement componentShouldUpdate).
State explanation
So inside a react component you have a local state object in this.state it can be set in the constructor like this.state = {bar: 'foo'};. After the constructor has set the state it should only be changed with the this.setState() method.
Upon changing the state with setState the component will re-render with the updated values.
The state is not available outside of the component, at least not available as this.state because that would call the local state of the current component.
If you need to use a value from the state of a parent component you can pass it to a child. At that point it becomes a prop of the child accessible with
this.props
To change the value of state from a child component you should pass a function to the child that changes the state of the parent.
This process of passing state changing functions becomes increasingly complex as your app grows, I would suggest using a library like Redux to manage app state via actions and reducers. There is a steep learning curve but once you have a grasp of this modified flux methodology you will wonder how you ever lived without it.

Categories

Resources