State Management in React

devfish·2023년 2월 23일
0

React

목록 보기
5/10

How to Think in React

Key points from Thinking in React

Build Process

  1. Start with the mockup
  2. Break UI into component hierarchy
  3. Build static version in React
  4. Find minimal but complete representaiton of UI state
  5. Identify where your state should live
  6. Add inverse data flow
  • Single Responsibility Principle: A component should ideally only do one thing.

  • DRY (Don't Repeat Yourself): Think of state as the minimal set of changing data that your app needs to remember, and compute everything else on-demand.

    • Determining what is state: Cross out what is not state: A. stuff that remain unchanged over time, B. stuff passed in from a parent via props, C. stuff that can be computed based on existing states or props in the component
    • Single source of truth for each state: For each unique piece of state, you need to choose the component that owns it. Instead of duplicating shared state between components, you will either lift it up to a common parent or pass it down to the children that need it.
  • Determining where to store the state: You can identify every component that needs that state for rendering, and find the closest common parent component. You can store the state there, or in a component above the parent, or create a new component just for holding the state and add it above the parent component.

  • Adding interactivity: To change the state according to user interaction / input, you need to support data flowing the other way: components deep in the hierarchy updating states higher up the hierarchy. You need to link the input changes to state changes by passing down the setState functions.

  • Limiting Side Effects: Try to limit the use of side effects as much as possible in building components. The component should be able to work even with fake data as input (there are situations in which side effects are inevitable, such as if the data is managed on the server and needs fetching to load (state that tracks whether data is being loaded is dependent on a side effect!)

Props Drilling

... a situation where data is passed from one component through multiple interdependent components until you get to the component where the data is needed. (freeCodeCamp)

Problems with props drilling

  • Code becomes harder to read and maintain
  • Re-renders all components involved (in passing down the prop) each time there is a state change, which adversely affects performance

Props drilling examples (view Console to see how all the components get re-rendered!): Example1, Example2

Props drilling can be avoided by using state management tools listed below, and also storing the state as close to an interdependent component as possible.

Performance issues with props drilling might seem small at first, but as the app grows bigger the following issues might arise that not only hinder performance but also prove time-consuming:

  • Renaming props would be super tedious and painful the bigger and more complex the app is, since we'd have to track down every component involved in passing the prop,
  • Refactoring components or making any structural changes will make the forwarding of props harder to trace, the code harder to read. This is in addition to the chronic re-rendering of components that do not use the props themselves but only pass it down.

State Management Tools

State management tools (React Context, Redux, MobX) allow state to be stored globally and easily updated/invoked from anywhere in the app.

Don't Redux-ify everything if you don't need to!

According to Redux developer Dan Abramov, you should assess if you really cannot do without Redux before rushing to use it, and that it's more important to learn to "think in React." Read this article to see the tradeoffs involved in 'doing things the Redux way' - it'll help you figure out if those tradeoffs are worth it.

Examples of Redux being used to avoid props drilling: Example1, Example2

Practice: Basic shopping mall

The App component has two pages: 1. Page to browse items, 2. Shopping cart.

The first page allows you to click a button (in the item component) to add the item to the cart (multiple clicks increases its quantity). The second page allows you to increase/decrease the quantity of items already in the cart, or delete items. The OrderSummary component computes the total price of the cart based on the price and quantity of each cart item.

Two state variables are managed in the App component - items, and cartItems.

//item in items array
{
  "id": 1,
  "name": "노른자 분리기",
  "img": "../images/egg.png",
  "price": 9900
}
//cartItem in cartItems array
{
  "itemId": 1,
  "quantity": 1
}

The Nav component only updates the total number of cartItems, so we only need to pass down the cartItems.length as a prop. Items (data) need to be passed down to both the ItemListContainer and ShoppingCart component, since the former needs to display the items based on the data, and the shopping cart needs the data to compute the total price. Both cartItems and setCartItems need to be passed down to the two components as well, since both pages enable users to change the number and type of items in the cart.

All event handlers (addItem, increaseQuantity, deleteItem etc.) that change state variables when clicks are detected on input components (the bottom of the hierarchy), are defined on the ItemListCOntainer and ShoppingCart level, then passed down as props to the input components. The checkedItems state is managed in the ShoppingCart component.

Removing inefficiencies with find()?

The ItemListContainer Component defines a clickHandler that checks the id of the item button that was clicked, then checks if that item already exists in the cartItems array to either increase its stored quantity, or add the item to the array.

I did think of initially using find() to determine whether the item clicked was already in the cart, and if so to change its quantity and somehow use destructuring to overwrite the property of that item alone in the array.. then I got stuck working that out. The itemId is not a unique property, so using the spread operator to expand the existing cartItems array then adding a new item after it obviously did not overwrite anything - it just created a new cartItem with the same itemId. (What was I thinking?) I also tried just assigning a variable to store whatever the find function spits out, and using that to determine whether to add a new item or not, but that also felt inefficient as well.

My initial code that worked (which I already knew going in was clumsy but the point was to get it working) had a boolean for checking whether the item already was in the cart or not, and used the map function to change just the quantity of the item that matched the clicked id and have the array saved in a new variable. Then depending on that boolean value, it either returned the mutated array or added a new item to the back of the existing cartItems array.

//1ST VERSION
const handleClick = (clickedIdx) => {
    let existingItem = false;
    const arr = cartItems.map(item => { //INEFFICIENT
      if(item.itemId === clickedIdx) {item.quantity += 1; existingItem = true;}
      return item;
    })
    if (existingItem){
      setCartItems(arr);
    }else{
      console.log('item does not exist');
      setCartItems([...cartItems, {itemId: clickedIdx, quantity: 1}]);
    }
}

Basically this code runs a map function to iterate through all its elements even when the clicked item's quantity might have already been changed. And when the clicked item is not in the cart, it wastes a variable to store the same data as cartItems.

//2ND VERSION
const handleClick = (clickedIdx) => {
    const arr = cartItems.filter((item) => item.itemId === clickedIdx);
    if (!arr.length){
      setCartItems([...cartItems, {itemId: clickedIdx, quantity: 1}]);
    }else{
      setCartItems(cartItems.map(item => {
        if(item.itemId === clickedIdx) item.quantity += 1;
        return item;
      }));
    }
}

Actually this second version might be even more inefficient, since it iterates through all the elements with filter, then iterates again with map to change a single element property if the clicked item is already in the cart.

//3RD VERSION 
const handleClick = (clickedIdx) => {
    if (cartItems.find(el => el.itemId === itemId) === undefined){
          setCartItems([...cartItems, {itemId: clickedIdx, quantity: 1}]);
    }else{
     setCartItems(cartItems.map(item => {
        if(item.itemId === clickedIdx) item.quantity += 1;
        return item;
      }));
    }
}

I thought this last version was slightly more efficient and elegant in that the find function stops evaluating each element the moment it finds a matching id, then .. wait a minute. then it also runs the map function again to identify that element all over again and change its quantity.. so really is it more efficient than my initial code? Food for thought.

References

Thinking in React

Props Drilling In React.Js
Guidance to a better React props planning

Managing Global State With Redux
You Might Not Need Redux (2016)

Presentational and Container Components: Apparently outdated, so take it with a grain of salt

profile
la, di, lah

0개의 댓글