React: rendering vs running your components
Recently I stumbled upon on an interesting bug which reminded me what is actually happening with my components when React is rendering them.
Consider the following code:
const Form = function () {
const Input = () => <input placeholder='type here'/>
return <Input />;
};
const App = function () {
const [ counter, setValue ] = React.useState(0);
return (
<React.Fragment>
<Form />
<button onClick={ () => setValue(counter + 1) }>increase</button>
</React.Fragment>
);
}
ReactDOM.render(<App />, document.querySelector('.output'));
I have an <App>
component that has its own state and every time when we press the button the value is incremented. We all know that because the state is updated React re-renders the component. Re-rending for React means executing/running our function again and seeing what React elements are generated. Then it decides what changes (or not) in the actual DOM tree.
So, rendering a React app means running all of our functions (which we call "components") and examining their output. This may lead to an interesting side effects. Like the bug that I encountered.
You can see it yourself here. We type something in the field and if we press the button (or in other words update the state of the <App>
component) the value in the input field disappears. Of course this example is simplified version of the actual implementation but perfectly illustrates the problem. What is actually happening is that React replaces the input field with a new one every time when we hit the button. I was wondering why is this happening considering the fact that I'm not changing any of the <Form>
's or <Input>
's props. So the generated virtual DOM should be the same and no changes to the actual DOM should be applied. But of course, that's not the case.
On every rendering cycle React DOES run our components. Over and over again our functions are executed. That's why I'm saying that there is a big difference between running a component and rendering a component. Each component is re-run multiple times but may be rendered just once. The problem in my case was that I was dynamically creating the <Input>
. Every time when <App>
component is updated the <Form>
component gets executed and a brand new <input>
field gets created. So, React behaves correctly by replacing the actual DOM element.
The solution is either to create <Input>
outside of <Form>
or to use React.memo
which will prevent the re-running of the Form
function. I had to pick the second option because of reasons not listed in this example.
const Form = React.memo(function () {
const Input = () => <input placeholder='type here'/>
return <Input />;
});
const App = function () {
const [ counter, setValue ] = React.useState(0);
return (
<React.Fragment>
<Form />
<button onClick={ () => setValue(counter + 1) }>increase</button>
</React.Fragment>
);
}
ReactDOM.render(<App />, document.querySelector('.output'));