In this project, we're going to take a small, existing React application and add new features to it.
Here's what the application will look like once you are done:
The code you are given for the project implements the search form and the loading of basic user info. You'll have to do all the rest.
Let's take a look at the code that's already there. Many of the starter files should already be familiar to you if you completed the previous workshop.
package.json
: Configuration file for NPM, contains dependencies and project metadata.gitignore
: Files that should be ignored by Git.node_modules
can always be regeneratedpublic/index.html
: File that gets served thru Webpack after having been filled insrc/index.js
: This file is the entry point for the app. It puts our app on the screen!src/components/*
: All the components of our application.src/index.css
: The styles for our app. Check it out to see how your starter app is being styled, and add to it to complete the project. Notice that we don't<link>
this CSS from the index? How does this work?? Make sure you understand!!!
To get started coding on this project, remember the following steps:
npm install
the first time you clone this reponpm start
anytime you want to start developing. This will watch your JS files and re-run webpack when there are changes- Start coding!
In index.js
we have the following route structure:
<Route path="/" component={App}>
<IndexRoute component={Search}/>
<Route path="user/:username" component={User}/>
</Route>
The top route says to load the App
component. Looking at the code of App.jsx
, you'll see that its render
method outputs {this.props.children}
. If the URL happens to be only /
, then React Router will render an <App/>
instance, and will pass it a <Search/>
as its child. If the route happens to be /user/:username
, React Router will display <App/>
but will pass it <User />
as a child.
When the Search
component is displayed, it has a form and a button. When the form is submitted, we use React Router's browserHistory
to programmatically change the URL. Look at the Search
's _handleSubmit
method to see how that happens.
Once we navigate to the new URL, React Router will render a User
component. Looking at the componentDidMount
method of the User
, you'll see that it does an AJAX call using this.props.params.username
. The reason why it has access to this prop is because the Router passed it when it mounted the component.
The AJAX call is made to https://api.github.com/users/{USERNAME}
and returns the following information:
{
"login": "gaearon",
"id": 810438,
"avatar_url": "https://avatars.githubusercontent.com/u/810438?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/gaearon",
"html_url": "https://github.com/gaearon",
"followers_url": "https://api.github.com/users/gaearon/followers",
"following_url": "https://api.github.com/users/gaearon/following{/other_user}",
"gists_url": "https://api.github.com/users/gaearon/gists{/gist_id}",
"starred_url": "https://api.github.com/users/gaearon/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/gaearon/subscriptions",
"organizations_url": "https://api.github.com/users/gaearon/orgs",
"repos_url": "https://api.github.com/users/gaearon/repos",
"events_url": "https://api.github.com/users/gaearon/events{/privacy}",
"received_events_url": "https://api.github.com/users/gaearon/received_events",
"type": "User",
"site_admin": false,
"name": "Dan Abramov",
"company": "Facebook",
"blog": "http://twitter.com/dan_abramov",
"location": "London, UK",
"email": "dan.abramov@me.com",
"hireable": null,
"bio": "Created: Redux, React Hot Loader, React DnD. Now helping make @reactjs better at @facebook.",
"public_repos": 176,
"public_gists": 48,
"followers": 10338,
"following": 171,
"created_at": "2011-05-25T18:18:31Z",
"updated_at": "2016-07-28T14:41:02Z"
}
GitHub API documentation for Users
In the render
method of the User
component, we are displaying the user info based on the received result, and we have three links that don't lead anywhere for the moment:
If you click on followers, notice that the URL of the page changes to /users/:username/followers
. If you have your dev tools open, React Router will give you an error message telling you that this route does not exist.
The goal of this workshop is to implement the three links above. To do this, we'll start by implementing the followers page together with step by step instructions. Then, your job will be to implement the two remaining screens and fix any bugs.
When clicking on the followers link in the UI, notice that the URL changes to /user/:username/followers
. Currently this results in a "not found" route. Let's fix this.
In index.js
, you currently have your user route setup like this:
<Route path="user/:username" component={User} />
Let's change it to a route with a nested route
<Route path="user/:username" component={User}>
<Route path="followers" component={Followers} />
</Route>
For this to do anything, we first have to implement the Followers
component.
Create a component called Followers
. Since this component is also a route component, it will receive the same this.props.params.username
. In this component, we're eventually going to do an AJAX call to grab the followers of the user.
For the moment, create the component only with a render
function. In there, use your props to return the following:
<div className="followers-page">
<h3>Followers of USERNAME</h3>
</div>
When the URL changes to followers
, we want to display the followers alongside the current User
component. This is why we are nesting the followers route inside the user route.
To reflect this nesting in our tree of components, we have to add a {this.props.children}
output to our User
component.
Modify the User
component to make it display its children just before the closing </div>
in the render
method.
When this is done, go back to your browser. Search for a user, and click on FOLLOWERS. The followers component should be displayed below the user info.
We want to load the followers of the current user as soon as the Followers
component is mounted in the DOM. In the componentDidMount
of Followers
, use fetch
to make a request to GitHub's API for the followers. Simply add /followers
to the GitHub API URL for the user e.g. https://api.github.com/users/ziad-saab/followers
In the callback to your AJAX request, use setState
to set a followers
state on your component.
Using the this.state.followers
in your render
method, display the followers that you receive from GitHub. We'll do this in a few steps.
- Create a new pure component called
GithubUser
. It should receive auser
prop, and use itsavatar_url
andlogin
properties to display one GitHub user. The whole display should link back to that user's page in your app, using React Router'sLink
component. Here's what a sample output of yourGithubUser
component should look like:
<Link to="/user/ziad-saab">
<img src="AVATAR URL"/>
ziad-saab
</Link>
And here's a visual example of four GithubUser
instances (you can use vertical-align
in your CSS to align the image and the name):
- In
Followers
, import yourGithubUser
component. - In the
render
method ofFollowers
, usemap
to take the array atthis.state.followers
, and map it to an array of<GithubUser />
elements, passing theuser
prop. The code ofFollowers
'render
method should look like this:
if (!this.state.followers) {
return <div>LOADING FOLLOWERS...</div>
}
return (
<div className="followers-page">
<h2>Followers of {this.props.params.username}</h2>
<ul>
{this.state.followers.map(/* INSERT CODE HERE TO RETURN A NEW <GithubUser/> */)}
</ul>
</div>
);
Having done this, you should have a full Followers
component ready to go.
Try to click on a follower in the followers list. Notice that the URL changes to match the user you clicked, but the display does not change to reflect that. We had the same problem in the previous workshop. If you recall, it was due to us fetching the data in componentDidMount
, but sometimes a component's props change while it's still mounted.
Here's what's happening in this case:
- User is on
/
and does a search for "gaearon" - User gets redirected to
/user/gaearon
and React Router mounts an instance of theUser
component, passing it "gaearon" asthis.props.params.username
. TheUser
component'scomponentDidMount
method kicks in and fetches data with AJAX - User clicks on FOLLOWERS, gets redirected to
/users/gaearon/followers
. React Router keeps the instance ofUser
mounted, and passes it a new instance ofFollowers
asthis.props.children
. TheFollowers
instance is mounted and itscomponentDidMount
kicks in, fetching the followers data. - User clicks on one follower called "alexkuz" and the URL changes to
/users/alexkuz
. React Router does not mount a newUser
instance. Instead, it changes theparams
prop of the existingUser
instance to make it{username: "alexkuz"}
. - Since
componentDidMount
ofUser
is not called, no AJAX call occurs.
To fix this bug, follow the same instructions you did in yesterday's workshop:
- Move the logic from
componentDidMount
to another method calledfetchData
- Call
fetchData
fromcomponentDidMount
- Implement
componentDidUpdate
and callfetchData
again but conditionally, only if theusername
prop has changed.
componentDidUpdate
gets called frequently, whether the props or the state changed. That's why it's important to always check the new vs. old state/props before calling setState
again.
Implementing the following page is an exact copy of the followers page. The only differences are:
- Use
/following
instead of/followers
in your AJAX call - The title of the page and its URL will be different
When displaying the following list, note that you can -- and should -- reuse the same GithubUser
presentational component.
Implementing the repos page is similar to the other two pages you implemented. The only differences are:
- Use
/repos
in your AJAX call - Title and URL are different
- Instead of using a
<Link>
element to link to the repo, use a regular<a href>
since you're linking to an external resource. - You'll need a new
GithubRepo
component that will act similar to theGithubUser
component you used to display the followers/following.
When you finish everything, your end-result should look and behave like this:
For this challenge, we're going to use the react-infinite
component to load extra data from the GitHub API.
Right now, if you look at a profile with a lot of followers, you'll notice that GitHub API only returns the first 25 followers. The API has a per_page
query string parameter that you can set, but the maximum number of items per page is still 100. If someone has more than 100 followers, you'd have to do many requests to get all of them.
React Infinite will take care of most of the heavy lifting for us. First of all, it's never a good idea to have thousands of elements on the page if the user is only seeing a handful. React Infinite will be efficient in showing only the elements that are in the viewport. Second, React Infinite will detect the scroll position and can fire off a callback when the scrolling reaches the edge of your container.
All you have to do is provide React Infinite with the callback function that will load your data, and pass your items to the <Infinite>
component.
Let's do it step by step for followers and then you can reproduce it for the other pages. This is what your app will look like once you are done:
Read the documentation for React Infinite to get an idea of what's going on. Once you have read the documentation, make sure to install the react-infinite
package from NPM.
Your Followers
component currently loads its data on componentDidMount
. It turns out that if you mount an <Infinite>
component without any data, it will automatically call your callback function to fetch more data.
In the constructor
method of Followers
, let's add a few more pieces of state. Let's add a page
state and initialize it to 1
. Add another state called loading
and set it to false
. Finally, add a followers
state and set it to an empty array.
Step 1.2: Change the componentDidMount
method name to fetchData
. In your AJAX call, add two query string parameters to the GitHub API URL: page
will come from your state, and per_page
can be set to anything between 1 and 100. Set it to 50. Your URL should look like this:
https://api.github.com/users/USER/followers?access_token=XXX&page=1&per_page=50
Before doing the AJAX call in fetchData
, set the loading
state to true
.
In the callback of the AJAX call, you're currently setting the followers
state to the response you receive from the GitHub server. Instead, since you already have a followers array, use the concat
method to add the new items to your existing this.state.followers
array. Additionally, set the loading
state to false
, and the page
state to whatever it currently is + 1
.
Load react-infinite
in your Followers
component, and assign it to the variable Infinite
.
In the render
method, we're currently checking if this.state.followers
is truthy. We don't need to do that anymore, because we'll always have a list of followers.
Replace your container with an <Infinite>
container, and pass it the following props:
isInfiniteLoading
: take it from yourloading
stateonInfiniteLoad
: point to yourfetchData
methoduseWindowAsScrollContainer
: this prop doesn't have a value! It will be set totrue
automaticallyelementHeight
: to scroll efficiently, React Infinite needs to know the height of an element. Use your browser's inspector to find the approximate height of yourGithubUser
elements. It's not perfect, but it'll do for now.infiniteLoadBeginEdgeOffset
: this sets the amount of pixels from the edge of your container at which more data will be loaded. Set it to100
so that the data starts loading before you reach the edge (bottom) of the window.
Your render
code should have the following in it now:
<Infinite ...all the props...>
{this.state.followers.map(...)}
</Infinite>
After you've done all these changes, your infinite scroll should be working. React Infinite will call your fetchData
method as often as needed to display new elements. Every time a new page is fetched, you're incrementing the page
state so that future page fetches will fetch the next page. Since you're concat
ing followers, your list will keep growing until there is no more data.
React Infinite lets you use a loadingSpinnerDelegate
. It's basically a React element that will be displayed below the list when loading
is true
.
You can do this as simply as loadingSpinnerDelegate={<div>LOADING</div>}
or you can go for a CSS animation, or even a GIF.
When you are done, make sure to add infinite scrolling to the following and repos pages. They should work exactly the same way :)