-
Notifications
You must be signed in to change notification settings - Fork 10
Lesson 1.8
With Hooks, it became possible for function components to manipulate state, and have access to a few other parts of React that only class components could use until then. One of those parts of React is the ability to run code after a component has rendered.
A class component has access to component lifecycle methods such as ComponentDidMount, ComponentDidUpdate, ComponentWillUnmount. These methods only allow class components access to the mounting, updating, and unmounting events of a component.
For example, when the user lands on the page, the component is mounted, then as the states updates, the component is also updated, and, finally, the component is unmounted when the user leaves the page.
But what about functional components?
The useEffect hook gives you the ability to run code after a function component has rendered. By default it runs after render and after each update. This default behavior has particularly nasty consequences when writing a useEffect that could trigger a render. For example, your useEffect triggers a render, the component renders, and
useEffect runs again - causing an infinite loop.
Luckily, we can control when it is triggered by taking advantage of useEffect's second argument, a dependency array. You add all of the state values into this array that you want the useEffect to pay attention to. Whenever one of the values in the dependency array changes, the useEffect fires again. There are a couple of other details to note about with this array. If no array is provided, the effect will fire off any time anything in that component changes. If an empty array [] is provided, the useEffect only fires once on each mount.
If you happen to be using a useEffect to add in a 3rd party event listener, websocket connections, setInterval, or any other similar tools, you must also provide a cleanup function that the useEffect returns. We will not have to do this with our basic todo app but this will come in handy as you progress as a React developer. See more in the React docs
useEffect(
() => {
/*MOUNTING (see below)*/
return () => {
/*CLEANUP FUNCTION (see below)*/
};
},
[] /*DEPENDENCY ARRAY (see below)*/
);- mounting: That's when the component is initially rendered when the user lands on the page.
- cleanup function: or when the user leaves the page and the component will unmount.
- dependency array: array of dependencies that trigger the useEffect when they change
The useEffect hook is useful when you want to update the UI when state changes. You can also define a state on first load (i.e. componentDidMount), and also clean the state when the component is unmounting (componentWillUnmount).
import { useState } from "react"
const Button = () => {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>You clicked {count} times</button>;
}
export default ButtonIt's very common when building an application to fetch data from an API.
When a user lands on our page, we want to call the API. In other words, we want to call the API during the mounting part of the component's lifecycle.
Note: We will use the use-case of connecting and writing to the Airtable API in our examples:
We connect to the API with a unique token to authorize the Fetch API. Responding to server error(s) are handled by the try/catch but if there is a 4xx client error, checking the response.ok property tells us if the response was successful or not.
After fetching the response object, we first need to convert the data stream to JSON with response.json(). We must retain the id field returned by the Airtable API so it can be used for CRUD operations such as writing, updating and deleting.
The data stored in a table is returned in a fields object with each property a field in the table you created earlier. In this example, there is just one property of name stored in fields.
After retrieving the data it can them be used in local state of whichever component needs to store it to be re-rendered to the UI.
const loadTodos = async() => {
try {
const response = await
fetch(`https://api.airtable.com/v0/${import.meta.env.VITE_AIRTABLE_BASE_ID}/Default`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
});
if (!response.ok) {
const message = `Error: ${response.status}`;
throw new Error(message);
}
const todosFromAPI = await response.json();
const todos = todosFromAPI.records.map((todo) => {
const newTodo = {
id: todo.id,
title: todo.fields.title
}
return newTodo
});
setTodos(todos);
} catch (error) {
console.log(error.message)
}With creating using a POST method, the Airtable API requires new record(s) to be wrapped in an object within a fields property. The data sent to the Airtable API will be sent in the body of the response.
const postTodo = async (todo) => {
try {
const airtableData = {
fields: {
title: todo,
},
};
const response = await fetch(
`https://api.airtable.com/v0/${import.meta.env.VITE_AIRTABLE_BASE_ID}/Default`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${import.meta.env.VITE_AIRTABLE_API_TOKEN}`,
},
body: JSON.stringify(airtableData),
}
);
if (!response.ok) {
const message = `Error has ocurred:
${response.status}`;
throw new Error(message);
}
const dataResponse = await response.json();
return dataResponse;
} catch (error) {
console.log(error.message);
return null;
}
};Note that this code will not be the same as the examples above.
- Sign up (or login) for an Airtable account.
- Create a new base.
- Choose a Title, Icon, and Color for your base.
- Rename "Table 1" to "Default".
- Within the table, rename the first column to
titleand delete the other columns. - Create another column called
completedAtand set it to a date format and use the ISO option in the dropdown. We will not be using this field in this lesson but it will allow us to enhance the application later. - Add one or more todo items to the table.
- Open code editor
- Create a new file named
.env.localin the root directory
"dotenv" files use the syntax
VARIABLE=valueto add key-value pairs of data available to an application at runtime. In the case of React applications scaffolded with Vite variable keys prefixed with "Vite" are available for developers to reference inside of their application. See Env Variables and Modes in the Vite documentation for more details.
- Navigate to https://airtable.com/account and create a new token (to see visuals on this step, visit https://github.com/Code-the-Dream-School/react/wiki/API-Token-visual-steps)
- If you have any tokens already assigned to the base, we recommend deleting your old ones first.
- Make sure the
Scopesoption includes "data.records:read" and "data.records:write" - Make sure the
Accessis set to your table that you made according to assignment 1.8 instructions above. - Once you save, it will give you a token for you copy. NOTE: It will only show the API token once. If you lose it you will need to regenerate a new token.
- Open
.env.local. - Create a new variable named
VITE_AIRTABLE_API_TOKEN. - Paste the token as its value.
- Back in Airtable, click "Web API Documentation" located in the "Help" section of the page on the right, then click on the table you made.
- Copy the "Base ID".
- Open
.env.local. - Create a new variable named
VITE_AIRTABLE_BASE_ID. - Paste the ID as its value.
- Create a new variable named
VITE_TABLE_NAMEand then set it equal to "Default"
At this point, you should have a dotenv file that resembles:
VITE_AIRTABLE_API_TOKEN=super_secret_value
VITE_AIRTABLE_BASE_ID=super_secret_value
VITE_TABLE_NAME=Default
- Open
src/App.jsx - Above the first
useEffectcreate a new async functionfetchData(refer to examples above)- replace the contents of the useEffect with a call to
fetchData() - Inside the
fetchDatafunction, declare an empty object variable namedoptions- add a
methodkey with the value 'GET' - add a
headerskey with an object{Authorization:Bearer ${import.meta.env.VITE_AIRTABLE_API_TOKEN}}`
- add a
- create a new variable
urland set it tohttps://api.airtable.com/v0/${import.meta.env.VITE_AIRTABLE_BASE_ID}/${import.meta.env.VITE_TABLE_NAME} - set up a try/catch statement after
options - in the
tryblock:- add a const
responsethat awaitsfetch. Pass inurlandoptionsas arguments for the fetch. - add a conditional statement that throws a new Error if
response.okis false.- provide the Error an argument
Error: ${response.status} - Note: throwing an error here will prevent the continued execution of the
tryblock and moves execution directly to the catch block if there is something wrong with fetch's response.
- provide the Error an argument
- below (not in) the
ifblock, declare a variable,data, that awaits a parsed version ofresponse(hint:response.json()) - Make a console statement that prints out the data variable to observe Airtable's API response. Here is an example response:
Note that Airtable returns an object that contains a recordsarray. You should also notice that the objects in the array contain anid, acreateTime, and afieldsobject. Thefieldsobject contains atitlekey set to the todo's title. We are going to have to do some work to transform this data into something that the todo app can use. - declare another variable,
todoswhich accepts the results of mappingdata.recordsinto an array of todo objects that have the same schema as the existing todos: (hint:{title:"make breakfast", id: "someUniqueValue" }). See the first code example above if you get stuck. - now
console.logthetodosarray and observe that they now resemble the todos that we have been working with. - remove both
console.logstatements as we are done with them. - set the application's todoList by passing the
todoscreated above tosetTodoList - use
setIsLoadingto setisLoadingto false to indicate to the user the fetch is complete
- add a const
- in the
catchblock:- create a console statement that logs the error's message. This can be used to enhance your application later by letting the user know that something went wrong with the fetch.
- replace the contents of the useEffect with a call to
- Add a POST feature to add todos to the Airtable API.
One detail to note with the code above is that we are using the id off of the todo entry from Airtable. We are no longer using our own Date.now() to set todo ids. To account for this, you will have to send the new todo title to the Airtable API. When it hits, Airtable will immediately send back a response containing the new todo including an id. You then use this data to add the todo to the todo list.
sequenceDiagram;
todo form -->>addTodo(): submit event
addTodo()->>fetch(POST): "make lunch" (todo.title)
fetch(POST)-)API: '{"fields":{"title":"make lunch"}}'
API-)addTodo(): success response with todo payload (see below)
addTodo()-)setTodoList(): {id: resp.id, title: resp.Fields.title}
setTodoList()--xtodoList component: re-renders
// example Airtable API response payload
{"id":"recu7AWDntmBYNkSI","createdTime":"2023-03-01T11:41:20.000Z","fields":{"title":"make lunch"}}This approach, called a "pessimistic rendering". So called, because we are waiting to ensure that we have good data to work with before we update our user interface- we are not assuming that the api call is going to be processed by the server 100% of the time. I encourage you to take this approach first and then do some research into "optimistic vs pessimistic rendering" for your final project. See if you can figure out how to make an intermediate step of adding the todo to the list but removing it if there are any problems saving it on Airtable.