/rn-GoogleNews

React Native with async, await, try, catch, fetch for displaying news!

Primary LanguageJavaScript

Week 5 - Homework - Google News 📰 🗞️

Introduction 🌟

Let's build a news 📰 app 📱 using React Native & Expo. Our app will help users find information about current world 🌎 events. We'll do so by requesting data from a 3rd party API and then consuming this data in our app.

pwd

Features 🎯🥇🏆

  • The user can see a list of news articles loaded from an API.
  • For each article the user sees a title, source, link to read more, and hero image.
  • The user can see how long ago the story was published in a human-friendly format; e.g. "15 minutes ago".
  • The user can see the total number of articles they've fetched from the API.
  • When the user scrolls to the end of the list, the app automatically fetches more articles and appends them to the list of current articles(adds them to the bottom of our list).
  • If the user pushes the "read more" button then the app opens up the article in the phones default browser.
  • If the api request fails, the app should prompt the user.
  • If the app is fetching additional articles, the user should be prompted accordingly.
  • If the api has no more articles to fetch, the app should not make unnecessary api requests.
  • If the user has fetched all the articles, the user should be prompted accordingly.

Learning Objectives ✍️📚📝 ️

  1. Learn how to fetch() data from an API.

    • Recognize data fetching takes time.
    • fetch() - Used to make requests to API.
    • json() - Used to parse to JS object.
  2. Learn what async & await are used for. Read more detailed async & await.

    • Recognize they're used to make asynchronous.
      • async - Tells JS that a function is asynchronous.
      • await - Tells JS that this line will take a few moments.
  3. Learn what try & catch are used for.

    • Recognize they're when we need to be careful because our code may fail. An example is an api request. There are other use cases.
  4. Learn what an open source library is and how to use them in our work.

  5. Learn how to render n items to the screen efficiently.

    • Recognize this is such a common requirement that React Native provides the component FlatList for this usecase.

Tip 💡: Almost all apps use data fetched from an API. Work slowly through this lab to make sure you understand each step and why they're required.

Milestone 1 🛣🏃 Set up initial state of loading

Let's indicate to the user we're working. Show a spinner on app load because we haven't gotten our data(news articles) yet and will immediately be working on fetching it.

A) Use expo init to create your project. I'm calling mine rn-google-news.

pwd

B) Import the required functions from React that can add statefulness to our app.

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

C) Import the ActivityIndicator component.

import { ActivityIndicator } from 'react-native';

D) Define the loading variable/state, setter method, and initial value of loading in the App component's body as true.

const [loading, setLoading] = useState(true);

E) Add a conditional to App which returns the ActivityIndicator in the event the app's state is loading.

if (loading) {
  return (
    <View>
      <ActivityIndicator />
    </View>
  );
}

pwd

F) Add style and pass the loading state to our ActivityIndicator's loading prop. This will determine whether or not the spinner should spin.

<View style={styles.container}>
  <ActivityIndicator size="large" loading={loading} />
</View>

pwd

We should now see that there's a spinner when the app loads excellent.


Key Points 🔑📝

  • Indicating to the user we're working on their behalf provides a nice experience.
  • ActivityIndicator by React Native is a component which looks like a spinner.

Milestone 2 🛣🏃 Request data from the API

We need to get the news articles data. We'll do so by using a combination of Javascript's fetch, try, catch, async, & await functions/keywords.

A) Get required api key.

Create an account here to get the free api key we'll need. The api key will look something like this:

  9eec2f7fe6cd4c40a3fef8f33f5778fa

B) Fetch the required data.

  1. Define a function which will request the data we need. I'll call it getNews.
const getNews = () => {
  // ... code soon ...
};
  1. Use JS's fetch method in the body of this function to request data from the appropriate endpoint. The argument this function takes is the URL endpoint. You'll also neeed to pass the apiKey that you got in the previous step as a parameter in the request url; the &apiKey=6eec2f7fe6cd4c40a3fef8f33f5778fe at the end.
