End to End React with Prisma 2
Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.
28. Create the generate article list component
Objective: Create a component to dynamically fetch your RSS feed.
We started off this whole web building journey by saying that we were going to build a news application. We've been building a whole lot of scaffolding but there hasn't been a single spot yet where we actually load a news feed or show an article title! In this step we will change all of that.
We are going to create a component that will take the url that we enter for a feed and dynamically pull all the article information from every item in that RSS feed.
components/generateArticleList.tsx
tsx
import { useEffect, useState } from 'react'import * as _ from 'lodash'import { Feed } from '@prisma/client'import { NotifyError } from './notifyError'import { NotifyLoading } from './notifyLoading'export const GenerateArticleList = ({ feeds }: { feeds: Feed[] }) => {const [{ loading, error, data }, setGet] = useState({error: false,loading: false,data: [],})useEffect(() => {console.log(feeds)}, [])if (loading) {return <NotifyLoading />}if (error) {return <NotifyError />}console.log(data)return <p>Generate Article List</p>}
This component is pretty simple right now- we have a react hook that will store the fetched data in a variable called data
and then we also have a loading
and error
state. This is all mimicing apollo's useQuery
function.
Now would be a good time to clean out our database using the clearAll
mutation so that we will have valid urls for our feeds so that we can be sure that this GenerateArticleList
component is actually loading the data correctly. We should load up several new feeds and bundles with actual rss urls. You might want to google for this or here is an article I found which has some rss feeds from popular news sources.
Next, we should add this component to all of the pages that will need it. Even though it isn't working yet, we might as well get it in place so that we'll immediately know that it works once it is finished.
pages/bundle/[id].tsx
tsx
// more component above<Layout><h3 className="text-lg font-medium pt-4">{bundle.name}</h3><p className="pb-4">{bundle.description}</p><h3 className="pb-4 font-medium">Feeds</h3><div className="grid grid-cols-3 gap-4">{bundle.feeds.length > 0 ? (bundle.feeds.map((item: FeedObject) => (<OneListItem item={item} type={ItemType.FeedType} key={item.id} />))) : (<p>None are present. Why not add one?</p>)}</div><GenerateArticleList feeds={bundle.feeds} /></Layout>// more component below
pages/feed/[id].tsx
tsx
// more component above<Layout><h3 className="text-lg font-medium pt-4">{feed.name}</h3><p className="pb-4">{feed.url}</p><h3 className="pb-4 font-medium">Bundles</h3><div className="grid grid-cols-3 gap-4">{feed.bundles.length > 0 ? (feed.bundles.map((item: BundleObject) => (<OneListItem item={item} type={ItemType.BundleType} key={item.id} />))) : (<p>This feed does not belong to any bundles.</p>)}</div><GenerateArticleList feeds={[feed]} /></Layout>// more component below
pages/feeds.tsx
tsx
// more component above;<ItemListtype={ItemType.FeedType}useSelected={true}allowEdits={true}selected={selected}setSelected={setSelected}/>{selected.feeds.length > 0 ? (<GenerateArticleList feeds={selected.feeds} />) : (<h3 className="py-4 font-medium">No Feed Selected</h3>)}// more component below
pages/bundles.tsx
tsx
// more component above;<ItemListtype={ItemType.BundleType}useSelected={true}allowEdits={true}selected={selected}setSelected={setSelected}/>{selected.feeds.length > 0 ? (<GenerateArticleList feeds={selected.feeds} />) : (<h3 className="py-4 font-medium">No Bundle Selected</h3>)}// more component below
pages/index.tsx
tsx
// more component above<Layout><h3 className="grid-cols-1 justify-start flex text-lg font-medium py-4">Home Page</h3><ItemListtype={ItemType.BundleType}useSelected={true}selected={selected}setSelected={setSelected}/>{selected.feeds.length > 0 ? (<GenerateArticleList feeds={selected.feeds} />) : (<h3 className="py-4 font-medium">No Bundle Selected</h3>)}</Layout>// more component below
Now we should be seeing the text Generate Article List
on all of the various pages. Off to a good start! If we look at the console, we'll see that the feed urls get displayed for the various feeds or bundles as we click between them.
The first thing we want to do is lock down the useEffect
hook so that it only gets triggered with changes in the feeds
prop.
components/generateArticleList.tsx
tsx
useEffect(() => {console.log(feeds)}, [feeds])
Now let's use the library rss-parser
to actually fetch the RSS feed.
npm install --save rss-parser
components/generateArticleList.tsx
tsx
const Parser = require('rss-parser')const parser = new Parser()const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/'// more react codeuseEffect(() => {;(async () => {try {await Promise.all(feeds.map(async oneFeed => {const { items } = await parser.parseURL(CORS_PROXY + oneFeed.url)console.log(items)}))} catch (error) {}})()}, [feeds])
The parser will take the url and fetch the RSS feeds from it. Due to restrictions with CORS this normally would fail, but there is a nifty application that will fix this so we can actually fetch the RSS feeds even though they aren't located on the same server as our own web server- this is the cors-anywhere
app.
Now when we look at the console.log we will see that it is populated by a bunch of rss articles. This is exactly what we want.
You'll notice the start of a problem- when we go to the bundles page. We'll get the console.log fired off twice. This is because we are feeding an array of feeds into the GenerateArticleList
component. We somehow need a way to create 1 list of rss feeds from the 2 feeds.
A good way to do this is using the reduce javascript function. This allows us to convert the 2 arrays into 1 big array.
components/generateArticleList.tsx
tsx
useEffect(() => {;(async () => {try {const items = _.reduce(await Promise.all(feeds.map(async oneFeed => {const { items } = await parser.parseURL(CORS_PROXY + oneFeed.url)return items.map(o => ({ ...o, feed: oneFeed }))})),(sum, n) => [...sum, ...n])setGet(o => ({ ...o, data: items, loading: false }))} catch (error) {setGet(o => ({ ...o, error, loading: false }))}})()}, [feeds])
How this reduce function works is that for each array we will use the ...
spread operator to put all of the elements from the particular array into a large array called sum
which is initially empty.
If we console.log(items)
we will see that it is a single array. Once we have that we can then set data
state equal to that and set the loading
to false. In the event of an error we set the error as the error that occurred.
The sum total of what we are doing here is that we use useEffect
to asynchronously fetch each of the rss feeds and combine them into an array that we store in the state data
.
We'll stop this chapter here and pick up next with actually creating the article list.