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.
fetch()
- Used to make requests to API.json()
- Used to parse to JS object.async
& await
are used for. Read more detailed async & await.async
- Tells JS that a function is asynchronous.await
- Tells JS that this line will take a few moments.try
& catch
are used for.n
items to the screen efficiently.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.
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
.
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>
);
}
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>
We should now see that there’s a spinner when the app loads excellent.
Key Points 🔑📝
ActivityIndicator
by React Native is a component which looks like a spinner.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.
getNews
.const getNews = () => {
// ... code soon ...
};
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'
);
};
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 ...
};
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
.
You should see something like this in your console. This is called a Promise. Promises can become much more complicated.
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'
);
};
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();
}, []);
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();
};
jsonData
object and view it’s shape.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.
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!
J) Hide the spinner when the data is fetched by setting our loading state to false.
const getNews = async () => {
// ... code ...
setLoading(false);
};
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 🔑📝
async
, await
, fetch()
, & json()
.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
.
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 }} />
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>
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>
);
});
}
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.
H) Fix the warning and implement scrolling in one go!
FlatList
from React Native.import { FlatList } from 'react-native';
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}
/>
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.
Key Points 🔑📝
Card
, Icon
, & Button
.data
prop of FlatList
is the list of items we want rendered.keyExtractor
prop of FlatList
requires a function which returns a unique key for each item for performance reasons.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
.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>
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).
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);
};
We request a specific page number when we make this request, as indicated by the string concatination at the end of the url.
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.
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 ...
};
Now we’ll see that we’ve implemented fetching additional articles with no warnings, yay.
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.
B) Add onPress
to the button so we can open up the article in the user’s browser.
Linking
from React Native.import { Linking } from 'react-native';
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}`);
}
});
};
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.
C) Wrap our api request try
& catch
to handle potential errors.
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);
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);
};
App
.if (hasErrored) {
return (
<View style={styles.container}>
<Text>Error =(</Text>
</View>
);
}
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.
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.
D) Handle case where the user has reached the last page.
lastPageReached
, which will initially be false
.const [lastPageReached, setLastPageReached] = useState(false);
getNews
. This conditional will set loading to false
and return in the event we’ve reached the last page.if (lastPageReached) return;
useEffect(() => {
getNews();
}, [articles]);
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);
}
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.
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.
FlatList
is a React Native component used to render lists
of items in a performant way.Author: PrimeTimeTran
GitHub: https://github.com/PrimeTimeTran/rn-GoogleNews
#react-native #react #mobile-apps