Week 5 - React State - CI256

Week 5 - React State

Refresher

Lets review the JSX we’ve learned so far!

Functional Components

React “Components” are re-usable pieces of code. They’re used just like a traditional HTML tag would be used.

// Defitinition of <HelloWorld />
function HelloWorld() {
  return <div> Hello, World! </div>
};

// Usage elsewhere in application
function App() {
  return <div>
	<HelloWorld></HelloWorld>
	<!-- some tags can be self-closing -->
    <HelloWorld />
  </div>
}

They must always return a single node. This example would be invalid:

function TodoList() {
  // INVALID!
  return (
	  <li>Item One</li>
	  <li>Item Two</li>
  )
};

We could fix it by returning the items inside an unordered list

function TodoList() {
  // this is fine because we return a single <ul> with mutiple <li> inside
  return (
	  <ul>
		  <li>Item One</li>
		  <li>Item Two</li>
	  </ul>
  )
};

Sometimes we need to return multiple items but we don’t want our single-node return value to affect the flow of the page. Ex, if we needed to do this:

<ul>
  <TodoList />
  <TodoList />
</ul>

For this, we can use a fragment - it looks like an empty HTML tag: <> ... </>. The fragment will “disappear” once rendered to the browser.

function TodoList() {
  // this is fine because we return a single fragement with mutiple <li> inside
  return (
	  <>
		  <li>Item One</li>
		  <li>Item Two</li>
	  </>
  )
};

Components can take arguments, which we call “props”. Props are always passed as an object as the first parameter to our component function.

function TodoList(props) {
  // props has a single key/value pair, key=items, value=string[]
  console.log(props)
  console.log(props.items)
  return (
	  <>
		  <!-- render props.items into html list items -->
		  {props.items.map(i => <li>{i}</li>)}
	  </>
  )
};

<TodoList items={['Item One', 'Item Two']}

Typically we destructure our props, rather than accepting the object “as a whole”

function TodoList({items}) {
  // items is a string[]
  console.log(items)
  return (
	  <>
		  <!-- render props.items into html list items -->
		  {items.map(i => <li>{i}</li>)}
	  </>
  )
};

<TodoList items={['Item One', 'Item Two']}

We use “curly-brace syntax” {} to switch from XML back into javascript. This is useful when we want to call another function, render a variable, or perform some higher order function to render multiple things with map. We can swap between XML and Javascript anytime we want by using either Angle Brackets <> or Curly Braces {}.

return ( // javascript
	  <> <!-- HTML -->
		  <!-- render props.items into html list items -->
		  { // javascript
		    items.map(i => 
		      <li> <!-- HTML -->
		        { // javascript
			        i // render string `i` to screen
		        }
		      </li>
		    )
		  }
	  </>
  )

Event Listeners

Events drive all reactivity in webpages under the hood. We can tap into events with event listeners.

In React, we can use event listeners by passing a prop representing the event, prefixed by on. We pass a callback function to this prop, which will be called whenever that event is triggered with a single argument, the click event.

The simplest example is onClick when a button is pressed:

Live Example

function Counter({ items }) {  
  function doStuffOnClick(e) {  
    console.log('button was clicked!', e)  
  } 

  return (
    <Button onClick={doStuffOnClick}>  
      Click me!
	</Button>
   );
}

We will be passed an Event object, which will vary based on the event type. The event will have a target attribute, which is the HTML element which triggered the event. This can be useful for extracting the value from the element.

Often, we don’t care about the event itself, but the value of the target triggering the event. For this reason, we will commonly look at event.target or event.target.value. Lets take a look at e.target.value of the onChange listener for an Input box.

Live Example

function OnChangeTest() {
  function onChange(e) {
    console.log('input changed, value is:', e.target.value)
  }

  return (
    <>
      <Input onChange={onChange} />
    </>
  )
}

⚠️ Warning: Don’t pass the result of a function!

A common mistake is calling the function, instead of passing the function. This will not work!