const getNews = () => {
  const response = fetch(
    'https://newsapi.org/v2/top-headlines?country=us&apiKey=6eec2f7fe6cd4c40a3fef8f33f5778fe'
  );
};
  1. Fire the getNews function when the component mounts by passing it to useEffect(). Add a console.log to the body of getNews to confirm.
useEffect(getNews);
const getNews = () => {
  console.log('getNews function firing');
  // ... code ...
};

name

You should now see what you console.logged in your debugging console.

C) Checkout the data we got from the api request by console.logging the response.

name

You should see something like this in your console. This is called a Promise. Promises can become much more complicated.

For now, just understand that a promise is data that we will soon have

Because the fetch request takes some amount of time before it completes, our getNews function is constitutes what is known as an asynchronous function.

Asynchronous functions are so common that JS provides us a technique to handle them as if they were synchronous.

D) Add async & await to our function definition to handle the Promise.

const getNews = async () => {
  // ... code ...
  const response = await fetch(
    'https://newsapi.org/v2/top-headlines?country=us&apiKey=6eec2f7fe6cd4c40a3fef8f33f5778fe'
  );
};

You should now be able to see the response! As well as some complaints...

name

An Effect function must not return anything besides a function, which is used for clean-up.

E) Update our useEffect() function call to get rid of this warning. If you want to be an advanced React dev one day, read why here.

useEffect(() => {
  getNews();
}, []);

name

Now we'll see that the complaint goes away.

We've almost got the data we need. We just need to complete one more step.

F) Use json() to parse the JSON response to a JS object. Add another await because response is a Promise and we need to wait for it before calling json()

const getNews = async () => {
  // ... code ...
  const jsonData = await response.json();
};

You should now be able expand the jsonData object and view it's shape.

Questions ⁉️🤔😉

  1. Do you know what key we're interested in?
  2. What's the data inside this key?

name

G) Define a new piece of state, articles, to hold the data we get from the API. We choose to set it's initial state to an empty array because we want to maintain a consistent datatype.

const [articles, setArticles] = useState([]);

H) Refactor getNews to set the state with the articles we get from the api.

const getNews = async () => {
  // ... code ...
  setArticles(jsonData.articles);
};

You'll now see that now we're causing an infinite loop, our request continuously fires.

name

The reason this is occuring is because our hook fires when the component mounts, afterwards it updates state. The result of an update to our component's state is that our hook fires again; thus, the infinite loop.

I) Update the useEffect() to not cause the infinite loop.

useEffect(() => {
  getNews();
}, []);

We should now see that the getNews logs in the console once, indicating that the function only fired once; excellent!

name

J) Hide the spinner when the data is fetched by setting our loading state to false.

const getNews = async () => {
  // ... code ...
  setLoading(false);
};

name We should now see the ActivitySpinner display for a few moments while the API request processing. Then it hides when the request has completed. This is a visual indicator that we have our data, excellent.


Key Points 🔑📝

  • Making a request to an api takes time.
  • Some of the keywords involved are async, await, fetch(), & json().

Milestone 3 🛣🏃 Render news articles to screen

A) Stop your packager & simulator and install two new packages using npm.

Run the following command in your terminal window to install the required dependencies.

npm install react-native-elements moment

Afterward, run:

npm install

Start your packager & simulator. If everything installed ok you should see two new entries in your package.json.

name

B) Import the dependencies in order to render a card for each article.

import moment from 'moment';
import { Card, Button } from 'react-native-elements';

C) Use the first article we have to test our Card component from React Native Elements. Render one card with only a title & image in the body of our return. We need to pass title & image props to the Card component.

<Card title={articles[0].title} image={{ uri: articles[0].urlToImage }} />

name

We should now see one card displayed whenever the request has completed, excellent.

D) Grab Icon component from react native elements so we can complete the card.

import { Icon } from 'react-native-elements';

E) Customize our cards style by defining some new styles at the bottom.

