Missing Document Title
theme: Next,1
React Hooks: useEffect
useState covers the basics of adding a piece of state and providing a means of updating it.
We need a way to listen to state value changes and invoke code as a side-effect...
Enter useEffect

useEffect for side effects
We could do this if we had a method that behaved like this:
Setup a callback function that will run whenever a list of variables changes value.
Returning to our Counter
import React, { useState, useEffect } from 'react'function Counter() {const [count, setCount] = useState(0)function handleClickButton() {setCount(count + 1)}return (<div><p>Count: {count} <button onClick={handleClickButton}>Click Me</button></p></div>)}
Let us set up some code that runs whenever the count changes!
function theCountChanged() {console.log(`Wow, the count changed and is now ${count}`)}
- But we want this called when the count changes!
Enter useEffect!
- Accepts TWO arguments.
- The first is the function to call
- The second is an array of values to watch for changes
function theCountChanged() {console.log(`Wow, the count changed and is now ${count}`)}const listOfDataToWatchForChanges = [count]useEffect(theCountChanged, listOfDataToWatchForChanges)
Simplify
- Using our rule:
Whenever we define a variable and use it once, we can replace the variable usage with the value
useEffect(function theCountChanged() {console.log(`Wow, the count changed and is now ${count}`)},[counter])
Simplify
- Since
theCountChangedis now an anonymous inline function, we can remove the name.
useEffect(function () {console.log(`Wow, the count changed and is now ${count}`)},[counter])
Notice the effect function runs the first time too!
- This is because
useEffectwill always run at least once! - The first time, it acts like a function that runs when a component mounts.
- Future calls detect when state or props change. NOTE, the "watch" array could also contain props!
[.autoscale: true]
[fit] Use the useEffect to run code once at mount
If we need to do something just once when the component first mounts we can provide an empty change list and useEffect will only run once.
useEffect(function () {console.log(`This runs once when the component first mounts`)}, [])
Since there is no watch array, there is nothing for useEffect to ever see.
NOTE: We can have multiple
useEffect!
Using useEffect and useState
Build an app to use the OneList API
One List Refresher
[.autoscale: true]
Base URL: https://one-list-api.herokuapp.com
| Action | URL | Body |
|---|---|---|
| Get all items | /items?access_token=illustriousvoyage | None |
| Create item | /items?access_token=illustriousvoyage | { "item": { "text": "Learn about Regular Expressions" } } |
| Get one item | /items/42?access_token=illustriousvoyage | None |
| Update an item | /items/42?access_token=illustriousvoyage | { "item": { "text": "Learn about useEffect" } }` |
| Delete an item | /items/42?access_token=illustriousvoyage | None |
As always, we start with static HTML and CSS
import React from 'react'import logo from './images/sdg-logo.png'export function App() {return (<div className="app"><header><h1>One List</h1></header><main><ul><li>Do a thing</li><li>Do something else</li><li>Do a third thing</li><li>Remind me about the important thing</li><li>The important things are the important things</li></ul><form><input type="text" placeholder="Whats up?" /></form></main><footer><p><img src={logo} height="42" alt="logo" /></p><p>© 2020 Suncoast Developers Guild</p></footer></div>)}
@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&family=Neucha&display=swap');:root {font: 24px / 1 sans-serif;}html {height: 100%;}body {margin: 0;padding: 0;color: #fff;font-family: 'Comfortaa';background-color: #309869;}.app {align-items: center;display: flex;flex-direction: column;min-height: 100vh;}h1 {font-weight: 300;text-transform: uppercase;}main {flex: 1;}input {border: none;padding: 0.3em 0.2em 0;width: 100%;color: #074863;font-size: 1em;line-height: 1.5em;background-color: #fff;opacity: 0.5;}input:focus {outline: 0;background-color: #fff;opacity: 1;}a:link,a:visited {line-height: 1rem;text-decoration: none;color: #fff;font-size: 0.6rem;}ul {padding: 0;list-style: none;cursor: pointer;li {display: flex;justify-content: space-between;margin: 0.7em;&.completed {text-decoration: line-through;opacity: 0.5;}}}ul,input {font-family: 'Neucha', cursive;}ul li,form {align-items: flex-start;display: flex;}header,main,footer {max-width: 30em;}footer {margin-bottom: 0.5em;font-size: 0.8em;text-align: center;img {vertical-align: middle;// animation: spin infinite 10s linear;}}@keyframes spin {from {transform: rotate(0deg);}to {transform: rotate(360deg);}}
Step 2: Convert static JSX to JSX derived from state
[.column]
const [todoItems, setTodoItems] = useState([{ id: 1, text: 'Do a thing', complete: false },{ id: 2, text: 'Do something else', complete: false },{ id: 3, text: 'Do a third thing', complete: false },{ id: 4, text: 'Remind me about the important thing', complete: false },{id: 5,text: 'The important things are the important things',complete: false,},])
[.column]
<ul>{todoItems.map(function (todoItem) {return <li key={todoItem.id}>{todoItem.text}</li>})}</ul>
Step 3 - Try changing some data
Change text, add items, remove items, change completed state
Notice we need a little logic on our
liso we can change the class based on complete
<ul>{todoItems.map(function (todoItem) {return (<li key={todoItem.id} className={todoItem.complete ? 'completed' : ''}>{todoItem.text}</li>)})}</ul>
Step 4 - Actions
- Hook this up to the API and load data when the component first mounts
- Time for
useEffect
useEffect(function () {console.log('this runs when the component first mounts')}, [])
Update to load from the API
- This time, we will use
axiosto see how it improves onfetch
NOTE: We need to add the library!
Adding a library
- From the terminal and the same place we run
npm start, we can add a library to our project with:
npm install axios
This is very similar to our C# equivalent of
dotnet package add
Update our useEffect
useEffect(async function () {const response = await axios.get('https://one-list-api.herokuapp.com/items?access_token=cohort42')if (response.status === 200) {console.log(response.data)}}, [])
NICE!
Thanks axios
- No
headers - No
await response.json - Axios knows we are getting back JSON data and provides
response.data
TypeScript issues
We cannot use an async function directly in useEffect
The solution is to define the async function INSIDE and then call it.
useEffect(function () {async function loadItems() {const response = await axios.get('https://one-list-api.herokuapp.com/items?access_token=cohort42')if (response.status === 200) {console.log(response.data)}}loadItems()}, [])
Step 5 - Update state
- Make our default state an empty array again
- Inside the
useEffect, callsetTodoItems
const [todoItems, setTodoItems] = useState([])useEffect(async function () {const response = await axios.get('https://one-list-api.herokuapp.com/items?access_token=cohort42')if (response.status === 200) {console.log(response.data)setTodoItems(response.data)}}, [])
TypeScript ... again
Now our todoItems is of type never[] because our default state is [].
We need to teach TypeScript the shape of our API.
type TodoItemType = {id: numbertext: stringcomplete: booleanupdated_at: Datecreated_at: Date}
const [todoItems, setTodoItems] = useState<TodoItemType[]>([])
Beyond useEffect - more practice with React
Make the input field function correctly!
- Let the user type and track their item in state
- Add a state:
newTodoText - Add a
valueandonChangeto the input
const [newTodoText, setNewTodoText] = useState('')
<inputtype="text"placeholder="Whats up?"value={newTodoText}onChange={function (event) {setNewTodoText(event.target.value)}}/>
When the user presses enter!
- Since this
inputis inside aform, we will get anonSubmitfor the form!
function handleCreateNewTodoItem() {console.log(`Time to create a todo: ${newTodoText}`)}
<form onSubmit={function(event) {// Don't do the normal form submit (which would cause the page to refresh)// since we are going to do our own thingevent.preventDefault()handleCreateNewTodoItem()}}>
Update handleCreateNewTodoItem to submit
async function handleCreateNewTodoItem() {const response = await axios.post('https://one-list-api.herokuapp.com/items?access_token=cohort42',{ item: { text: newTodoText } })if (response.status === 201) {console.log(response.data)}}
We get back an updated item, but how do we update the state?
- We have two choices:
- Append the item
- Reload the entire list and replace it!
We can look at both options
Append the item
Once we have the item, we can build a new list of todos with that one appended.
const newTodo = response.data// Create a new array by *spreading* the old list and putting our new item at the end. Use [newTodo, ...todoItems] to *prepend* the new itemconst newTodoItems = [...todoItems, newTodo]setTodoItems(newTodoItems)
Replace the list
Replacing the list can be straightforward when we are not sure how to manipulate the state.
const refreshTodoResponse = await axios.get('https://one-list-api.herokuapp.com/items?access_token=cohort42')setTodoItems(refreshTodoResponse.data)
One bug: Our new todo item input should clear
Add this to the end of handleCreateNewTodoItem
setNewTodoText('')
Click to mark an item as completed
Now we are giving the items some behavior, so it might be time to refactor those into a component!
We can reuse our TodoItemType for our
props.
type TodoItemProps = {todoItem: TodoItemType,}export function TodoItem(props: TodoItemProps) {return (<li className={props.todoItem.complete ? 'completed' : ''}>{props.todoItem.text}</li>)}
<ul>{todoItems.map(function (todoItem) {return <TodoItem key={todoItem.id} todoItem={todoItem} />})}</ul>
[fit] We can now add a click handler on the li inside the TodoItem component
export function TodoItem(props: TodoItemProps) {function toggleCompleteStatus() {console.log('Clicked!')}return (<liclassName={props.todoItem.complete ? 'completed' : ''}onClick={toggleCompleteStatus}>{props.todoItem.text}</li>)}
What logic do we need in that code?
[.column]
We want to send a PUT request to the API that looks like this if the item is NOT complete:
{"item": {"complete": true}}
[.column]
We want to send a PUT request to the API that looks like this if the item is complete:
{"item": {"complete": false}}
Try an if/else
async function toggleCompleteStatus() {if (props.todoItem.complete) {const response = await axios.put(`https://one-list-api.herokuapp.com/items/${props.todoItem.id}?access_token=cohort42`,{ item: { complete: false } })if (response.status === 200) {console.log(response.data)}} else {const response = await axios.put(`https://one-list-api.herokuapp.com/items/${props.todoItem.id}?access_token=cohort42`,{ item: { complete: true } })if (response.status === 200) {console.log(response.data)}}}
Look at the similarity in the branches!
The only difference is that if the props.complete is true, we send false -- and if it is false, we send true.
Simplify:
async function toggleCompleteStatus() {const response = await axios.put(`https://one-list-api.herokuapp.com/items/${props.todoItem.id}?access_token=cohort42`,{ item: { complete: !props.todoItem.complete } })if (response.status === 200) {console.log(response.data)}}
Except we do not have a way to reload the list
STATE: DOWN, EVENTS: UP
[.autoscale: true]
Refactor the App a little
Extract the code used in the useEffect to make a function we can share.
function loadAllItems() {async function loadItems() {const response = await axios.get('https://one-list-api.herokuapp.com/items?access_token=cohort42')if (response.status === 200) {setTodoItems(response.data)}}loadItems()}useEffect(loadAllItems, [])
Now we have a function we can share with the TodoItem via props
[fit] Add it to the props supplied to the TodoItem
<TodoItem key={todoItem.id} todoItem={todoItem} reloadItems={loadAllItems} />
[fit] And add reloadItems to TodoItemProps
This type definition says "reloadItems is a function that takes no arguments and returns nothing."
type TodoItemProps = {todoItem: TodoItemTypereloadItems: () => void}
Use it after we finish with the API
async function toggleCompleteStatus() {await axios.put(`https://one-list-api.herokuapp.com/items/${props.todoItem.id}?access_token=cohort42`,{ item: { complete: !props.todoItem.complete } })props.reloadItems()}
We can use this to cleanup handleCreateNewTodoItem
async function handleCreateNewTodoItem() {await axios.post('https://one-list-api.herokuapp.com/items?access_token=cohort42',{ item: { text: newTodoText } })loadAllItems()setNewTodoText('')}
props, props, props, props
We keep seeing props. in our TodoItem component
Refactor using destructuring!
export function TodoItem(props: TodoItemProps) {const { todoItem, reloadItems } = propsasync function toggleCompleteStatus() {await axios.put(`https://one-list-api.herokuapp.com/items/${todoItem.id}?access_token=cohort42`,{ item: { complete: !todoItem.complete } })reloadItems()}return (<liclassName={todoItem.complete ? 'completed' : ''}onClick={toggleCompleteStatus}>{todoItem.text}</li>)}
We can improve this code
We can also destructure right inside the function definition.
So:
export function TodoItem(props) {const { id, text, complete, reloadItems } = props
can become:
export function TodoItem({ todoItem, reloadItems }: TodoItemProps) {
Furthermore, this looks like our function takes arguments -- but it is really destructuring props!
export function TodoItem({ todoItem, reloadItems }: TodoItemProps) {async function toggleCompleteStatus() {await axios.put(`https://one-list-api.herokuapp.com/items/${todoItem.id}?access_token=cohort42`,{ item: { complete: !todoItem.complete } })reloadItems()}return (<liclassName={todoItem.complete ? 'completed' : ''}onClick={toggleCompleteStatus}>{todoItem.text}</li>)}
[fit] Destructure the destructured
export function TodoItem({todoItem: { id, text, complete },reloadItems,}: TodoItemProps) {async function toggleCompleteStatus() {await axios.put(`https://one-list-api.herokuapp.com/items/${id}?access_token=cohort42`,{ item: { complete: !complete } })reloadItems()}return (<li className={complete ? 'completed' : ''} onClick={toggleCompleteStatus}>{text}</li>)}