Static Site Generation with Next.js and TypeScript (Part VI) - Client-Side Rendering
Disclaimer - Please read the fifth part of this blog post here before proceeding. It covers how to efficiently build a Next.js application with a single access token that can be used across all getStaticProps() and getStaticPath() functions. It also covers how to export a Next.js application to static HTML. If you just want to jump straight into this tutorial, then clone the project repository and install the dependencies. If all rendering happened on the client-side, then you end up with several problems. For example, suppose you build an application with Create React App . If you disable JavaScript in the browser and reload the application, then you will find the page void of content since React cannot run without JavaScript. Therefore, checking the DOM in the developer console, the <div id="root" /> element, where React renders all of the dynamic content, will be shown to be empty. There's also the possibility of the client running on an underpowered device, so rendering might take longer than expected. Or worse, the client has a poor network connection, so the application might have to wait longer for JavaScript bundles and other assets to be fully fetched before being able to render anything to the page. This is why it's important to not blindly render all content with only one rendering strategy. Rather, you should consider taking a hybrid approach when building an application. By having some content pre-rendered in advance via static-site generation (or server-side rendering) and having the remaining content rendered via client-side rendering, you can simultaneously deliver both a highly performant page and an enriching user experience. Thus far, every page of our Next.js application has been pre-rendered: the / and /types/:type pages. The initial HTML of the home page ( / ) contains the markup of the eight pet animal type cards that each allows users to navigate to a list of recently adopted pets. The initial HTML of each type page ( /types/:type ) contains the markup of the list of recently adopted pets. Once the page's initial HTML gets loaded and rendered, the browser hydrates the HTML. This breathes life into the page, giving it the ability to respond to user events and enabling features like client-side navigation and lazy-loading. Below, I'm going to show you how to render dynamic content on the client-side of a Next.js application with the useEffect Hook. When the user visits the /types/:type page, the list of pet animals available for adoption will be fetched and rendered on the client-side. By delegating the rendering of this list to the client-side, we reduce the likelihood of the list being outdated and including a pet that might have been adopted very recently. To get started, clone the project repository and install the dependencies. If you're coming from the fifth part of this tutorial series, then you can continue on from where the fifth part left off. Within the <TypePage /> component, let's define two state variables and their corresponding setter methods: ( pages/types/[type].tsx ) To fetch a list of adoptable pet animals, let's call a useEffect Hook that fetches this list upon the <TypePage /> component mounting to the DOM. The type prop is passed to the dependency array of this Hook because its id property is accessed within the Hook. ( pages/types/[type].tsx ) When you run the Next.js application in development mode ( make dev ) and visit the page http://localhost:3000/types/horse , you will encounter the following error message: Since the PETFINDER_ACCESS_TOKEN environment variable is not prefixed with NEXT_PUBLIC , the PETFINDER_ACCESS_TOKEN environment variable is not exposed to the browser. Therefore, we will need to fetch a new access token for sending requests to the Petfinder API on the client-side. ( pages/types/[type].tsx ) To communicate to users that the list of adoptable pet animals is being fetched from the Petfinder API, let's add a <Loader /> component and show it to the user anytime the page is updating this list. To the right of the animated spinner icon, the <Loader /> component shows a default "Loading ..." message. With the className and children props, you can override the <Loader /> component's styles and the default "Loading..." message respectively. ( components/LoadingSpinner.tsx ) Now let's add a new section to the <TypePage /> component that displays the list of adoptable pet animals. Additionally, let's update the <TypePage /> component accordingly so that the <Loader /> component is shown when the isUpdatingAdoptableListings flag is set to true . ( pages/types/[type].tsx ) When the Next.js application refreshes with these changes, fully reload the page. The list of recently adopted pet animals (above-the-fold content) instantly gets rendered since it was pre-rendered at build time. If you quickly scroll down the page, you can momentarily see the loader just before it disappears and watch as the client renders the fetched list of adoptable pet animals (below-the-fold content). Suppose the page is kept open for some time. The initial list of adoptable pet animals will become outdated. Let's give the user the ability to manually update this list by clicking an "Update Listings" button. ( components/UpdateButton.tsx ) When the user clicks on the button, and the handleOnClick method sets the isUpdating flag to true ... Within the <TypePage /> component, let's move the fetchAdoptableAnimals function out of the useEffect Hook so that it can also be called within the updateAdoptableListing() function, which gets passed to the handleOnClick prop of the <UpdateButton /> component. By wrapping the updateAdoptableListing() function in a useCallback Hook, the function is memoized and only gets recreated when the prop type changes, which should never change at any point during the lifetime of the <TypePage /> component. Therefore, anytime a state variable like adoptableAnimals or isUpdatingAdoptableListings gets updated and causes a re-render of the component, the fetchAdoptableAnimals() function will not be recreated. ( pages/types/[type].tsx ) Upon the initial page load, a simple animated spinner icon with the text "Loading..." is shown to the user as the client fetches for a list of adoptable pet animals. Then, anytime the user clicks on the "Update Listings" button to update the list of adoptable pet animals, an overlay with an animated spinner icon gets placed on top of the previous listings. This preserves the previous listings in case the client cannot update the list of adoptable pet animals (due to a network error, etc.). Let's make a few adjustments to the <TypePage /> component to ensure that the previous listings are kept in state when the client fails to update the list of adoptable pet animals. We add another state variable isUpdateFailed that's set to true only if an error was encountered when fetching an updated list of adoptable pet animals. When isUpdateFailed set to true , a generic error message "Uh Oh! We could not update the listings at this time. Please try again." is shown to the user. ( pages/types/[type].tsx ) If you find yourself stuck at any point during this tutorial, then feel free to check out the project's repository for this part of the tutorial here . Please stay tuned for future parts, which will cover topics like API routes and deployment of the Next.js application to Vercel! If you want to learn more advanced techniques with TypeScript, React and Next.js, then check out our Fullstack React with TypeScript Masterclass :