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:
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.
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!
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!
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.
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:
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
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.
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.
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:
- Define any hooks at the top of your component
- Define any functions that your component will use