Skip to content

Commit

Permalink
Remove async-await “exercise”
Browse files Browse the repository at this point in the history
  • Loading branch information
pepopowitz committed Mar 23, 2019
1 parent 6a13099 commit c578e0b
Show file tree
Hide file tree
Showing 180 changed files with 1,751 additions and 1,752 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "Launch Program",
"program": "${workspaceFolder}/_not_important/scripts/start.js",
"env": {
"APP_EXERCISE": "exercise-17"
"APP_EXERCISE": "exercise-16"
}
}
]
Expand Down
7 changes: 4 additions & 3 deletions exercise-12/App.js → exercise-10/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import React from 'react';

import { BrowserRouter, Route } from 'react-router-dom';

import Header from './Header';

import Friends from './friends/Friends.entry';
import FriendDetail from './friend-detail/FriendDetail.entry';

Expand All @@ -13,7 +11,10 @@ function App() {
return (
<BrowserRouter>
<div className={styles.app}>
<Header />
<header className={styles.appHeader}>
<h1 className={styles.appTitle}>Exercise 10</h1>
<h2 className={styles.subTitle}>Loading Data</h2>
</header>
<div className={styles.exercise}>
<Route path="/" exact component={Friends} />
<Route path="/friends/:id" component={FriendDetail} />
Expand Down
22 changes: 22 additions & 0 deletions exercise-10/App.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.app {
text-align: center;

--brand-dark: blueviolet;
--brand-light: ghostwhite;
}

.appHeader {
background-color: var(--brand-dark);
height: 100px;
padding: 20px;
color: white;
}

.emphasize {
text-decoration: underline;
}

.appTitle {
font-family: 'Pacifico', cursive;
line-height: 0.5em;
}
168 changes: 168 additions & 0 deletions exercise-10/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Exercise 10

## Loading Data

This exercise introduces you to a method of loading data from an API in a React component.

### Prerequisites

- If you're unfamiliar with the `async`/`await` syntax for writing asynchronous code, [read about it](../junk-drawer/ASYNC-AWAIT.md).

👉 Start the app for Exercise 10

In a console window, pointed at the root of this project, run `npm run start-exercise-10`.

This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 10: Loading Data", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand.

### The `friends` API

Prior to this exercise, we were using a static list of friends, imported from the file `data/friends.js`. We're going to instead retrieve our data from a simple API based on the contents of `data/db.json`.

The API is already running. To see it in action, you can navigate to an endpoint in your browser.

👉 Browse to the URL `http://localhost:3000/api/friends`.

You should see a JSON response that contains our three friends.

If you change any contents in `data/db.json`, the `friends` endpoint will reflect it. (Though you will have to refresh the page to see the updates.)

### Foundation

In the previous exercise, we used the `useState` hook to store component state. When rendering a component based on data from an API, you'll want to maintain state for the loaded data. You'll see the `useState` hook again in this exercise.

We'll also need to be concerned about multiple possible states, now that we're using an actual API to get our data. When a component initially renders, the state property will be empty. After the API call completes, the state property will not be empty. We will need to address this dichotomy in our components in this exercise.

#### The `useEffect` hook

Another hook, `useEffect`, lets you perform side effects when your component renders. We'll use it to call the API, and set the component's state property with the result of the API call.

Here is an example of a component using an effect hook:

```jsx
import React, { useEffect } from 'react';

function Chat(props) {

useEffect(() => {
// This will run whenever the component mounts or `props.friendId` has changed
socket.emit('join', { id: props.friendId });

return () => {
// This will run whenever the component unmounts or `props.friendId` is going to change
socket.emit('leave', { id: props.friendId });
}

// This indicates that the effect should re-run whenever `props.friendId` changes
}, [ props.friendId ])

return ( ... )
}
```

The first argument to `useEffect` is a function that should execute when the component mounts, or when props change that require the effect to re-run. In the example above, this function joins a socket based on `props.friendId`.

Not all effects require "cleanup" code - but if they do, this is accomplished by the first argument function returning another anonymous function. This returned function will execute when the component unmounts, or when props change that require the effect to re-run. In our example above, this "cleanup" function will leave a socket based on the friendId.

The second argument to `useEffect` is an array. This array will contain all props which, when their value changes, would require the effect to re-run. In the example above, we pass `[ props.friendId ]` - this means that the effect will re-run whenever the value of the `friendId` prop changes.

In some cases you won't have any reason to re-run an effect - for instance, if the component needs to load data from an API when it mounts, but never again. In this case, you'll usually want to pass an empty array (`[]`) as the second argument.

### A. Loading the Friends data from the API

The first component we'll update to pull from the API is the `FriendsEntry` component, located at `friends/Friends.entry.js`.

#### 1. Import the API client

We've included a function in `friends/get-friends-from-api.js`, which will make the API call to collect all of our friends. It uses the `axios` library to make an HTTP call to the `friends` API endpoint. Our `Friends.entry` component will use this function to load friends.

👉 Import the `getFriendsFromApi` function into `friends/Friends.entry.js`.

If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendsentry-import-api).

#### 2. Create a state property

👉 Add a state property to `friends/Friends.entry.js` to maintain the list of friends.

See [Exercise 9](../exercise-9/README.md#creating-a-state-property) for a reminder of how to create a state property.

The default value for this state property should be an empty array.

If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendsentry-state-property).

#### 3. Point the `<Friends>` component at the state property

The `FriendsEntry` component is still rendering static data directly, rather than using our new state property. Let's change that!

👉 Pass the state property from step 2 above into the `<Friends>` component

If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendsentry-pass-state-property).

