Quotes Project


Next.js

Updated Aug 23rd, 2021

Need an isLoading state with spinner icon for requests since the heroku server sleeps.

Not a fan of this because when it is not sleeping it glitches out. We should ping API as soon as the page loads to get started even before button is clicked. Or tweak the isLoading to turn off after a request has come in.

GSSP complicates things because can’t use “useEffect”?

Why get ServerSideProps instead of getStaticProps on the home page? Since there is no protection on the page, I don’t think it matters, and GSP could definitely be used instead of GSP.

Why Is DB Interaction So Slow with Internal API on the Dev Server?

The internal API with calls to the DB in GSSP is running very slow. So much so I decided to build an external API in Express. After more thought, the “dev server” mode results may be drastically different once hosted live. Heroku could be much slower and Next JS much faster once the application is actually deployed. For this reason I should build an app in each environment and see how they perform hosted on live servers.

Re-fetching the GSSP without reloading the page is also funky and although I found the “useRouter trick,” via “router.replace(router.asPath)” I’m not sure this is the best option. On Admin Load All Quotes, or partial like the first 25?

This is not a big deal now but it seems like pulling all could be very inefficient as the database grows. You could browse a page of 25 at a time and/or bring in a search feature. Use start and end timers for the DB requests to see how long it takes to grab.

Store Quotes from DB in State?

More for the admin page (given an external API pulling one quote at a time from the DB on the home page). If displaying all of the quotes on the admin page, taking a trip to the DB after every Create, Edit, Delete operation seems very inefficient. You could store in state on the initial pull and then tweak state with (concat, filter, etc.) to update until the next hard page reload.

Detail Page or No Detail Page

Each quote gets it’s own id in the database. This is how is it identified for read/update/delete operations. If using an API, whether internal or external this doesn’t have to show up in the URL so there isn’t a need for a detail page.

If you are going with a static build, in order to tell Next.js which pages to build, via “getStaticPaths,” each quote would end up being it’s own dynamic URL, and we get major speed and SEO benefits. There would be now way to put these pages behind an “auth-wall.” But you still could keep the admin page behind an “auth-wall” since we would have the ability to use “getServerSideProps” specifically for that admin page.

Going Static Build

As long as there’s under 10,000 quotes you could just use GSP with revalidation to statically generate to get lightning fast and auto-rebuild with any changes. You could even go full next/export. The point is to build this for a non-dev, builds happen automatically after any crud change.

Going Full Next/Export

Cannot go full Next/Export without using external API and Skipping GSP. Test this out using CSR in Next and/or CRA. Lightning fast is good for the portfolio site.

When Taking a Trip to the Database (or an external API), Grab One or More Quotes?

Seems like you may want to grab more than one. Use start and end timers for the DB requests to see how long it takes to grab.

Find Versus $sample

Is grabbing all and running my own logic for the random pull faster than having the DB do the random pull? Try the “find random” functionality on my end? Use start and end timers for the DB requests to see how long it takes to grab.

Cleaning Up Mongo “ObjectId”

Error serializing data. Using “.find().toArray()” gives a an array of documents but you will get an error from Next. So we need to clean up. Relates to mongo ID which is a more complex object. Why did we have to do this in Next but not in the API?

Going CRA

Once I have the external API built, I could built a front-end in CRA, using all client side rendering, which definitely has a full static build deployment that could be super fast.

Oh WordPress

How much is easier is it to just use WordPress with Custom Post Types for this? I do this with every project and the doubt never helps so disregard for now.

Issue with Local Storage

I have been trying to update state with “localStorage” and I keep getting an error in Next. This is due to the local storage not being defined when next is running on the server. Running the code in a “useEffect” worked like a charm.

Why Even Bother with GSSP or GSP

So I realized before deploying that the only page using a Next.JS data-fetching function is the home page. If I refactor this I am able to use “Next Export” and can deploy as a static site anywhere, including a subdomain like quotes/brandonvisokay.com. This saves me the trouble of needing a custom domain name or having a random next.js string.

If you ever want to go back this was the GSSP code