Live Example

function ClickMe() {
  function doStuff(e) {
    console.log('button was clicked!', e)
  }
  return <Box sx={{gap: 1, display: 'flex'}}>
    {/* Bad: this will immediately call doStuff(),
        setting onClick = undefined 
        (the return value from doStuff */}
    <Button color="danger" onClick={doStuff()}>
      Don't do Stuff
    </Button>

    {/* Good: Either of these are fine */}
    <Button onClick={doStuff}>Do Stuff Direct</Button>
    <Button onClick={() => doStuff()}>Do Stuff Anon</Button>
  </Box>
}

Using contextual variables

Most of the time, we don’t care about the event or the element that triggered it, just what context the event happened in. For example, what “item” was being handled when we rendered that button.

For this, we’ll often use an anonymous callback arrow function (what a mouthful!), and pass the variable into the handler function ourselves. This is where arrow functions are very powerful!

Live Example

function Cart(props) {
  let items = [
    {name: 'apple', price: 1.99},
    {name: 'banana', price: 0.99},
    {name: 'carrot', price: 0.49},
  ]
  function purchaseItem(item) {
	// Full item object is printed to the logs
    console.log('added to cart', item)
  }

  return (
    <>
      {items.map(item => <div key={item.name}>
          <Button 
            sx={{m: 1}} 
			// anonymous function can pass 
			// the entire item object to purchaseItem
			// notice we ignore the event object completely
            onClick={() => purchaseItem(item)}
          >
            {/* Here we want to render the txt and price,
				so we can't just just take the
				string value of the button */}
            Add {item.name} - ${item.price}
          </Button>
        </div>
      )}
    </>
  )
}

Here, we use an anonymous arrow function as the listener, then call addItem with the local variable item.

React Hooks

React hooks provide an interface for powerful React features like state management.

This is where React shines over vanilla HTML.

The useState hook

The use state hooks adds state management to React components.

Calling useState returns an array with two objects - the value which can be used in code, and a function to set the value. Setting the value directly will do nothing and will not update the rendered page.

Typically, we use array destructing to access the useState return values.

const [value, setValue] = useState();

// this doesn't work as you expect - setting this does nothing.
// This is why we use always use `const` with React Hooks
value = 5

// instead, we need to call setValue
setValue(5)

When used correctly, useState allows us to keep our entire UI in sync. Multiple components and parts of our UI can reference the same value, and React handles updating the value everywhere for us.

Live Example

function Counter() {
  let [count, setCount] = useState(0)

  return <Box sx={{gap: 1, display: 'flex'}}>
    <Button onClick={() => setCount(count += 1)}>
      Click to increase. Current count is {count}
    </Button>
    
    <Button onClick={() => setCount(count += 1)}>
      Click to increase. Current count is {count}
    </Button>
    
    <Button color="danger" onClick={() => count += 1}>
      Click to set value directly. Current count is {count}
    </Button>
   </Box>
}

render(<Counter />)

Initial Value

The first parameter passed to the useState function will be the initial value and type.

In the previous example, we could initialize useState with a value of 11 for example:

  let [count, setCount] = useState(11)

Setting the value with a callback

So far we’ve called setCount with a number, but we can also use a callback function.

Using a callback function provides us with the current value of the state as the first parameter.

const [count, setCount] = useState(0);

function increment() {
    setValue(currentCount => currentCount += 1)
}

See it in action:

Live Example

function Counter() {
  let [count, setCount] = useState(0)
  
  // on click function moved out for readability
  function onClick() {
    // using a callback instead of setCount(count + 1)
    // this has the side effect of rendering our red button useless!
    setCount(currentCount => currentCount + 1)
  }

  return <Box sx={{gap: 1, display: 'flex'}}>
   <Alert>The current count is {count}</Alert>
   <Button onClick={onClick}>
     Click to increase count. Current count is {count}
   </Button>
     
   <Button color="danger" onClick={() => count += 1}>
     Click to set value directly. Current count is {count}
   </Button>
 </Box>
}

