Level Up Your React: Insights and Notes from Nadia Makarevich’s Advanced Playlist
In this blog, we will explore a range of new concepts from Nadia Makarevich’s Advanced React Playlist. These topics might be familiar to you when you were or are learning of React, but we will dive deeper into this with help of Nadia Makarevich’s . Let’s begin our exploration.
Episode 1: Intro to Re-render
In React, when a component is mounted, it creates an instance, initializes states, runs hooks, and then appends the component to the DOM. When the component is unmounted, it removes the instance, states, and DOM elements.
Re-rendering occurs when any state declared inside a component is updated. During re-rendering, all child components declared or created inside the parent component are also re-rendered. You can imagine that re-rendering doesn't finish until all nodes in the tree data structure are covered. During this process, React reuses the existing instance and properties of the component and updates them with new data. It tries to reuse as much as possible, from instances to DOM elements.
Re-rendering is independent of whether a child component depends on the updated state of its parent. Creating custom hooks also does not prevent this behavior.
In the above component, whenever the button is clicked, state updates occur inside the <App/>
component, which triggers a re-rendering of the entire <App/>
component, including <VerySlowComponent/>
, <BunchOfStuff/>
, and <OtherStuffAlsoComplicated/>
.
To optimize and prevent unnecessary re-rendering, it's beneficial to move the state declaration closer to where it's actually used. This means declaring state where it’s directly needed, rather than higher up in the component tree.
In the above figure you can see how button and its state is separated out from <App/>
Component. Now whenever state updates happen it will re-render only the <ModalWithButton/>
Component not the <App/>
Component.
Episode 2: Element, Children And Re-render
A component in React is a function that returns an element. For example: const Component = () => { return <Element />; }
The HTML-like syntax <Element />
is shorthand for React.createElement(Element, null, null)
.
When you log a component in React, you’ll see an object with various properties. One important property is type.
When state updates occur in any component, React starts building a new object tree and then performs a shallow comparison between the new object tree and the previous one. Shallow comparison means comparison by reference, not by value. If the references are the same, there is no re-render. If they are different, then the type is checked. If the type are the same, the element is re-rendered; otherwise, the element is removed from the DOM.
When you pass a component as a prop or as children in React, it is created outside the parent component's scope. Therefore, the props or children would not re-render if element re-render occurs. This is because React compares object references, and if the reference doesn’t change, the component won’t re-render.
Episode 3: Component as prop
There are various ways to pass a component as a prop:
Component as an Element : When we don't want to modify any props of component or you can say when we want to use default props only pass that component as element straight away.
Ex :
<Button icon={<Icon/>} />
This example passes an instance of
<Icon/>
as a prop named icon to the Button component.Component as Component : When we want to modify the props or add some other props to the component from the parent component then we can use Component as Component or Component as a function.
Ex :
<Button icon={Icon} /> const Button = (props) => { const Icon = props.icon; return ( <button> <Icon fontSize="12" /> </button> ); };
This example passes the Icon component itself as a prop named icon to the Button component.
Component as a Function :
Ex :
<Button icon={(props) => <Icon isActive={props.isActive} />} /> const Button = (props) => { return ( <button> {props.icon({ isActive: true })} </button> ); };
This example passes a function as a prop named icon to the Button component, which returns an instance of Icon with additional props. Same thing can be done via children too.
<Button> {({ isActive }) => <Icon isActive={isActive} />} </Button>
In the above <Button/>
component you can see cloning of <Icon/>
is happening and size and color are added to icon by default inside button. You cannot override icon color and size manually for icon here.
The color red cannot be applied to icon because button is not reading the props of icon and is applying color on basis of appearance directly.
If we use <Icon/>
without passing it as prop inside <Button/>
it will be able to apply value passed via prop in that icon element.
This contradictory behavior of icon type element causes lack of clarity can make it harder to understand and fix issues in your code later on. Therefore, React docs also suggest to avoid cloning elements.
If the default props are hidden then it is problematic because the developer is unaware of what prop is being applied in default and what prop can he/she actually override.
Elements are basically an object until it is not rendered inside a component. It doesn't matter where it is declared in that component the object stays in memory until it is rendered.
Episode 4: Render props
A render prop is a prop on a component, whose value is a function that returns a element.
In the above example of button you can see how props are passed to <Icon/>
by button, how props are overridden as well as how props of <Icon/>
are assigned specific values.
A very useful application of render props pattern is sharing stateful logic. Hooks can also do that today completely but there are some situations where render prop pattern is useful.
Ex:
In second episode we saw if we use children re-rendering won’t happen but in this case it will happen because state is moving down from parent to children.
Episode 5: Memoization
In react when a component re-renders it call the component function again and it redeclare and reassign all the functions and variables which in turn changes the reference of the functions and variables. This is why useEffect
triggers on every re-render because reference to the variables change.
In order to prevent this behavior during re-render we use useCallback
and useMemo
. The useCallback
hook takes two arguments: the function you want to memoize and a dependency array. The function will only be recreated if one of the dependencies changes.
The useMemo
hook takes two arguments: a function that computes and return a value and a dependency array. The value will be recalculated only when one of the dependencies changes.
If the dependency array is empty means only once the function will be memoize in case of useCallback
and once the value will be computed in useMemo
.
There is a myth that useMemo
is better than useCallback
simply because useCallback
recreates the function on each render.
It's important to understand that everything within a React component is recreated on every render. This includes the function passed to useCallback
as well as the value computed by useMemo
. Consequently, a new reference is generated each time. However, during the initial render, the callback function from useCallback
and the computed value from useMemo
are stored in an external variable.
Upon subsequent re-renders, React first checks for changes in the dependencies. If a change is detected, new references are assigned to the external variable; otherwise, the existing references are retained.
Thus, while both useCallback
and useMemo
both recreate functions and values in rerender, the notion that one is better than the other is a myth.
When to actually memoize props ?
If parent component is re-render then child will be also re-render which will cause the useEffect
to trigger unnecessarily on every re-render so here memoization of props .
So how to memoize here ?
Well React has React.memo for memoizing a component. If you memoize the child component inside parent then rerender by parent component will trigger the rerender of child component only if its props has changed.
What if prop is an object ?
When you pass an object directly as a prop, a new object reference is created each time the parent component renders. This will cause the Child component to re-render every time, even if child is memoized.
Therefore we need to memoize the object too before passing it as prop.
If a prop such as an object, array, or function which are recreated on every render then memoizing the child component with React.memo
won't be effective because the reference for that prop will change with each render.
Some more examples of place where memoization is useless:
What happens when component has children ?
Children is also a prop. A div
is a element which is converted into object by React so every time you re-render its reference changes and since it is not memoized it will make child memoization useless.
Now, let’s evaluate another example. Will memoization work in this case ?
No, the memoization is broken in this case too. Memoizing component is not equivalent to memoizing object. <MemoChild/>
is children and in React tree it is an object and on every re-render of parent it is created again so its reference changes. We need to memoize <MemoChild/>
.
So when to memoize actually ?
People say when there is expensive calculations. However, the true nature of these calculations can only be assessed when measured on a device that accurately reflects your user base. It also depends on several factors occurring in that environment at that particular time. Additionally, we need to analyze how calculations performed in an ideal state, before implementing memoization as well as consider the overall context of the application itself.
Using useMemo
or useCallback
is primarily beneficial for optimizing re-renders. Each time you employ these hooks, React caches the value or function. However, the overall impact may not be known until you have numerous instances of useMemo
and useCallback
, which could inadvertently affect your application's performance.
Episode 6: Reconciliation
So what do you think what will happens when there is state change ? how the re-rendering happens ?
Typically people will say depending on what the state of isCompany is the respective input component will mount for first render and then depending on state change, re-rendering happens and the current input will be unmounted and new input will be mounted on .
But the fact is there is no unmounting and mounting thing happening while re-rendering.
Now you will ask why ?
In our second episode, we discussed the component object tree and its types. In our case, the reference to the component object tree has changed. However, when React manipulates the DOM, it doesn’t remove all elements. Instead, it checks the component tree from top to bottom in following manner:
Type Check: React first checks if the element types are the same.
Props Check: If the types match, it then compares their props and properties.
No Change: If both the type and props are the same, React does nothing.
Update: If the props or properties have changed, React updates them.
Different Type: If the types are different, React unmounts the old element and mounts the new one.
If React removed elements from the DOM for every small change, it would significantly slow down performance. The same logic applies to other child components present inside a component.
React maintains a Virtual DOM, which stores the entire tree of components and elements that need to be rendered in the UI. The syncing between the Virtual DOM and the real DOM is handled by libraries like ReactDOM. This process of generating a new Virtual DOM tree for every change, comparing it with the existing Virtual DOM, and updating the real DOM accordingly is called reconciliation.
For element straight away conversion into real DOM element happens and for component it calls the component function.
Components inside components :
What do you think what will happen when re-rendering above Component ?
The answer is on re-rendering the component is locally created again and because of which its reference changes and so on re-rendering even though type is same re-mounting happens. Yes you heard right remounting happens because of this existing Input from DOM tree is removed and new identical Input is added.
Avoid this anti-pattern of creating component inside component.
Next topic we will be focusing is Reconciliation and Arrays.
In React, when multiple elements or components are rendered as siblings under a single parent, they are indeed treated as an array of elements.
Example :
During the process of reconcillation for this array of element, the array in new virtual DOM and real DOM is compared at index level meaning element present at ith index of both array are compared against each other.
Now come to rendering a list of items as elements. We all know react pushes us to add unique key to each element in list when rendering.
Do you know why it does ?
It does this so that while re-rendering even if the order of element has changed in list the comparison between new virtual DOM and real DOM element should take place as per key not as per index. It also help us from not losing the state of that list item while re-rendering due to change of position.
There is a myth that keys improve the performance of lists by preventing re-renders. However, keys do not prevent re-rendering; instead, they help identify elements in an array during the reconciliation process. To prevent re-rendering, we need to memoize the list elements.
Keys also play a role in memoization. When you memoize a component, it will only re-render if its props change. Whenever operations occur on list of elements, such as adding, deleting, or reordering, the keys should move along with the position of the list item. If the position of a list item changes and its key also changes, memoization becomes ineffective. Therefore, choose keys wisely based on the properties of the list items.
Example of ineffective memoization:
Example of effective memoization:
Key is not limited to list you can use it with normal elements or components too.
In above example if key was not there then only props would have change on re-render and whatever you have written in input stay as it is. With key unmounting and input state reset happens when state of isCompany changes. But we know that unmounting and mounting might cause performance issue in larger components.
Have you ever thought why does React doesn’t throw key warning or error when you place array of elements individually vs dynamically via list ?
Dynamic list elements can change position but static list of elements can’t change its position.
What will happen if we mix dynamic list of items with static list of items ?
For the DOM tree dynamic list of items will be present in separate array as an individual element.
Episode 7: Higher Order Components
Higher Order Components are basically a function that accept one of the argument as component and perform some additional logic and return a modified Component with help of other arguments.
While hooks simplify many use cases, HOCs can still be useful for certain patterns, particularly when you need to share behavior across multiple components without modifying their internals.
Ex: Suppose you want to send some analytics/logging data to your server for every click. In that case you can create a HOC which will log every click on any component.
Now we can use it our Button and Link component.
This concludes my understanding and insights from Advanced React playlist by Nadia Makarevich. She has expertly covered a range of concepts, many of which were previously unfamiliar to me, such as the reasons behind re-rendering, the principles of memoization and its appropriate use cases, the significance of keys when rendering lists, the importance of avoiding cloning and the potential harm of hiding props for overall development, the behavior of children components when passed as props and used inside component, and many more.