Here are the some of the concepts and practices that I learnt while making my projects.
Batching in React.
When I started using useState
, I had a misconception that the component is rendered as soon as the setState
statement is encountered. I realized this later that React does not render right after the setState
statement. Any state update operations are batched together and queued to be computed when the useState
is invoked during the next render. The component renders only when the event handler function has executed all the code that it had. Then, during the next render, those state updates that were batched are computed and the value is returned to the state variable. Thus, when the component renders and the hooks are invoked, they return the updated value to the state. Here is an example,
export default function App() {
const [a, setA] = useState(1);
const handleBtnClick = () => {
setA(a + 1);
setA(a + 1);
setA(a + 1);
setA(a + 1);
};
console.log("rendered", a);
return (
<div className="App">
<button onClick={handleBtnClick}>do it</button>
</div>
);
}
If you run this, you will see that the console.log
will run only once and give the value 2
. If you have used React.StrictMode
you might get the same output twice. Now, this example brings me to the next point which is the updater function.
In this example, since we are providing an operation inside setA
, the setA
is converted to setA(1+1)
which is converted to setA(2)
and then these updates are queued for the next render. Then, during the next render, useState
has four state updates and all of them are setA(2)
. Why 2 for all of them? That is because every render has it's own state value and this state value does not change for that particular render. You might have noticed that in the line where useState(1)
is invoked and we are de-structuring the array into the state value and state update function, we have used const
. This means that we cannot change the state value during the same render. The previous render had the value of 1
for a
. Therefore, all the setA
statements were read as setA(2)
. The final value that is returned to the component is 2
.
In case of a scenario where we want to serially update the state unlike the scenario mentioned above where the state was only replaced by the same constant value of 2
on every update, we would use an updater function.
An updater function is a callback function that is supplied to the setA
. It's argument is the latest state value prior to this calculation. Here is an example,
const handleBtnClick = () => {
setA((a) => a + 1); // this returns 2
setA((a) => a + 1); // this returns 3
setA((a) => a + 1); // this returns 4
setA((a) => a + 1); // this returns 5
};
By giving a callback function, we are telling React to calculate the state value during the next render.
Reconciliation
React uses this algorithm to make sure the DOM rendering is as efficient as possible. React has the diffing algorithm through which it narrows down which elements are different so that only those elements are updated in the browser DOM. This process starts with ReactDOM.render()
method. This render
method creates a virtual DOM. During diffing, the newly created virtual DOM is compared with the virtual DOM before the state update. But first, a bit about virtual DOM.
Virtual DOM is a JSON object that represents the browser DOM. It is extremely fast compared to the browser DOM. It is created from scratch on every state update.
How does React compare the corresponding elements from two different virtual DOMs? For that, let me show you this example,
console.log(
createElement(
<p className="App">some random text</p>
));
This code gives the following output,
React sees every node like this and then compares their respective properties. For any element to be called different, any of these properties have to differ from the properties of the same element of the previous virtual DOM.
All the child nodes are mentioned in children
object. React gives warning for having unique keys for child nodes. Whenever React sees a different element, it not only re-creates that element but also all its children. So, having a unique key helps React in determining whether a particular child node is new or updated or removed from the list of children.
In a case without keys, adding a child on top of the list would mean that the whole list is destroyed and re-created. But having a key would tell the React to add the new element to the list instead of destroying the whole list.
One more thing. Think of the whole component tree in terms of a React Elements. The root component would have children
where the child components would be listed and some of these would have children
too. Continuing like this you can imagine a tree is forming that starts at the root and the leaf nodes are the HTML elements. This is the component tree that React is traversing during the diffing to spot the differences. To traverse this tree, React uses breadth first approach. To make a case for depth first traversal, let's say during diffing, React sees that a leaf node is different. So it destroys this node and creates it again. Now, it goes to the parent node and sees that this node is also different. It destroys this parent node and it's subtree and re-creates the whole subtree again. The same could have been done in a single operation had there been a breadth first traversal. React would first check the parent node instead of going directly to the child node.
Once the process of diffing is complete, React prepares a list of minimum updates required to be done on the browser DOM.
Composition
React uses the idea of function composition from JS. Components can be composed in a similar manner. Higher order component is one such function that takes the child component as an argument and returns this component wrapped in the parent component. What component is passed as an argument will change depending on the use case. Here is an example,
const FeedPageWrapper = PostsSection => {
const FeedPage = () => {
return (
<div
className={`bg-off-secondary ${styles.feedPageGrid}`}>
<div className={`${styles.navDiv} flx flx-maj-end`}>
<NavBar />
</div>
<PostsSection /> {/*child component used here*/}
<div className={styles.extraDiv}>
{/* third div empty for now.*/}
</div>
</div>
)
}
return FeedPage
}
export default FeedPageWrapper
In the above example, I have a higher order component that takes a component called PostsSection
. This component that is passed as an argument will differ based on the page that the user is on. If the user is on bookmarks page, PostsSection
will have a list of bookmarked posts. If the user is on user-feed page, PostsSection
will have a list of posts personalized for the user.
Apart from the PostsSection
, everything else on the page will be the same. Hence, I decided to use the higher order component here. Here is how this component will be used,
const BookmarksFeedPage = FeedPageWrapper(BookmarksSection)
export default BookmarksFeedPage
I have passed BookmarksSection
as the argument and BookmarksFeedPage
is returned that is exported in the next line. Similarly for the user-feed page,
const UserFeedPage = FeedPageWrapper(UserFeedSection)
export default UserFeedPage
Private routes using react router
By private routes I mean the routes that are personalized for the users and should only be shown if a user is logged in. For example, in an e-commerce app, wish-list and cart pages are the private routes because they will have different data for different users, unlike the products page.
Here is the algorithm,
- Check if the current route is private or not.
- If it is, then check whether the user is logged in or not.
- If user is logged in, let the user continue with this route.
- If user is not logged in, re-direct the user to the login page.
- If the current route is not private, then let the user continue with this route.
<Route path={ROUTE_CART} element={
<RequireAuth>
<Cart />
</RequireAuth>
} />
In the above code, I have wrapped <Cart/>
inside <RequireAuth/>
which checks whether the user is logged in or not.
const RequireAuth = ({ children }) => {
const location = useLocation()
return isUserLoggedIn ? children : <Navigate to='/login' state={{ from: location }} replace />
}
The above code shows that user's log in status is maintained in the state isUserLoggedIn
. <Navigate/>
is a component in react-router@6 which takes a parameter to
to navigate to a particular location.
The user's current location is also saved inside the state of Navigate
so that after log in, user can be redirected to this location. Here is the code for that,
const from = location.state?.from?.pathname
navigate(from, { replace: true })
Setting replace to true means that the login page would be removed from the history stack of the browser. This is helpful when the user presses the back button, the app skips the login page and goes to page that was opened previous to the login page.
CSS Modules
Initially I had used normal CSS stylesheets in my projects. This was raising specificity conflicts because every stylesheet had a global scope. CSS modules resolved this issue because it limits the scope of stylesheet to the file that it is imported in.
useRef
I have used useRef in one of my apps to make DOM manipulations. The requirement was that whenever a user clicks on any option from the given options, the app should change the background color of that option to red or green depending upon whether the answer was correct or wrong. Here is the first part of logic,
optionBtnRefs.current = currentQues?.options?.map((option, i) => optionBtnRefs[i] ?? createRef())
This code is creating an array of refs for each option of the question. Then,
<button key={index} ref={optionBtnRefs.current[index]} onClick={() => handleOptionSelect(optionBtnRefs.current[index])} value={option}>{option}</button>
When mapping over the options, I have assigned a ref to each option and the same ref is passed to onClick
event handler. Then,
if (ref.current.value === currentQues.answer) {
ref.current.style.background = 'var(--clr-success)'
setTimeout(() => { ref.current.style.background = 'var(--clr-primary)'; setScore(s => s + 1) }, 1000)
} else {
ref.current.style.background = 'var(--clr-error)'
setTimeout(() => ref.current.style.background = 'var(--clr-primary)', 1000)
}
Depending upon whether the chosen option is the correct answer or not, I have updated the background of that particular ref
. The background is restored to normal after 1 second.
These were some of the things I wanted to share. Hope it helps. Thanks for reading.
Top comments (2)
Good look on your learning journey.
thanks Andrew.