Fetching Data from the Reddit API
Eventually we are going to implement a rather complex stream to deal with fetching data from Reddit — this will include pagination and automatic retries to fetch more content if we didn’t find enough GIFs. But, to begin with, we are going to keep things simple. We are just going to make a single request to the Reddit API to retrieve data and we will save paging until the next lesson.
Create the Interfaces
We like to know what data is available in our objects. We have used this
multiple times now where we have an interface so that we can enforce that
something has certain properties. This way, TypeScript will warn us when we have
done something wrong and we can also get our data auto completing in the
template.
We are about to pull in some data from Reddit. We have our Gif interface
already, but that is the target object we want to create from the data we pull
in, the data that Reddit returns is a bit different.
We could just pull in the data from Reddit, give it an any type, and not worry
about it. But let’s say we then access some data like:
data.titleIf we just give our data any type then it will assume the value is valid,
regardless of whether or not that property exists. So, first, we are going to
create some interfaces that define the “shape” of the data returned from Reddit.
export interface RedditPost { data: RedditPostData;}
interface RedditPostData { author: string; name: string; permalink: string; preview: RedditPreview; secure_media: RedditMedia; title: string; media: RedditMedia; url: string; thumbnail: string; num_comments: number;}
interface RedditPreview { reddit_video_preview: RedditVideoPreview;}
interface RedditVideoPreview { is_gif: boolean; fallback_url: string;}
interface RedditMedia { reddit_video: RedditVideoPreview;}We really just want one interface called RedditPost here, but that data is an
object that contains multiple children. Some of those children also contain
additional objects. What we need to do is inspect the response returned from
making a request to the Reddit API:
https://www.reddit.com/r/gifs/hot/.json?limit=100And for all the simple values like strings and numbers we can just add directly to our interface. But, when we run into another object, we need to create a separate interface that is then used in our parent interface. This is why we end up with such a complex structure that in a general sense looks like this:
RedditPost { RedditPostData { RedditMedia {} RedditPreview { RedditVideoPreview } }}We drill down through the structure returned from the API and map it out in our interface.
import { RedditPost } from './reddit-post';
export interface RedditResponse { data: RedditResponseData;}
interface RedditResponseData { children: RedditPost[];}The RedditPost data we want is not the only data that is returned from Reddit, but it is all we are interested in. The purpose of this interface is to be a container for the response returned from Reddit.
export * from './gif';export * from './reddit-post';export * from './reddit-response';Making a real HTTP request
Before we can make HTTP requests, we need to add the HttpClientModule to our
application. See if you can remember how to do that, but I will also post the
solution below.
Click here to reveal solution
Solution
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';import { provideRouter } from '@angular/router';
import { routes } from './app.routes';import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), provideHttpClient(), ],};import { Injectable, computed, inject, signal } from '@angular/core';import { takeUntilDestroyed } from '@angular/core/rxjs-interop';import { HttpClient } from '@angular/common/http';import { Gif, RedditPost, RedditResponse } from '../interfaces';import { EMPTY, catchError, map } from 'rxjs';
export interface GifsState { gifs: Gif[];}
@Injectable({ providedIn: 'root' })export class RedditService { private http = inject(HttpClient);
// state private state = signal<GifsState>({ gifs: [], });
// selectors gifs = computed(() => this.state().gifs);
//sources private gifsLoaded$ = this.fetchFromReddit('gifs');
constructor() { //reducers this.gifsLoaded$.pipe(takeUntilDestroyed()).subscribe((gifs) => this.state.update((state) => ({ ...state, gifs: [...state.gifs, ...gifs], })) ); }
private fetchFromReddit(subreddit: string) { return this.http .get<RedditResponse>( `https://www.reddit.com/r/${subreddit}/hot/.json?limit=100` ) .pipe( catchError((err) => EMPTY), map((response) => this.convertRedditPostsToGifs(response.data.children)) ); }
private convertRedditPostsToGifs(posts: RedditPost[]) { // TODO: Implement }
private getBestSrcForGif(post: RedditPost) { // TODO: Implement }}We have the same general outline for our state management, but we have set up a little bit of extra structure here.
Our request to Reddit is going to become quite complex, so we have separated it
out into a separate fetchFromReddit method that accepts the subreddit we want
to fetch GIFs from as a parameter. Since we are dealing with an HTTP request, we
have to consider that it might fail. We are dealing with this in a simple way
here with catchError. If there is an error, we will catch it and return the
EMPTY stream instead which will stop the stream from breaking entirely.
Basically, this strategy just ignores errors as if they never happened — if a user makes a request to a subreddit that does not exist, they will just get no results. Later, we will handle errors properly by actually displaying something to the user.
We also map the response we get from our HTTP request because we don’t want
all of the raw data — we want to convert it into our specific format for
a RedditPost. To do this, we will first pass it to the
convertRedditPostsToGifs method which will handle most of the data formatting.
There is another tricky aspect here though, and that is that there are multiple
different types of media that might be used for a “GIF”. Our getBestSrcForGif
method will handle searching through the data and returning the best available
src for our video tag.
Again, there is a lot more manual handling than is required typically for applications. Usually when you are working with an API it will return data in the format you need — in this case, we are using an API in a non standard way. This does give us good practice for dealing with more complex scenarios in our application though.
Let’s finish implementing those placeholder methods now.
private convertRedditPostsToGifs(posts: RedditPost[]) { const defaultThumbnails = ['default', 'none', 'nsfw'];
return posts .map((post) => { const thumbnail = post.data.thumbnail; const modifiedThumbnail = defaultThumbnails.includes(thumbnail) ? `/assets/${thumbnail}.png` : thumbnail;
const validThumbnail = modifiedThumbnail.endsWith('.jpg') || modifiedThumbnail.endsWith('.png');
return { src: this.getBestSrcForGif(post), author: post.data.author, name: post.data.name, permalink: post.data.permalink, title: post.data.title, thumbnail: validThumbnail ? modifiedThumbnail : `/assets/default.png`, comments: post.data.num_comments, }; }) .filter((post): post is Gif => post.src !== null); }All we are doing here is taking values from the RedditPost array that the API
returns, and we are converting it into our own Gif format. If the Gif does
not have a src then we filter it out. Note that we are also mapping the
thumbnail to different values here — this is so that later we can supply our
own images for certain types of thumbnails.
private getBestSrcForGif(post: RedditPost) { // If the source is in .mp4 format, leave unchanged if (post.data.url.indexOf('.mp4') > -1) { return post.data.url; }
// If the source is in .gifv or .webm formats, convert to .mp4 and return if (post.data.url.indexOf('.gifv') > -1) { return post.data.url.replace('.gifv', '.mp4'); }
if (post.data.url.indexOf('.webm') > -1) { return post.data.url.replace('.webm', '.mp4'); }
// If the URL is not .gifv or .webm, check if media or secure media is available if (post.data.secure_media?.reddit_video) { return post.data.secure_media.reddit_video.fallback_url; }
if (post.data.media?.reddit_video) { return post.data.media.reddit_video.fallback_url; }
// If media objects are not available, check if a preview is available if (post.data.preview?.reddit_video_preview) { return post.data.preview.reddit_video_preview.fallback_url; }
// No useable formats available return null; }This is just kind of an ugly method because the Reddit API is quite awkward to
work with in this manner. There are all sorts of different types of properties
the media might be stored under, so we are checking each of those in order and
returning the media if it is available — otherwise we return null.
Displaying the GIFs
Ok, obviously we need to do something about this invisible videos situation. The
GifPlayerComponent actually works quite well at this point — we can load the
videos successfully, we will see the loading spinner display, and the video will
start playing when it has loaded. We can also click the video again to pause it.
But wouldn’t it be nice if we could actually see the videos first?
What we are going to do now is make use of the GIFs thumbnail property so that
we have a preview of the GIF before we actually play it.
<div class="preload-background" [style.background]="'url(' + thumbnail() + ') 50% 50% / cover no-repeat'" [class.blur]=" status() !== 'loaded' && !['/assets/nsfw.png', '/assets/default.png'].includes(thumbnail()) " > <video (click)="togglePlay$.next()" #gifPlayer playsinline preload="none" [loop]="true" [muted]="true" [src]="src()" ></video> </div>We are now dynamically setting the background to use the thumbnail for that
particular GIF.
Because the thumbnails are generally going to be a low resolution, we are also adding a blur to disguise that fact (instead it makes it look like a mysterious preview). However, we don’t always want to blur the thumbnail. There are some special thumbnails — like those that appear for NSFW (Not safe for work) videos — that we don’t want to display with a blur. That is why we use a dynamic class which will allow us to dynamically apply the styling depending on the loading status and whether we are dealing with one of our “special” thumbnails.
Keep in mind that if you want the special thumbnails to display — like the NSFW
warning — you will need to copy the assets from the assets folder in the
source code provided with this module, or supply your own images.
The resulting styles in the application are still not great, but it is a lot better now and we can interact with the videos more easily. We will continue to improve the styling of the application as we go.
Take a break, have some fun playing with the gifs we have loaded in so far. Over the next few lessons we are going to crank up the complexity even more by handling things like loading in additional pages of gifs, allowing the user to change the subreddit whenever they want, and handling tricky situations like errors and what to do if we don’t get enough valid gifs returned from a single request.