Understanding the component tree and re-renders in React.
Published: 2022-08-16 15:30:00


When working in React, it is crucial to understand how your component tree is organized. Usually you would have the top most level component and child components of said top most level component. Many cases the child components would also have their own set of child components as well. Understanding how your component tree is organized is crucial so that you can send the right information to the right components.

Let’s take, for example, a component hierarchy such as this,
App
|_Header
|_Main Container
  |_SearchBar
  |_StockContainer
  |  |_Stock
  |_PortfolioContainer
     |_Stock

If, for example, I wanted to send an array of stocks from my App component to my StockContainer component, I could pass that data as props to my MainContainer and again down to the StockContainer. StockContainer could then render those individual stocks by using the Stock component because it now has the relevant data due to my knowledge of the component hierarchy.  

Another thing to consider regarding the component tree is the relationship between components. If a component returns another component. The component that’s returning is considered a parent and the returned component is considered a child of that component. As seen prior, if we want to send data down to a child component, we use props. If we want to send information up the component tree however, we would need to pass a callback function from the parent component to the child component and invoke it in the child component. 

function App() {
 const [stocks, setStocks] = useState([]);
 
 useEffect(() => {
   fetch(" http://localhost:3001/stocks")
   .then((resp) => resp.json())
   .then((stocks) => setStocks(stocks))
 }, [])
 
 function sortStockByTicker() {
   const sortedStocks = [...stocks].sort((a, b) => {
     if(a.ticker < b.ticker) { return -1; }
     if(a.ticker > b.ticker) { return 1; }
     return 0;
   })
   setStocks(sortedStocks);
 }
 
 function sortByPrice() {
   const sortedStocks = [...stocks].sort((a, b) => {
     if(a.price < b.price) { return -1; }
     if(a.price > b.price) { return 1; }
     return 0;
   })
   setStocks(sortedStocks);
 }
 
 
 return (
   <div>
     <Header />
     <MainContainer stocks={stocks} sortByTicker={sortStockByTicker} sortByPrice={sortByPrice}  />
   </div>
 );
}

function SearchBar({sortByTicker, sortByPrice, onCategoryChange}) {
  return (
   <div>
     <strong>Sort by:</strong>
     <label>
       <input
         type="radio"
         value="Alphabetically"
         name="sort"
         checked={null}
         onChange={null}
         onClick={sortByTicker}
       />
       Alphabetically
     </label>
     <label>
       <input
         type="radio"
         value="Price"
         name="sort"
         checked={null}
         onChange={null}
         onClick={sortByPrice}
       />
       Price

Passing the callback functions, defined in the App component, down the component tree to the SearchBar component, and binding it to an event to be invoked as a result of a specific user interaction, lets the App component have access to the information the child component has. 


In React, re-renders are triggered due to a change in state or new props being passed to another component. 

function App() {
 const [stocks, setStocks] = useState([]);
 
 useEffect(() => {
   fetch(" http://localhost:3001/stocks")
   .then((resp) => resp.json())
   .then((data) => setStocks(data))
 }, [])

As you can see we have an initial state of an empty array, and after fetching data from the url we set our state to the data we got back from the resolved promise. Our stocks variable can now be passed down as a prop to any component that needs it. 

Let’s say we have rendered all the data from our stocks variable on the page. Each individual stock has an onclick event. This is where knowledge of how re-renders work really helps. We want two things to happen each depending on where in the page the event took place. If the stock was clicked in the StockContainer we want that stock to appear in the PortfolioContainer. If the stock was clicked in the PortfolioContainer we want that stock to be deleted. 

We know we need a separate state for the portfolio because it's going to change. We’re also going to need callback functions in order to communicate to the parent component that there has been a change in the PortfolioContainer and or the StockContainer. 

 function MainContainer({stocks, sortByTicker, sortByPrice}) {
 const [portfolio, setPortfolio] = useState([]);
 
 function handleAddStock(stock) {
   const clickedStock = stocks.find((s) => s === stock)
   if (portfolio.includes(clickedStock)) {
 
   } else {
     setPortfolio([...portfolio, clickedStock]);
   }
 }
 
 function handleRemoveStock(stock) {
   const newPortfolio = portfolio.filter((p) => p !== stock)
   setPortfolio(newPortfolio);
 }
 
 function onCategoryChange(e) {
   setCategory(e.target.value);
 }
 
 
 return (
   <div>
     <SearchBar sortByTicker={sortByTicker} sortByPrice={sortByPrice} onCategoryChange={onCategoryChange}/>
     <div className="row">
       <div className="col-8">
         <StockContainer stocks={stocks} onAddStock={handleAddStock} category={category}/>
       </div>
       <div className="col-4">
         <PortfolioContainer portfolio={portfolio} onRemoveStock={handleRemoveStock} />
       </div>
     </div>
   </div>
 );
}

MainContainer is the parent to both StockContainer and PortfolioContainer so we set our portfolio state there. We also define our callback functions and pass them to their respective components as props. 

function PortfolioContainer({portfolio, onRemoveStock}) {
  return (
   <div>
     <h2>My Portfolio</h2>
     {portfolio.map((stock) => {
       return (
         <Stock key={stock.id} stock={stock} onStockClick={onRemoveStock} />
       )
     })}
   </div>
 );
}
function StockContainer({stocks, onAddStock, category}) {
 
 const stocksToDisplay = stocks.filter((stock) => {
   if (category === "") {
     return stocks;
   } else {
     return stock.type === category;
   }
 })
 
 return (
   <div>
     <h2>Stocks</h2>
     {stocksToDisplay.map((stock) => {
       return (
       <Stock key={stock.id} stock={stock} onStockClick={onAddStock} />
     )})}
   </div>
 );
}

Something neat about a re-render is that a generically named prop like onStockClick can have different values depending on which route down the component tree the information traveled. So for instance if the re-render happened as a result of a change in the StockContainer, onStockClick will have a value of onAddStock. If the re-render happened as a result of a change in the PortfolioContainer onStockClick will have a value of onRemoveStock. 

This was the biggest takeaway for me on how a re-render truly works. The same prop can have a different value depending on where functions or variables are defined in the component tree.