const styles = StyleSheet.create({
  containerFlex: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center'
  },
  container: {
    flex: 1,
    marginTop: 40,
    alignItems: 'center',
    backgroundColor: '#fff',
    justifyContent: 'center'
  },
  header: {
    height: 30,
    width: '100%',
    backgroundColor: 'pink'
  },
  row: {
    flexDirection: 'row'
  },
  label: {
    fontSize: 16,
    color: 'black',
    marginRight: 10,
    fontWeight: 'bold'
  },
  info: {
    fontSize: 16,
    color: 'grey'
  }
});

F) Update Card to render all the required content in the body of our return.

<Card title={articles[0].title} image={{ uri: articles[0].urlToImage }}>
  <View style={styles.row}>
    <Text style={styles.label}>Source</Text>
    <Text style={styles.info}>{articles[0].source.name}</Text>
  </View>
  <Text style={{ marginBottom: 10 }}>{articles[0].content}</Text>
  <View style={styles.row}>
    <Text style={styles.label}>Published</Text>
    <Text style={styles.info}>
      {moment(articles[0].publishedAt).format('LLL')}
    </Text>
  </View>
  <Button icon={<Icon />} title="Read more" backgroundColor="#03A9F4" />
</Card>

pwd

You should now see a nicely formatted card with all the required data we need, excellent.

Also, notice that in the body of the card we used the moment function we installed a few steps back. We pass it an argument of a string date and format it the way we want. There are many other formats.

moment(articles[0].publishedAt).format('LLL');

G) Render every article to the screen

{
  articles.map(article => {
    return (
      <Card title={article.title} image={{ uri: article.urlToImage }}>
        <View style={styles.row}>
          <Text style={styles.label}>Source</Text>
          <Text style={styles.info}>{article.source.name}</Text>
        </View>
        <Text style={{ marginBottom: 10 }}>{article.content}</Text>
        <View style={styles.row}>
          <Text style={styles.label}>Published</Text>
          <Text style={styles.info}>
            {moment(article.publishedAt).format('LLL')}
          </Text>
        </View>
        <Button icon={<Icon />} title="Read more" backgroundColor="#03A9F4" />
      </Card>
    );
  });
}

We should see multiple articles rendered to the screen now

pwd

However, we also find React complaining. We need to add a key prop to the Card component because it's being rendered in a list. This is for performance reasons.

More importantly, we cannot scroll down to view other articles we've fetched from the api.

pwd

H) Fix the warning and implement scrolling in one go!

  1. Import FlatList from React Native.
import { FlatList } from 'react-native';
  1. Render FlatList in the body of the return & pass it the appropriate props. Learn more about the props FlatList can take here.
<FlatList
  data={articles}
  renderItem={renderArticleItem}
  keyExtractor={item => item.title}
/>
  1. Define the renderArticleItem function we passed to the FlatList component's prop renderItem. It should return the Card we previously had in the body of the App component(the jsx for rendering an article).
const renderArticleItem = ({ item }) => {
  return (
    <Card title={item.title} image={{ uri: item.urlToImage }}>
      <View style={styles.row}>
        <Text style={styles.label}>Source</Text>
        <Text style={styles.info}>{item.source.name}</Text>
      </View>
      <Text style={{ marginBottom: 10 }}>{item.content}</Text>
      <View style={styles.row}>
        <Text style={styles.label}>Published</Text>
        <Text style={styles.info}>
          {moment(item.publishedAt).format('LLL')}
        </Text>
      </View>
      <Button icon={<Icon />} title="Read more" backgroundColor="#03A9F4" />
    </Card>
  );
};

We should now see that the list of news articles is scrollable and the warning goes away, nice.

pwd


Key Points 🔑📝

  • React Native Elements is a library that provides beautifully styled components. We've used Card, Icon, & Button.
  • Moment is a library which helps us to parse dates to human readable formats. It works for other locales as well!
  • The data prop of FlatList is the list of items we want rendered.
  • The keyExtractor prop of FlatList requires a function which returns a unique key for each item for performance reasons.
  • The renderItem prop of FlatList requires a function which returns jsx for an individual list item. This function will take an item prop which is an individual article.