import Head from "next/head"
import { useRouter } from "next/router"
import styles from "../styles/Home.module.css"
export default function Home(props) {
  const router = useRouter()
  function newQuoteHandler() {
    router.replace(router.asPath)
  }
  return (
    <div className={styles.container}>
      <Head>
        <title>Quotes</title>
        <meta name="description" content="We Love A Good Quote" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.maincontain}>
        <h2 className={styles.description}>Welcome to Quotes</h2>
        <button onClick={newQuoteHandler} className={styles.quoteBtn}>
          Next Quote
        </button>
        <p className={styles.quote}>{props.data.quote}</p>
        <p className={styles.author}>-{props.data.source}</p>
      </div>
      <footer className={styles.footer}></footer>
    </div>
  )
}
export async function getServerSideProps() {
  const result = await fetch("http://localhost:8080/random-quote")
  const data = await result.json()
  if (!data) {
    return {
      notFound: true
    }
  }
  return {
    props: { data }
  }
}

Default Quote

I initially created two useEffects, one when the component first runs, and the other when the “next quote” button is clicked. I also set some default state to be “loading.” By why not have a default quote? The problem with this is that you can’t read it, because it only flashes for a second while the http request finishes loading. So we only need one “useEffect” and so I am removing this code below, kept here for reference:

  /* Use Effect Triggered When Component First Loads */
  useEffect(() => {
    // create variable that identifies axios cancel token
    const ourRequest = Axios.CancelToken.source()
    async function fetchFirstPost() {
      // create try catch block
      try {
        const response = await Axios.get(`http://localhost:8080/random-quote`, { cancelToken: ourRequest.token })
        // if the delete request was a success, remove the global quotes state
        if (response.data) {
          console.log(response.data)
          setRandomQuote(response.data.quote)
          setRandomSource(response.data.source)
          //create a random quote object to reference in the JSX
        } else {
          console.log("Failed to Fetch First Quote. " + response.data)
        }
      } catch (err) {
        // in the catch show a warning message to the console
        console.log("There was an error or the request was cancelled." + err)
      }
    }
    // immediately call function
    fetchFirstPost()
    // cleanup function
    return () => ourRequest.cancel()
  }, [])

Nitty Gritty Details

When a new quote, (or item or whatever is added to the list I like it to be at the top). Like blog posts I want to see in reverse chronological order.

quotesCtx.setLoadedQuotes([response.data, ...quotesCtx.loadedQuotes])

To add it to the bottom use this code:

quotesCtx.setLoadedQuotes(prevState => {
      return prevState.concat(response.data)
    })

When trying to inspect my quotes I kept seeing the following

[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]

Yet they were being stored in state as the actual values, “[{…}, {…}, {…}].”Stack overflow said it means you are alerting an instance of an object. When alerting the object, toString() is called on the object, and the default implementation returns [object Object]. If you want to inspect the object, you should either console.log it, JSON.stringify() it, or enumerate over it’s properties and inspect them individually using for in.

The Headache

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    at QuoteItem (http://localhost:3000/_next/static/chunks/pages/admin.js?ts=1628249769694:1553:66)

Watching the “React for the Rest of Us” course it is clear that this likely needs a cleanup (or teardown) function. Implementing this worked.

Removing the API – Saved Code for Reference

import { MongoClient } from "mongodb"
// /api/quotes
// data to send: quote and author
// add error handling with try/catch
async function handler(req, res) {
  if (req.method === "POST") {
    const data = req.body
    const client = await MongoClient.connect("mongodb+srv://homerfrom:thesimpsons@cluster0.rn5hp.mongodb.net/quotes?retryWrites=true&w=majority")
    const db = client.db()
    const quotesCollection = db.collection("quotes")
    const result = await quotesCollection.insertOne(data)
    console.log(result)
    client.close()
    res.status(201).json({ message: "Quote inserted..." })
  }
  if (req.method === "GET") {
    const client = await MongoClient.connect("mongodb+srv://homerfrom:thesimpsons@cluster0.rn5hp.mongodb.net/quotes?retryWrites=true&w=majority")
    const db = client.db()
    const quotesCollection = db.collection("quotes")
    const result = await quotesCollection.find().toArray()
    console.log(result)
    client.close()
    res.status(201).json(result)
  }
}
export default handler

Top