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
    ;<ItemList
    type={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
    ;<ItemList
    type={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>
    <ItemList
    type={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 code
    useEffect(() => {
    ;(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.