Milestone 4 🛣🏃 Implement fetching of additional articles

A) Add some jsx to the top of the return to indicate articles count.

<View style={styles.row}>
  <Text style={styles.label}>Articles Count:</Text>
  <Text style={styles.info}>{articles.length}</Text>
</View>

pwd

We should now see a articles count at the top.

However, if we scroll to the bottom, we'll see nothing happens(we dont grab additional articles from our api).

pwd

B) Define a new piece of state pageNumber.

const [pageNumber, setPageNumber] = useState(1);

C) Refactor getNews to use this state as well as update it after we've made the api request.

const getNews = async () => {
  const response = await fetch(
    `https://newsapi.org/v2/top-headlines?country=us&apiKey=6eec2f7fe6cd4c40a3fef8f33f5778fe&page=${pageNumber}`
  );
  const jsonData = await response.json();
  setArticles(articles.concat(jsonData.articles));
  setPageNumber(pageNumber + 1);
  setLoading(false);
};
  1. We request a specific page number when we make this request, as indicated by the string concatination at the end of the url.

  2. We need to concat our previous articles with the newly fetched articles.

D) Pass two new props to FlatList which will handle the behavior for fetching more articles when the user has scrolled to the end.

<FlatList onEndReached={getNews} onEndReachedThreshold={1} />

Now we'll see that when the user scrolls to the bottom of the list our we automatically fetch additional articles. You'll notice the articles count increase at the top of the app.

pwd

However, we're getting a ton of complaints from React. This is because we're firing our getNews very quickly and getting the same data. We need to fix this by removing duplicated articles from our array.

G) Define a new function outside our App component, filterForUniqueArticles, which filters out duplicate articles.

const filterForUniqueArticles = arr => {
  const cleaned = [];
  arr.forEach(itm => {
    let unique = true;
    cleaned.forEach(itm2 => {
      const isEqual = JSON.stringify(itm) === JSON.stringify(itm2);
      if (isEqual) unique = false;
    });
    if (unique) cleaned.push(itm);
  });
  return cleaned;
};

H) Call this function in the body of our getNews and pass the return value to setArticles.

const getNews = async () => {
  // ... code ...
  const newArticleList = filterForUniqueArticles(
    articles.concat(jsonData.articles)
  );
  setArticles(newArticleList);
  // ... code ...
};

pwd

Now we'll see that we've implemented fetching additional articles with no warnings, yay.

Milestone 5 🛣🏃 Add Polishing touches and safety mechanism

A) Add a spinner to the bottom of the FlatList to let the user know we're fetching more Articles after they've reached the bottom.

<FlatList
  ListFooterComponent={<ActivityIndicator size="large" loading={loading} />}
/>

We should now see that there's a spinner at the bottom of the FlatList when we get to the end for a few moments.

pwd

B) Add onPress to the button so we can open up the article in the user's browser.

  1. Import Linking from React Native.
import { Linking } from 'react-native';
  1. Define an onPress function in the body of our App component.
const onPress = url => {
  Linking.canOpenURL(url).then(supported => {
    if (supported) {
      Linking.openURL(url);
    } else {
      console.log(`Don't know how to open URL: ${url}`);
    }
  });
};
  1. Pass our custom onPress function to the onPress prop of Button. This function takes the item's url as an argument.
<Button onPress={() => onPress(item.url)} />

We should not be able to press/click on the button in order to navigate to the article in the phone's default browser, excellent.

pwd

C) Wrap our api request try & catch to handle potential errors.

  1. Define a new piece of state, hasErrored, with an initial state of false because when the app loads, the app hasn't made a request that has failed yet.
const [hasErrored, setHasApiError] = useState(false);
  1. Wrap our getNews functions body with a try & catch. If the request fails, we call setHasApiError with an argument of true.
