Who are Most Popular Redditors? Here’s How to Find out While Learning ES6 and ES7
Let’s rank the top posters in our favourite subreddit. All we need is a little Javascript.
I’m going to walk you though building a simple web app in which you can type in the name of your favourite subreddit, and a list of its top-scoring posters will appear below.
TLDR — Where’s the Code?
Here you go: https://github.com/jonnyk20/reddit-ranking
What we’ll Use
This is a perfect opportunity to learn and use some ES6 and ES7 to make our code nice and clean. Specifically, I’ll explain and use the following:
- Arrow Functions
- Template Literals
- Fetch API
- Async/Await
- Object Property Shorthand
- Object Destructuring
- Default Arguments
If instead of (or in addition to) reading this, you would like a video walk-through with aquariums and Pokemon posters in the background, then here’s something just for you:
Before We Start
- I’m hoping that you’re already comfortable with HTML and CSS, as well as the basics of Javascript, as well as how promises work.
- Please make sure you use the latest version of Chrome to open this project (mine is 68.0.3440.84). With other browsers or older versions of Chrome, some of the syntax we’ll use may not be compatible and you’d have transpile the Javascript to make to make it work.
- Pick you favourite subreddit to use as an example. I’m using ‘/r/learnjavascript’ but you’re welcome to use another as you write yours.
- For each of the ES6 and ES7 concepts we’ll be using, I’ll give a brief overview. However, I’ll also hyperlink each topic to link to further reading.
The Set Up
Our app will simply show an input and a button and will work as follows:
- You type the name of a subreddit in the input.
- You press enter or click the button.
- After a few seconds, a ranked list of cards with the subreddit’s top-scoring posters will appear.
- Each user card contains the username, the number of posts from that user, and the total score from those posts.
- Clicking on the card opens that user’s profile on Reddit
The use is pretty simple, and so is the structure. All we need is 3 files. Create a new folder for this project, and within it, create index.html
, style.css
, and script.js
.
In index.html, create a simple html skeleton. In the head section, import your style.css
and script.js
files. Add a defer
attribute to the script tag so that it doesn’t get run until your document is loaded.
In the body, add a h1, and a form with an input and a button. Below the form create div that will serve as a container for the results. Add descriptive id attributes to the form, the input, and the results container.
<!DOCTYPE html>
<html><head>
<title>Top Redditors</title>
<link rel="stylesheet" type="text/css" media="screen" href="style.css" />
<script src="script.js" defer></script>
</head><body>
<h1>Top Redditors</h1>
<form id="subreddit-select-form">
<label>r/</label>
<input id="subreddit" />
<button type="submit">Rank</button>
</form><div id="results-container">
</div>
</body></html>
In style.css, add the following styles:
body {
text-align: center;
color: #0079d3;
}button {
background-color: #0079d3;
color: white;
border-radius: 2px;
border: none;
}#results-container {
display: flex;
flex-direction: column;
}.user {
text-decoration: none;
padding: 5px;
border: solid 1px #0079d3;
margin: 5px auto;
}
You won’t be making any more changes to either of those files. script.js is where where most of the work will happen. In it, to begin with, you can just put in a console log to make sure that everything is working fine.
console.log('hello from script.js')
Open index.html
from Chrome and you should see the following page.
Use Command+Option+J (mac)
or Control+Shift+J (PC)
to open the console and see the message from script.js
.
Reddit API 101
We can’t write Javascript to interact with Reddits API if we don’t understand it first. Let’s take a look at how to receive JSON from Reddit and how to interpret it.
Getting JSON Data
To get JSON about a resource from Reddit’s API, all we need to do is add ‘.json’ to the end of a url that we’d normally use to access an htm view of that resource. For example, so see all the posts from /r/learnjavacript, we’d normally type in ‘https://www.reddit.com/r/learnjavascript’ to make a get request to that enpoint. To receive the post data in JSON, we just need to make a get request to ‘https://www.reddit.com/r/learnjavascript.json’.
Try it right now by tying that into the browser’s address bar and pressing enter. You’ll end up with something like this:
It’s impossible to read as is, so to see the structure of the response, open the ‘Network’ tab of your devtools and from the left tab, click on the request called ‘learnjavascript.json’ to open it (refresh the page if you don’t see it). Then, click the ‘Preview’ tab on the right side to see the object that comes back.
How to use Params for Limits and Pagination.
You can add parameters to your request by adding a question mark ‘?’ at the end of your request line followed by the parameters. The param key and values are separated by equal signs and each parameter is separated by an ampersand ‘&’.
In our final product, the requests will look like the following:
https://www.reddit.com/r/learnjavascript.json?limit=100&after=d8duc7vds7
But that does ‘limit’ and ‘after’ mean?
Limit is the easiest to unresdand. By default, each requests will retrieve 25 posts, but but you can increase that to any number up to 100 by adding limit=
followed by the number of posts desired.
However, most popular subreddits have far more than 100 posts, so we’ll need to make multiple requests to get more. Reddit helps us out by providing an ‘after’ property in its response. This string of characters is kind of like a bookmark that we can use to tell reddit where we left off if we want to go and fetch more posts.
If, let’s say, subreddit has 1000 posts. Our first request will return posts 1–100, along with an ‘after’ property of, for example ‘123acb’. With that, we can make another request to reddit like this…
https://www.reddit.com/r/learnjavascript.json?limit=100&after=123abc
...and the Reddit api will automatically know to give us posts 101–200 in its response. In that second response, it will include another ‘after’ property, perhaps something like ‘xyz987’. We can use that to fetch posts 201–300...and so on and so on. This process may continue until there are no more posts to fetch, in which case the ‘after’ property in the response will be null
.
If you’d like to, feel free to use a tool like Postman to play around with this before we start writing code.
The Main Event: Javascript
Everything is set up, so it’s time to write our script.
I’m going to lay out the functions, and then write them out in their entirety, while explaining in detail what everything does. In addition to that I encourage you to stop periodically as you follow, (every 5 minutes, for example), refresh index.html
and make sure that the output is what is should be at that point. You can do so in one of two ways:
- Put
console.log
statements in the functions and print out variables - Open the ‘sources’ tab in the devtools, find
scrtipt.js
and put breakpoints in the script before refreshing the page in order to use the debugger and check your variable values
Script Structure
Our script will consist of 4 main functions:
- handleSubmit: captures subreddit name from the input and then calls
fetchPosts
- fetchPosts: retrieves post from then Reddit API and then calls
parseResults
- parseResults: converts API response objects into ranked list of users and stats and then calls
displayRankings
- displayRankings: turns list of users into HTML displays it
Also we need declare some descriptive variables at the top:
- postsPerRequest: we’ll set this as the maximum allowed, 100
- maxPostsToFetch: Our requests will happen sequentially, so we need to limit them so that our script doesn’t take too long, let’s choose 500
- maxRequests:
maxPoststoFetch
divided bypostPerQuest
, so in this case, 5. After we’ve reached this number, we want to stop our script from making any more requests to the Reddit API - responses: An array that we’ll use to store the responses from all of the requests that are made
And at the bottom of the script, we need to select our subreddit selection form and attach an event listener so that whenever it’s submitted, handleSubmit
is triggered.
At this point, our script might look something like this:
const postsPerRequest = 100;
const maxPostsToFetch = 500;
const maxRequests = maxPostsToFetch / postsPerRequest;
const responses = [];const handleSubmit = e => {
// Capture subreddit name from input
};const fetchPosts = (subreddit, afterParam) => {
// Fetch from Reddit
parseResults();
};const parseResults = responses => {
// Rank Users by Post Score
displayRankings();
};const displayRankings = results => {
// Attach Rankings to DOM
};const subRedditSelectForm = document.getElementById('subreddit-select-form');
subRedditSelectForm.addEventListener('submit', handleSubmit);
If you’re not familiar with ES6, then a couple of things here might confuse you:
- The keyword ‘const’: The ES6 way of writing code recommends using
const
andlet
instead ofvar
to declare variables. In short, useconst
for variables that will not be reassigned and uselet
for those that may - Arrow functions: Arrow functions (
const myFunction = arg => {/* do things */})
work mostly the same way as functions written in the traditional way (var myfunction = function(){/* do things */}
) but look cleaner. The functional differences of arrow functions are irrelevant to this project, but definitely useful, so I recommend that you read about how they work.
All we need to do now is will out our four main functions.
- handleSubmit
Let’s start with the first and the easiest, handleSubmit
. For this one, we just need to cancel the default submission event (because we don’t want to actually submit a form anywhere), target the input, find its value (the text that was typed in), save that value to a variable and then call fetchPosts
, passing in that variable as an argument. You should end up with something like this:
const handleSubmit = e => {
e.preventDefault();
const subreddit = document.getElementById('subreddit').value;
fetchPosts(subreddit);
};
2. fetchPosts
This function will make requests to Reddit’s API and then save the responses. This is a asynchronous action, as the request will return a Promise, so we need to handle that accordingly. We’re going to use ES7’s async/await which is syntactical sugar built on promises. We define a function with the async
keyword, and then within that function, you can write await
followed by a promise and the code that follows that line will not get executed until that promise is resolved.
Basically, instead of:
var myfunction = function() {
fetchDataFromAPI.then(function(data){
console.log(data);
}
)
};
We would have
const myFunction = async () => {
const data = await fetchDataFromAPI();
console.log(data)
}
Now that we understand async/await, there’s one more thing I’d like to introduce before filling in this function, and that is template literals. With ES6, instead of putting variables into strings like this:
var age = 25;
var greeting = 'I am ' + age + ' years old';
You can write a template literal using backticks and then insert Javacript into it using a dollar sign and two curly braces.
const age = 25;
const greeting = `I am ${age} and will be ${25 + 1} next year`;
This syntax will be useful for writing our request function, as the url will change based on variables. Specifically, it’ll depend on the ‘subreddit’ and ‘afterParam’ variables, so make sure you write both of those as parameters for the function (between the parentheses). Then, put a space and the async
keyword after the equal sign in your fetchPosts
. Your function should like this:
const fetchPosts = async (subreddit, afterParam) => {
// Fetch from Reddit
parseResults();
};
Now let’s fill it in. The function needs to do the following:
- Make a requests to Reddit’s API based on the desired subreddit, the limit we’re setting on the number of posts to retrieve, and the ‘after’ parameter.(Note: ‘after’ will be undefined the first time the function runs, that is, when we make our first request to the Reddit API)
- Wait for the response from that request and then save it to a variable
- Read that response and save it as JSON. This process is also asynchronous, so we need to
await
the function call and then save the result into a variable - Push the JSON response into the
responses
array near the top of your script
The next step after that will depend. It can go one of two ways…
5-A) If the response of the request you just made contains an after
property that isn’t null
AND you haven’t reached our predefined number of maximum requests (found by checking the length of your response
array), then recursively call fetchPosts
again make another requests to fetch the posts that follow. You can do this by passing in the ‘after’ property from the request you just fetched and then attaching that as a param on the following request.
5-B. If the after
property of the response is null OR you’ve reached your max number of requests. Stop making requests and parse all the posts that you’ve requested by calling parseResults
on the responses
array.
Here’s what this looks like in code:
const fetchPosts = async (subreddit, afterParam) => {
const response = await fetch(
`https://www.reddit.com/r/${subreddit}.json?limit=${postsPerRequest}${
afterParam ? '&afterParam=' + afterParam : ''
}`
);
responseJSON = await response.json();
responses.push(responseJSON);
if (responseJSON.data.afterParam && responses.length < maxRequests) {
fetchPosts(subreddit, responseJSON.data.afterParam);
return;
}
parseResults(responses);
};
2. parseResults
This function is in charge of turning our list of responses into a ranked list of users. Before walking through the pseudocode, I need to introduce three more ES6 concepts:
Implicit return: By omitting the curly braces, arrow function can return value simplicity, that is, without using the return
keyword.
var myFunction = function(){ return 5 + 1 };
..has the same output as:
const myFunction = () => 5 + 1
Spread Operator: When you write the spread operator ...
before an object, it takes all of properties of that object (or items of that array) and spreads them out so that they are interpreted as individual items. This is useful if you want to insert all the items of an array into another array as opposed to inserting the array itself (the latter case would end up in a nested array, which we don’t want).
That means that, if we want to combine two arrays, this wouldn't work:
const arr = [1, 2, 3];
arr.push([4, 5, 6]);arr // => [1, 2, 3, [4, 5, 6]]
But this would:
const arr = [1, 2, 3];
arr.push(...[4, 5, 6]);arr // => [1, 2, 3, 4, 5, 6]
Parameter Destructuring: In a function, we can access the property of an parameter directly by replacing the parameter’s name with a set of curly braces surrounding the property(ies) we want to access. This makes it easier to use the value in our function.
Let’s say I am passing into a function an employee record stored as an object:
const employee = {
name: 'Jonny',
shifts: ['Tuesday', 'Thursday'],
contact: {
phone: '555 5555',
email: 'jonny@email.com'
}
}
Instead of having to do this:
const nextShift = (person) => {
return `${person.name}` is coming in on ${person.shifts[0]}`;
}
I can write my function as follows:
const nextShift = ({ name, shifts }) => {
return `${name}` is coming in on ${shifts[0]}`;
}
We can even get properties that are further nested in the argument, by using the following syntax:
const getPhoneNumber = ({ name, contactInfo: { phone } }) => {
return `${name}`'s phone number is ${phone}`;
}
Object Property Shorthand: If you’re defining an object’s property and assigning a value to it by using a variable with the same name, you can clean it up by simply writing the name of the property. Here’s a simple example:
Instead of writing:
const age = 25;
const person = {
age: age
}
We cam simply write:
const age = 25;
const person = {
age
}
Now that we understand our toolbox, let’s lay out the blueprint. Our parseResults needs to do the following:
- Make an array called
allPosts
in which to store all the posts - Loop through the response objects from the
responses
array, and at each one, find the array of posts (response.data.children
) and push each post intoallPosts
. - Create an empty object to store the user stats and call it
statsByUser.
In this object, each key will be a username, and that key’s value will be an object with the user’s score and post count. - Loop through each post from
allPosts
and add the info tostatsByUser
, at each iteration, one of two things can happen:
5-A) If this post’s user doesn’t yet exist in statsByUser
, create it, make the post count 1 and make the score the score from the current iteration (post)
5-B) If this post’s user exists in statsByUser
increase the post count by 1 and add the score of the current post to the already existing score
6. In order to facilitate sorting, convert the statsByUser
object to an array by getting the keys of the object using Object.keys
as an array and then mapping that array into an array of objects, each with the user’s username, score, and post count
7. Sort that array in descending order by score
8. Call displayRankings
with the sorted list
This is what it will look like in in code:
const parseResults = responses => {
const allPosts = [];responses.forEach(response => {
allPosts.push(...response.data.children);
});const statsByUser = {};allPosts.forEach(({ data: { author, score } }) => {
statsByUser[author] = !statsByUser[author]
? { postCount: 1, score } // score
: {
postCount: statsByUser[author].postCount + 1,
score: statsByUser[author].score + score
};
});const userList = Object.keys(statsByUser).map(username => ({
username,
score: statsByUser[username].score,
postCount: statsByUser[username].postCount
}));const sortedList = userList.sort((userA, userB) => userB.score - userA.score);displayRankings(sortedList);
};
displayResults
This last function simply turns our sorted list into HMTL elements and inserts them into our page.
We don’t need to learn any additional concepts for this one. However, it will help that the forEach function available to arrays has passes in each item’s index as the second argument. We can make use of this if we want to display the ranking by number.
This function should simply loop though each item in the array that is passed into it, and on each iteration, it should do the following:
- Use the index to determine that user’s rank (index + 1, since arrays begin with index 0)
- Create an anchor element and save into a variable called userCard.
- Add an href property to useCard that links to the user’s profile on Reddit
- Add text to useCard that contains the user’s username, number of posts, and total score
- Select the results-container in the DOM by its ID
- Append userCard to the results container
Here’s what the function should look like:
const displayRankings = results => {
const container = document.getElementById('results-container'); results.forEach(({ username, score, postCount }, i) => {
const rank = i + 1;
// Text
const userCard = document.createElement('a');
userCard.href = `https://www.reddit.com/user/${username}`;
userCard.classList.add('user');
userCard.innerText = `${rank}. ${username} - ${postCount} post(s) - (${score}) `;
container.appendChild(userCard);
});
};
And We’re Done!
Try it out by opening index.html, typing in the name of a subreddit into the input and then pressing enter or clicking the button. Give it a few seconds for your requests to be made, and then the ranked list should appear below the input. If you want to further challenge, add some form validation and request error handling. Tip: try/catch can be useful for error handling with async await and fetch.
I hope you enjoyed learning about some fancy new Javascript techniques, as well as the Reddit API. Please reach out if you have any questions. Check out my other articles or my Youtube channel for more tutorials.