render(<Counter />)

⚠️ Warning: Values may appear “locked” if not using an event listener

If you set an <input> value to a useState value, but you never set the state, an input box will be restricted to that single value

Live Example

function App() {
  const [price, setPrice] = useState(150);
  return (
    <div className='App'>
      <div>
        Price:
        <!-- This will never allow the value to change -->
        <input value={price} type="number" />

        <!-- Using an onChange listener to set the price allows the input box to change values -->
        <input value={price} onChange={(e) => setPrice(e.target.value)} type="number" />
      </div>
    </div>
  );
}

⚠️ Warning: Infinite loops are possible!

Calling the setter function in the wrong place, like directly in the render function, can cause an infinite loop.

Live Example

function App() {
  const [count, setCount] = useState(0);
  setCount(5)
  return (
    <h2>Count is {count}</h2>
  );
}

The useMemo hook

The useMemo hook is a function that runs before and after each render event. It’s useful for calculating a value based on other values in the component.

By default, it will run on every render, but you tell React you only want the hook to run when a list of dependencies change. These dependencies are other variables.

The useMemo hook cannot be used conditionally - ex, you can’t put it inside an if statement or loop. Use effect hooks must be defined at the top level of your render component.

Image from React.dev, read more.

Live Example

function TaxCalculator() {
  // setup states for price and tax rate, 
  // which can change indepdently
  const [price, setPrice] = useState(150);
  const [taxRate, setTaxRate] = useState(0.08);
  // Setup a useMemo. The resulting value is saved to total
  const total = useMemo(
    // A callback function to calculate the total
    () => price * taxRate,
    // this tells useMemo our dependencies are
    // price and taxRate. Any time either of these
    // variables change, update the total
    [price, taxRate]
  )

  return <Box sx={{gap: 1, display: 'flex'}}>
    <div>
      Price: 
      <Input 
        value={price} 
        onChange={(e) => setPrice(e.target.value)}
        type="number" 
      />
    </div>
    <div>
      Tax:
      <Input 
        value={taxRate} 
        onChange={(e) => setTaxRate(e.target.value)} 
        type="number"
       />
    </div>
    =
    <Alert>
      Total: {total.toFixed(2)}
    </Alert>
  </Box>
}

The useEffect hook

The use effect hook is very similar to useMemo, except that it does not return a value. It still takes an array of dependencies, runs before and after every render, and cannot be used conditionally.

useEffect is useful if you need to avoid the caching mechanism of useMemo for some reason, or if you’re just causing a side effect but not generating a value for use in react.

An example might be updating the page title when a value is calculated:

function TaxCalculator() {
  const [price, setPrice] = useState(150);
  const [taxRate, setTaxRate] = useState(0.08);
  const total = useMemo(() => price * taxRate, [price, taxRate])  
  useEffect(() => {
      document.title = `Bill $${total}`
    }, 
    [total]
  )
}

Passing Hooks as Props

Hooks can be passed into child components just like any other variable. You can pass the value, the setter function, or both.

function CounterButton({setCount, increment}) {
  increment = increment || 1  // default increment to 1
  return <Button onClick={() => setCount(count => count + increment))>
      Click to Increment by {increment}
    </Button>
}

function CounterDisplay({count}) {
  let color = count <= 10 ? "primary" : "danger"
  return <Alert color={color}>{count}</Alert>
}

function App() {
  let [count, setCount] = useState(0)

  return <Box sx={{gap: 1, display: 'flex'}}>
    <CounterButton setCount={setCount} />
    <CounterButton increment={5} setCount={setCount} />
    <CounterDisplay count={count} />
  </Box>
}

Formatting our files

Consistent and well formatted code is much easier to work on. As we add in these additional pieces, remember that React components generall follow the same format:

  1. Define any hooks at the top of your component
  2. Define any functions that your component will use