Once you make this change, you will see no friends in your web app. (Don't worry, I'm still your friend!)

This is because we haven't hydrated the state property with friends yet.

#### 4. Add an effect hook to load Friends data

👉 Add a `useEffect` hook to `friends/Friends.entry.js` to load static data

See [above](#the-useeffect-hook) for details on how to create a `useEffect` hook.

This effect hook should call `setFriends` (or whatever you named your state modifier) with the static data that comes from the imported `myFriends` data source. We're regressing to using static data, but better positioning ourselves for dynamic data in a future step.

This effect hook does not require any cleanup code to execute. It also does not need to re-run when props change.

If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendsentry-useEffect).

#### 5. Get the list of friends from the API

We're finally ready to load friends dynamically.

👉 Update the `useEffect` hook to call the API and update the state property with the result.

You'll be using `async`/`await` keywords.

1. `await` the result of an asynchronous call to the `getFriendsFromApi()` API client
2. call `setFriends()` (or whatever you named your state modifier in step 2) to update the state of the component with the friend data

If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendsentry-call-api).

#### 6. Handle the "loading" state

At this point, you should notice a slight delay when the page loads, before the friends appear. This is because our API takes a bit of time to respond. The blank view you're seeing is what our component renders before the friends have been loaded from the API.

We should give the user an idea that the page is loading during this time. A great place to do this would be in the `Friends.js` component.

👉 Modify the `Friends.js` component to render an appropriately constructed page when an empty array of friends is passed in.

If the `friends` passed in contain more than one item, the `Friends.js` component should continue to render the full `Friends` list.

If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-loading-state).

#### 7. Test it out!

At this point, you should be loading the friends from an API. When the page first loads, you'll see a "Loading" message for about half a second. After that, you should see your friends.

### B. Loading the FriendDetails data from the API

The `FriendDetailEntry` component, located at `friend-detail/FriendDetail.entry.js`, also needs to load data from an API endpoint.

👉 Repeat the activity of loading data from an API for the `FriendDetailEntry` component.

Refer to your code and the notes above as a reminder of how to do this. There are a couple details that make this component different than the first:

- You'll only be loading one friend this time.
- The state property should default to `undefined`, instead of an empty array, since there is only one friend.
- The ID for the current friend will be passed into the `FriendDetailEntry` component via the `match.params.id` prop, thanks to ReactRouter.
- The function that calls the API is in `friend-detail/get-friend-from-api.js`.

