React elements are nothing more than plain objects which contain information about component or DOM element that needs to be rendered. They also include information about properties those nodes should have. Among other properties there is one special - children
. It includes the children nodes.
Example of element creation:
const element = React.createElement(
"button", // element type
{ id: "sign-in-button" }, // properties
"Sign In", // children
);
The same can be represented in JSX syntax as follows:
const element = <button id="sign-in-button">Sign In</button>;
The last approach is nothing more than syntax sugar for function React.createElement()
.
No matter the syntax we use, in the end the same new object will be created:
{
key: null,
type: 'button',
ref: null,
props: {
id: 'sign-in-button',
children: 'Sign In'
}
}
The above is a simplification, in reality the structure looks more complicated:
As you can see newly created object has four key properties: type
, props
, key
and ref
. It has no methods and nothing on the prototype. The object is a description of the things that will be rendered on the screen when it gets passed to React render tree.
Having such lightweight objects stored in memory allows representing the entire hierarchy of elements that are supposed to be rendered on the screen. We can say that such objects represent the state of what is rendered on the screen at any particular moment.
In order for React to be performant and utilize the whole power of such representations objects should posses two properties:
In React world component is a function (or class) which can accept properties and returns element.
Here is an example of component:
const Button = ({ title }) => {
return <button className="button">{title}</button>;
};
Previously we saw an example of creating elements by using default HTML tags. But we can also pass components as the first argument into createElement
:
const buttonElement = React.createElement(Button, { title: "Sign In" }, null);
Or in a more usual way with JSX:
const buttonElement = <Button title="Sign In" />;
React is responsible for creating instances of the components. It means that in your code you never call them as functions like so:
Button({ title: "Sign In" });
React decides when component is created during rendering cycle. According to React documentation:
Components become more than functions. React can augment them with features like local state through Hooks that are tied to the component’s identity in the tree.
When your React application is rendered, React repeats the same process of creating elements. In the end it has full object representation of the DOM tree. This process is known as reconciliation. It traverses the components recursively and renders them (by calling render
function for class components or invoking a function for functional components) and returns an element tree. It’s triggered each time you use ReactDOM.render
or updating the state of the component.
children
Now when we know how elements are created we can use it to our advantage when optimizing rendering performance.
Let’s have a look at this component:
const Home = () => {
const [scrollTop, setScrollTop] = useState(0);
return (
<div
onScroll={(ev) => {
setScrollTop(ev.target.scrollTop);
}}
>
<div>Scroll position: {scrollTop}</div>
<Dashboard />
</div>
);
};
It updates state with scroll position when scroll happens. Whenever state update happens the entire components gets re-rendered. You can see that we have <Dashboard />
as a direct child of our scroll container. If it happens to be expensive in terms of rendering the UX will be degraded.
But we know that <Dashboard />
is nothing more than element creation definition. What if we could elevate it one level higher and pass it down as a property? In that case it wouldn’t be re-rendered when we update scrollTop
state. Lets use this idea.
First lets create a component holding scroll logic:
const ScrollContainer = ({ children }) => {
const [scrollTop, setScrollTop] = useState(0);
return (
<div
onScroll={(ev) => {
setScrollTop(ev.target.scrollTop);
}}
>
<div>Scroll position: {scrollTop}</div>
{children}
</div>
);
};
And then we can use ScrollContainer
in our Home
component passing down <Dashboard />
as children:
const Home = () => {
return (
<ScrollContainer>
<Dashboard />
</ScrollContainer>
);
};
The above notation is nothing more than syntax sugar for:
const Home = () => {
return <ScrollContainer children={<Dashboard />} />;
};
With this simple refactor we detached <Dashboard />
rendering from ScrollContainer
. Re-renders of ScrollContainer
wouldn’t cause <Dashboard />
to re-render as it belongs to Home
.