const getNews = async () => {
  setLoading(true);
  try {
    const response = await fetch(
      `https://newsapi.org/v2/top-headlines?country=us&apiKey=6eec2f7fe6cd4c40a3fef8f33f5778fe&page=${pageNumber}`
    );
    const jsonData = await response.json();
    const newArticleList = filterForUniqueArticles(
      articles.concat(jsonData.articles)
    );
    setArticles(newArticleList);
    setPageNumber(pageNumber + 1);
  } catch (error) {
    setHasApiError(true);
  }
  setLoading(false);
};
  1. Add a conditional return in the body of App.
if (hasErrored) {
  return (
    <View style={styles.container}>
      <Text>Error =(</Text>
    </View>
  );
}
  1. Deliberately fail the api by passing it a nonsense endpoint to test behavior.
const response = await fetch(`https://wrongapi.com`);

We want to hide the spinner and notify the user if the request fails. The try & catch is useful for many other situations as well. In the event of a request failure, the user sees a prompt, in the event of success, the content; amazing.

pwd

If you look closely however, we can seemingly load an infinite number of pages. This is because we haven't checked our response for new articles. In other words, we allow the user to request pages infinitely, despite the fact that the api may not have that many pages.

pwd

D) Handle case where the user has reached the last page.

  1. Define a new piece of state, lastPageReached, which will initially be false.
const [lastPageReached, setLastPageReached] = useState(false);
  1. Add a conditional to the body of getNews. This conditional will set loading to false and return in the event we've reached the last page.
if (lastPageReached) return;
  1. Update our useEffect to monitor the articles piece of state only(not loading).
useEffect(() => {
  getNews();
}, [articles]);
  1. Add a conditional to the body of getNews within the try which will check for how many articles we got back from the api. In the event we've reached the last page, the length of articles will be 0.
const hasMoreArticles = jsonData.articles.length > 0;
if (hasMoreArticles) {
  const newArticleList = filterForUniqueArticles(
    articles.concat(jsonData.articles)
  );
  setArticles(newArticleList);
  setPageNumber(pageNumber + 1);
} else {
  setLastPageReached(true);
}
  1. Add a ternary operator to the ListFooterComponent prop of our FlatList. If there aren't more articles, return a prompt to the user. Otherwise, return the ActivityIndicator like before.
ListFooterComponent={lastPageReached ? <Text>No more articles</Text> : <ActivityIndicator
  size="large"
  loading={loading}
/>}

We should now see that when we get to the bottom of the list, we prompt the user that there are no more articles and stopped making unnecessary api requests, saving them money on their data plan, excellent.

pwd

Key Points 🔑📝

  • A lot of the work we do when we build apps is related to giving the user feedback.

  • We can open up web pages in our app by using Linking.

  • When something in our app may fail we should use try & catch as a safety mechanism.


Review 💻🤓🤔

  • Most apps requirie data of some form.
  • API's can be called to fetch dynamic data.
  • API requests are asynchronous.
  • APIs are unique and we need to study their documentation in order to use them correctly.
  • There are many public APIs available.
  • FlatList is a React Native component used to render lists of items in a performant way.
  • There are many public libraries availaboe for specific use cases.

Accomplishments 🥇🏆💯

  • The user can see a list of news articles loaded from an API.
  • For each article the user sees a title, source, link to read more, and hero image.
  • The user can see how long ago the story was published in a human-friendly format; e.g. "15 minutes ago".
  • The user can see the total number of articles they've fetched from the API.
  • When the user scrolls to the end of the list, the app automatically fetches more articles and appends them to the list of current articles(adds them to the bottom of our list).
  • If the user pushes the "read more" button then the app opens up the article in the phones default browser.
  • If the api request fails, the app should prompt the user.
  • If the app is fetching additional articles, the user should be prompted accordingly.
  • If the api has no more articles to fetch, the app should not make unnecessary api requests.
  • If the user has fetched all the articles, the user should be prompted accordingly.

Rockets 🚀

  • User can see a list of individual publishers.
  • User can see how many articles each publisher has made.
  • User can search articles by title.