If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetail).

### Test it out

You should now have your friends loading from API endpoints throughout the app.

You can verify this by making a change in `data/db.json`, and making sure the change is reflected in the app. You will need to refresh the app to see the change.

### Extra Credit

- Read more about [the `useEffect` hook](https://overreacted.io/a-complete-guide-to-useeffect/).
149 changes: 149 additions & 0 deletions exercise-10/SOLUTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Possible Solutions

## FriendsEntry: Import API

```js
import getFriendsFromApi from './get-friends-from-api';
```

## FriendsEntry: State Property

```jsx
import React, { useState } from 'react';

// ...

export default function FriendsEntry() {
const [friends, setFriends] = useState([]);

return <Friends friends={myFriends} />;
}
```

## FriendsEntry: Pass State Property

```jsx
export default function FriendsEntry() {
const [friends, setFriends] = useState([]);

return <Friends friends={friends} />;
}
```

## FriendsEntry: useEffect

```jsx
import React, { useState, useEffect } from 'react';

// ...

export default function FriendsEntry() {
const [friends, setFriends] = useState([]);
useEffect(() => {
setFriends(myFriends);
});

return <Friends friends={friends} />;
}
```

## FriendsEntry: Call API

```jsx
export default function FriendsEntry() {
const [friends, setFriends] = useState([]);

useEffect(async () => {
const friends = await getFriendsFromApi();
setFriends(friends);
}, []);

return <Friends friends={friends} />;
}
```

## Friends: Loading State

```jsx
export default function Friends(props) {
return <Page>{renderFriends(props.friends)}</Page>;
}

function renderFriends(friends) {
if (friends.length === 0) {
return <h1>Loading...</h1>;
}

return friends.map(friend => (
<FriendProfile
key={friend.id}
id={friend.id}
name={friend.name}
image={friend.image}
/>
));
}
```

## FriendDetail

### FriendDetail.entry.js

```jsx
import React, { useState, useEffect } from 'react';

import getFriendFromApi from './get-friend-from-api';

import FriendDetail from './FriendDetail';

export default function(props) {
const [friend, setFriend] = useState(undefined);

useEffect(async () => {
const id = props.match.params.id;
const friend = await getFriendFromApi(id);
setFriend(friend);
}, [props.match.id]);

return <FriendDetail friend={friend} />;
}
```

## FriendDetail.js

```jsx
import React from 'react';
import { Link } from 'react-router-dom';
import Page from '../shared/Page';
import Card from '../shared/Card';
import FriendFlipper from './FriendFlipper';

import styles from './FriendDetail.module.css';

export default function({ friend }) {
return (
<Page>
<div className={styles.friendDetail}>
<div className={styles.toolbar}>
<Link to="/">&lt; Home</Link>
</div>
<Card>{renderFriend(friend)}</Card>
</div>
</Page>
);
}

function renderFriend(friend) {
if (friend === undefined) {
return <h1>Loading...</h1>;
}

return (
<div className={styles.cardContents}>
<h1>{friend.name}</h1>
<FriendFlipper friend={friend} />
<p>{friend.bio}</p>
</div>
);
}
```
27 changes: 27 additions & 0 deletions exercise-10/complete/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';

import { BrowserRouter, Route } from 'react-router-dom';

import Friends from './friends/Friends.entry';
import FriendDetail from './friend-detail/FriendDetail.entry';

import styles from './App.module.css';

function App() {
return (
<BrowserRouter>
<div className={styles.app}>
<header className={styles.appHeader}>
<h1 className={styles.appTitle}>Exercise 10</h1>
<h2 className={styles.subTitle}>Loading Data</h2>
</header>
<div className={styles.exercise}>
<Route path="/" exact component={Friends} />
<Route path="/friends/:id" component={FriendDetail} />
</div>
</div>
</BrowserRouter>
);
}

export default App;
Loading

0 comments on commit c578e0b

Please sign in to comment.