Max Next – Section 6 – Project Time: Page Pre-Rendering & Data Fetching


Courses

Updated Mar 8th, 2022

Introduction:

Events project. Static Site Generation and Server-Side Rendering and Client-Side data fetching.

Preparations:

Building on project built in a previous section. A good example of using firebase as a backend database that exposes and API we can query, (Amplify is Amazon’s version of firebase). He added all the fields manually from a hard-coded file with dummy data. Images will stay in the public folder for now which is unrealistic.

Adding Static Site Generation (SSG) On The Home Page

The starting page which should be understood by search engine crawlers and we want to see the information immediately. It’s also not user-specific data or behind log in screen. Makes sense to pre-render, so the question here is getServerSideProps or getStaticProps.

Can filter the data you are retrieving with query parameters. See Firebase docs to see how you can adjust the URL.

Add a “helpers/api-utils.js” file to pull all events (via a “getAllEvents” function) then filter using JavaScript, (not the most efficient way but gets the job done):

export async function getAllEvents() {
  const response = await fetch('firebaseURL.json')
  const data = await response.json()
  // skipping error handling here
  const events = []
  for (const key in data) {
    events.push({
      id: key,
      ...data[key]
    })
  }

  return events
  
}

export async function getFeaturedEvents() {
  const allEvents = await getAllEvents()
  return allEvents.filter((event) => {event.isFeatured})
}

Note: Remember that firebase returns an object of nested objects and not an array of nested objects so we have to reformat with a “for in” loop in the code above.

Also Note: Using the spread operator to not have to manually write out all of the key/value pairs.

Now in the “index.js” file in the getStaticProps function we can call the “getFeaturedEvents” function.

import { getFeaturedEvents } from "../helpers/api-util"
import EventList from "../components/events/event-list"

function HomePage(props) {
  return (
    <div>
      <EventList items={props.events} />
    </div>
  ) 
}

export async function getStaticProps () {
  const featuredEvents = await getFeaturedEvents()

  return {
    props: {
      events: featuredEvents
    }
  }
}

This data will now be in the page source code.

Loading Data & Paths For Dynamic Pages

Event Detail Page.

Get from Firebase backend.

Copy getEventById and convert to async function.

Use in the dynamic id page.

More important to SEO than the homepage. Pre-render page with data. Static with getStaticProps or Server-side with getServerSideProps?

function EventDetailPage(props) {
  const event = props.selectedEvent

  if (!event) {
    return (
      <ErrorAlert>No Event Found!</ErrorAlert>
    )
  }

  return (
    <>
    <p>Data About Event {event.title} Here</p>
    </>
  )
}

export async function getStaticProps() {
  const eventId = context.params.eventId

  const event = await getEventById(eventId)

  return {
    props: {
      selectedEvent: event
    }
  }
}

But the code above will not work because it is missing an important step. Because this is a dynamic page and there is an infinite amount of possible pages, Next JS doesn’t know which ids it should generate. As a developer we also may not know that in advance, maybe a dynamic page with user-generated content. We need “getStaticPaths” and fetch the event ids dynamically using the getAllEvents function from the “api-util.js” file.

export async getStaticPaths() {

  const events = await getAllEvents()

  cosnt paths = events.map((event) => ({params: {eventId: event.id}}))

  return {
    paths: paths,
    fallback: false
  }
}

Optimizing Data Fetching

The current implementation works but maybe we can optimize it.

The data only updates if we manually build again. Using “getServerSideProps” to update after every request seems like overkill. We can just add the “revalidate” key side by side to the props key in the “getStaticProps” function, (set to 1800 on the homepage meaning at most once every half hour).

Another optimization would be to just pre-fetch the event details pages for the featured events and set “fallback” to either true or “blocking.” Setting fallback to true requires a loading state and “blocking” has a delay until the page is loaded.

Working on the “All Events” Page

Still want to statically generate using the “getAllEvents” function in the “api-util.js” file.

Add a revalidate option with a value of 60 to ensure we have fresh data.

Page source we have all event data.

Using Server-side Rendering (SSR)

On the slug page, catch all page of “[…slug].js” we could use “getStaticProps” but we will have a lot of combinations of parameters. Pre-generating all filter options if hard because we don’t know what filters are more likely to be searched. Could be a lot of pages.

export async function getServerSideProps(context) {
  const params = context

  cosnt filterData = params.slug

  const filteredYear = filterData[0]
  const filteredMonth = filterData[1]

  const numYear = +filteredYear
  const numMonth = +filteredMonth

  if (
    isNaN(numYear) ||
    isNaN(numMonth) ||
    numYear > 2030 ||
    numMOnth < 2021 ||
    numMonth < 1 ||
    numMonth > 12
  ) {
    // can't show JSX need to return object
    return {
      notFound: true, // show 404 page
     }
  }

  const filteredEvents = await getFilteredEvents({
    year: numYear,
    month: numMonth
  })
  
  return {
    props: {
      events: filteredEvents,
      date: {
        year: numYear,
        month: numMonth
      }
    }
  }
}

In the code above we could also show our own error page.

return {
  notFound: true,
  redirect: {
    destination: "/error"
  }
}

Alternatively we could set a prop of something like “hasErrors” to a value of true and then handle that with some conditional JSX.

if (props.hasError) {
  return (
  <>
    <ErrorAlert/>
  </>
  )
}

Copy the “getFilteredEvents” function into the “api-utils.js” file and tweak.

export async function getFilteredEvents(dateFilter) {
  const {year, month} = dateFilter

  const allEvents = await getAllEvents()

  let filteredEvents = allEvents.filter((event) => {
    const eventDate = new Date(event.date)
    return eventDate.getFullYear() === year & eventDate.getMonth() === month -1
  }) 

  return filteredEvents 
}

We can now reference the events as “props.events”

const filteredEvents = props.events

if (!filteredEvents || filteredEvents.length === 0) {
  return (
    <>
    // code to show warning msg if none found given filter criteria
    </>
  )
}

const date = new Date(props.date.year, props.date.month - 1)

return (
  <>
    // Whatever Event Card Here
  </>
)

Adding Client-Side Data Fetching

Not really needed in this app. But will implement on the filter page anyway just to see how it would work. The page would load a bit quicker as we don’t need to pre-load on the server but the data would initially be missing so we’ll have to show a loading state or spinner until the data is there. The data is not needed for SEO.

The “useSWR” hook is good for client side rendering in Next JS, as supposed to your typical “useEffect.” Need to install package with the “npm install swr” command.

import useSWR from "swr

function FilteredEventsPage(props) {
  const router = useRouter()

  const filterData = router.query.slug

  const { data, error } = useSWR('firebaseURL.json')

  useEffect(() => {
    if (data) {
      const events = []
      for (const key in data) {
        events.push({
          id: key,
          ...data[key]
      })
    }
    // don't return events, register as state
    setLoadedEvents(events)
    }
  }, [data])

  if (!loadedEvents) {
    return <p>Loading...</p>
  }

  // filter year and month code here

  // copy in filter logic from api-utils

  // remove props.hasError

  // check before we try to filter

  // change back from props.numYear

  // remove getServerSideProps
}

Notice you will now not see any data in the page source.