Hi I am Peter a senior software engineer I write in order to better learn subjects and to share what I learned. When I first started, React felt like a black box and I was just using without really understanding what was happening under the hood. I tried to follow best practices, but wasn't really sure why most of the time. My aim for the post is to help others understand the why behind the best practices and the common pitfalls that developers face. Hopefully this helps to write better code with less bugs and more efficient applications.
React, at its core, is a library designed for building user interfaces. It does this through a system of components. These components are functions that return React elements, which are simply JavaScript objects that represent the UI. When a component’s state changes, React takes care of updating the UI in the most efficient way possible using a process called reconciliation.
useState
, useEffect
(setup phase), useContext
, useReducer
useEffect
(execution after DOM updates) Understanding these two phases is crucial because it affects how you write your code. Here are some common pitfalls related to the reconciliation process and how you can avoid them:
When rendering lists of elements, you should always use keys. Keys help React identify which items have changed, added, or removed, which is important for performance and accurate UI updates. In this example try changing the key type and then modify the list of components. When the key is set as an index or none, you will see that the list will not update correctly. This is because React uses the key to determine which items have changed, added, or removed. If the key is not unique, React will not be able to determine which item has changed. The rule of thumb is if the dataset is static, you can use the index as the key. If the dataset is dynamic, you should use a unique identifier as the key.
useEffect()
dependencies -In this example, we explore the nuances of the useEffect hook. The demo features two addends and their sums calculated in different ways:
Performance optimization is a crucial part of building a React application. In this section, we'll explore some common performance pitfalls and how to avoid them. I know I have built application and wondered why they are sometimes slow or why some of my components are re-renderng more than I expected. Hopefully this section will help you understand why and how to fix it.
Before diving into specific optimization techniques, it's important to revisit the concept of reconciliation in React. This process is at the heart of React's efficiency. Reconciliation is how React updates the DOM, comparing the previous and next states of the UI and updating only what’s necessary. However, this process can become a bottleneck if not managed correctly. Efficient rendering is key; every unnecessary render cycle you prevent can significantly improve the overall performance of your application.
memo
and useCallback
React.memo()
is a higher order component that will memoize the output of a component.
This means that React will skip rendering the component if the props have not changed.
This is useful for optimizing components that are expensive to render.
React.memo()
only checks for prop changes, so if you have a component that uses useState or useContext, you will need to use the useCallback hook to memoize the function.
This is because the function will be recreated on every render, which will cause React.memo()
to always re-render the component.
This example showcases how React.memo and useCallback can optimize your React components.
useCallback
. Notice that memoized handlers prevent unnecessary re-renders by maintaining a stable reference across renders, unlike inline functions that change on every render.React’s setState
function is asynchronous and batches updates for performance gains. However, managing complex state updates can be challenging. Using functional updates or the useReducer hook can help manage state more predictably and reduce unnecessary renders.
Functional updates ensure that state transitions are based on the previous state, avoiding issues with asynchronous updates. The useReducer
hook is particularly useful for complex state logic, offering a more structured and scalable approach to state management.
In this interactive example, we explore common pitfalls and best practices of state management. The code demonstrates these scenarios:
From my experience, I have found useReducer
to be particularly helpful when tackling complex state logic that involves multiple sub-values.
One nice feature about using useReducer
is its dispatch function.
This function is memoized, ensuring stability and preventing extra re-renders when passed down to child components - a real advantage for avoiding prop drilling in complex component trees.
This example features a dynamic CSS property editor that uses the useReducer hook to manage a potentially complex state.
Since we used useReducer
, adding new properties that we want to manage becomes straight-forward and intuitive.
Using immutable data structures is a key practice in React’s state management. React relies on immutability to determine if re-renders are necessary. When you treat state as immutable, you create a new object for every update, making it easier for React to compare previous and next states during the reconciliation process.
Thanks for reading and I hope you've gained valuable insights into not just the 'how', but more importantly, the 'why' behind various best practices and common pitfalls. I know it has been helpful for me to go through all this write it down. As a senior software engineer, I've always believed that understanding the underlying mechanics of the tools we use is crucial for crafting efficient, robust, and maintainable applications. I encourage you to experiment with the practices, challenge them, write your own, and see how they apply to your own projects. Happy coding!
"A great developer, you seek to become. Share knowledge and grow, you will." - Yoda