Data fetching caching and rendering - vonschappler/Ultimate-React GitHub Wiki

Setting up Supabase

Since we already have created our project for "The Wild Oasis" company with a database setup on supabase, what we need to do initially is to then install the supabase package on our project and create our supabase connector file (supabase.js) inside our _lib folder:

npm install @supabase/supabase-js
// supabase.js
import { createClient } from '@supabase/supabase-js';

const URL = process.env.SUPABASE_URL;
const KEY = process.env.SUPABASE_KEY;

export const supabase = createClient(URL, KEY);

Note that this time around we are using enviromental variables, so we nee to define the variables in a .env.locals file, on the root of the project, with the following structure:

# .env.local
SUPABASE_URL=your_supabase_url
SUPABASE_SERVICE_ROLE=your_supabase_service_role

Keep in mind that with Next.js built-in support to enviromental variables, in case we need any public enviromental variable to be passed from web server to web client, we do that by defining the variable such as:

# .env.local
NEXT_PUBLIC_VARIABLE_NAME=variable_value

NOTE:

For this part of the project we are using the the service_role secret, so that way we don't need to set up new RLS for our tables, while keeping our data still secure and providing access to the data stored where it's required.


Fetching the data to display

After creating the connection with our application and supabase, it's now time to fetch some data into our server components.

Server components allow us to use async/await right at the top level of the code, meaning that we can easyly make use of async functions to fetch the required data.

Inside our _lib folder we create all the logic using supabase-js API to fetch the data (a similar logic to the one used in the internal hotel manangmement) and then we can simply (for example) make use of this code inside a server component which will receive the data, like so:

// server_component_with_data.js

//other imports
import { getData } from '@/app/_lib/services';

// other definitions

export default async function Component() {
  const data = await getData();

  return (
      {cabins.length > 0 && (
        <div className='grid sm:grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12 xl:gap-14'>
          {data.map((item) => (
            <div key={item.unique_prop} >
            {/* do something with item*/}
            </div>
          ))}
        </div>
      )}

  );
}

Since sometimes we may need to display images coming from a external source (like from a api fetch, which in this case it's the api from supabase) we need to edit the next.config.mjs file, so images can be optmized using the Image componented provided by Next.js, like so:

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'supabase_url',
        port: '',
        pathname: 'bucket_path/**',
      },
    ],
  },
};

export default nextConfig;

NOTE:

That procedure would be the same for any iamge source which is not self hosted, so the properties of the object above need to reflect the place from where the resource images are being fetched, if those are being fetched externally.


Data streaming inside Route segments

Well, as we have doing so far, if we want to create a loading inside a route segment, we should follow Next.js conventions and add create a loading.js file right inside the root of that route folder (segment).

By doing so, this can help us in the process of data streaming for specific routes which require data fetching.

Keep in mind that the simple fact of adding the loading.js file to any route (or subroute) enables automatically data streaming, meaning that if the web client does not have javascript enabled, the site wont work on their computer.

What is React Suspense Component?

Suspense is a built-in React component used to isolate components or subtrees of components whic are not ready to be rendered.

We can understand this component as a try/catch block, which in essence, instead of errors, will catch suspending components. A component can be set as a suspending component for tow main reasons:

  • Fetching data with a library that supports Suspense
  • Loading additional code with React's lazy loading

The way it works is by wrapping the component (or subtree of components when the components dependes on the same data which is being fetched) into the Suspense component provided by React.

This allows we to use a native way to support asynchronous operations in a declarative way, no more requiring isLoading states and other render logic.

But how does it work?

  1. While rendering, if a suspending component is found, React moves back to the closest parent Suspense component and discard any already rendered children
  2. So, a fallback component/JSX (usually a spinner indicator) is rendered on screen, while any async operation is being processed
  3. After all the async operations are concluded, React then renders the subtree under what we call Suspense boundary

NOTE:

Components do NOT automatically suspende just for having async operations inside them and integrating this functionality manually can be a bit hard, so we use libraries like React Query or Next.js for those scenarios.

Using Suspense to stream data

In order to use the Suspense to stream data inside a component, all that is really required is moving all the data feching logic and rendering into it's own component and then wrapping that inside React's Suspense component.

By doing so, we can have a better control of the parts of the UI which will be rendered instantly (for not depending on any data) and which parts wont.

As an example, lets create fictional snippets of code to display the usage of the Suspense component:

// loading.js

export default function Loading() {
  return <p>Loading the requested data...</p>;
}
// DataFetching.js

export default async function DataFetching() {
  const data = await fetch('path/to/api/endpoint');

  if (!data.length) return null;

  return (
    <div>
      {data.map((item) => (
        <div key={item.unique_property}>{/* do something with item*/}</div>
      ))}
    </div>
  );
}
// page.js

import Loading from 'path/to/loading.js';
import DataFetching from 'path/to/Datafetching';

export default function Page() {
  return (
    <div>
      <h1>This will be shown instatly</h1>
      <Suspense fallback={<Loading />}>
        <DataFetching />
      </Suspense>
    </div>
  );
}

Working with dynamic routes and dynamic metadata

To create dynamic routes in Next.js we need to follow yet another convention. This convention stated that dynamic routes should be named with [], so the dynamic route the route posts/postid should be created as a the subfolder [postid] inside the posts folder, with a page.js file inside it.

Adding the dynamic metadata

If we wish to empower our dynamic routes with dynamic metadata for each route, we can make use of a async function which MUST be namesgenerateMetadata, exporting it just as we do with static metadata.

The snippet of code below shows how to implement this approach:

// imports

// note that the main Page() and generateMetadata have access to the params property as well as to searchParams
export async function generateMetadata({ params }) {
  const { postTitle } = await fetch(`path/to/posts/${params.postId}`);
  return { title: postTitle };
}

export default async function Page({ params }) {
  const post = await fetch(`path/to/posts/${params.postId}`);
  // page code to render the post
}

Handling errors

Rendering errors

Within Next.js conventions, we have yet a new one, and this is related to error boundaries. In order to treat render errors gracefully, with a nice user interface, all that we need to do is to follow all known conventions which means:

  1. The global error boundary should be a file inside the root of the application named error.js
  2. If we want to display different error pages for each route, then we should add an error.js file to each route.

This error file has to be an client component with the error page to be rendered:

"use client"

// this functional component has access to the error object and to a reset function
export default function Error() {
  return (
      <h1>Something went wrong!</h1>
      <p>{error.message}</p>
      <button onClick={reset}>
        Try again
      </button>
  );
}

We need to remember two things:

  1. Error boundaries ONLY catch rendering errors

  2. The file error.js when placed in the root directory of the application does not catch errors that may happen in other files also placed in the root of the application.

    A solution to that is then to create a new file, following (yet) another convention, named global_error.js.

    Keep in mind though that this global error needs it's own layout, because it will overwrite the whole application, which means it needs to return the html and body tags present on the file layout.js.

Requests errors

One of the most common request errors happens when the user tries to navigate to a undefined route. Next.js already handle this error, but we could want to create our own error page for those situations. This is where the file not-found.js file comes, following all the other conventions that we already know.


Static and Dynamic SSR

Because Next.js is a React framework, all the rendering happens in React, as already discussed. We also know that both types of component (server and client) are rendered on the web server during the initial render. So, then comes the question - how does Next.js makes part of this proccess?

Well, what Nexj.js does is simply spliting different routes of the same application, so we can specify which strategy will be used to render each route. Routes can either be static (pre-rendered routes) or dynamic. There is also a way to "mix both strategies", which is done by a process called Partial Pre-Rendering (PPR).

Static Rendering

  1. The HTML markup is generated at built time or when we are re-fetching data (the render is triggered by the developer)
  2. This is useful when the data of the route doesn't change frequently and/or it's not personalized to the user like for example a products showcase
  3. This is de default render strategy used by Next.js even when a page or component needs to fetch data
  4. When deployed to Vercel, each static route is automatically hosted on a CDN (content develivery network)
  5. If all routes on the application are static, the entire application can be exported as a static site (SSG)

Dynamic Rendering

  1. The HTML markup is generated at request time, based on requests made to the server (the render is triggered by the user interaction)
  2. This is useful when the data changes frequently or it's personalized by the user, like in a shopping cart
  3. This is also useful when a route requires information that depends on the request made, like for example making use of search params
  4. A route automatically switches to dynanic under specific conditions:
    1. The route has dynamic segments (page uses params)
    2. The route has queries on it (page uses searchParams)
    3. If headers() or cookies() are used on route's server components
    4. An uncached data request is made to any route in the server components
    5. If "forced" by the developer using any of the methods described on the documentation
  5. When deployed to Vercel, each dynamic route becomes a serveless function

About termonilogy

  • Content develivery network (CDN) - a network of server located all around the worls that caches and delivers websites static content, from as close as possible to each user
  • Serveless computing - makes possible to run server code without a the devs managing the server themselves, in which single funcions (serverless functions) are run per request, in which the server is started ONLY during excecution time (dynamic routes are always serverless functions)
  • Edge - this means "as close as possible to the user". Keep in mind that there is also serverless edge computing and devs can select which routes will run on the "edge" when deploying to Vercel
  • Incremental static regeneration - a feature from Next.js which allows developers to update the conted of a static page (in the back ground), after a give time, even after a static page was deployed, by re-fetching data to it

Analizing rendering strategies from an application

In other to analize the rendering strategies from an application, we first need to build that application using the provided build script in our package.json file and start serving the application, using (generaly) the following commands:

# builds the application
npm run build

Running that command, this will display on the terminal the results of building our application and at the end of the proccess, it will display which pages are rendered statically or dynamicaly.

Converting Dynamic routes into Static routes

There is a way into converting a Dynamic route into Static routes if necessary. This can be done by using the function generateStaticParams provided by Next.js, as shown below, inside the page.js file which we wish to convert from dynamic to static page:

export async function generateStaticParams() {
  const data = await fetch('path/to/api/endpoint');
  const pages = data.map((item) => {
    {
      dinamic_route_name: dinamic_value_from_params;
    }
  });

  return pages;
}

Static Site Generation

Exporting a Static Site in Next.js, allows us to host that site on any host service that supports static pages hosting. In other to do that, first we need to change our next.config.mjs file, adding to the definitions the folloing code:

output: "export"

and then we can run the command below on the terminal:

npm run build

This process will create a new folder, by default called out with the generated files on your project directory, ready to be depolyed.

There is a ceavat though - images on this case will not work, if we are optmizing them with Next.js Image component. For more information about this, check the documentation

Partial Pre-rendering

Even though so far we are working with 100% dynamic render or 100% static render, this is not always the case.

Rendering for example the Home page of a site dyanmically just because the navigation bar uses some dinamic data, like for example the logged in user, is a waste of resources.

So as a solution to this is the creation of a new rendering strategy: Partial Pre-rendering, whic combines both strategies (static and dynamic) on the same route.

Keep in mind though, that PPR is still a highly experimental feature in Nex.js v14 - which means it's not recommended to be used as part of the production environment. Belo we have is a quick overview on how to use PPR (for testing purposes):

  • PPR needs to be turned on in the next.config.js file
  • By default, as much as possible of any route will be statically rendered, creating a static shell, which holes prepared to receive the dynamic content
  • Those dynamic parts should be placed inside suspense boundaries
  • We provide a static fallback to be shown in those holes, while the dynamic part is being rendered
  • As the dynamic part are being rendered, they'll be streamed to the web-client and then they'll be inserted into the static shell

Data Caching

Caching essentially means storing fetched or computed data in a temporary location for future access, without being necessary any new fetching or computation.

Next.js caching mechanism is very aggresive, once it will cache everything that can be cached. Next.js provides a set of APIs for cache revalidation, which removes old data from the cache while updating it with new fresh data.

Even being too agressive, caching is the main mechanism which turns apps more performant, while saving costs, on both on computing and accessing data. Keep in mind that not all caches can be turned off and that managing cache with Next.js can be very confusing, due the number of many different APIs which affect and controls caching.

Next.js caching mechanisms

NOTE:

The behaviors described below just happen in prodution, since during development there is no caching.

Request memoization Data cache Full Route cache Router cache
Where it's stored? Server Server Server Client
What data it stores? Data fetched with similar GET requests Data fetched in a route or a single fetch request Entire static pages (HTML and RSC payload) All the pre-fetched and visited pages
For how long? One page request Indefinetly (until revalidated) Until the data cace is invalidated 30s if dynamic and 5min if static (per user session) and there is no way to revalidade this cache
What this enables? No need to fetch at the top of the tree - the same fetch on multiple components makes a single request Data for static pages + ISR when revalidated Static pages working the way they do SPA-like navigations
How to revalidate? N/A
  • Time-based (automatically for all data in page), by exporting
    export const revalidate = <time>
    inside any page.js file
  • Time-based (automatically for a single data request), by exporting
    fetch('...', {next: {revalidate: <time>}})
    inside any page.js file
  • On demand (manual) with revalidatePath or revalidateTag
Same as data cache
  • Using revalidatePath or revalidadeTag inside Server Actions
  • Forcing a hard refresh with router.refresh
  • By setting or deleting cookies inside Server Actions
How to opt out? AbortController within the fetch systems
  • Entire page:
    export const revalidate = 0
  • Entire page:
    export const dynamic = 'force-dynamic'
  • Single request:
    fetch('...', {cache: 'no-store'})
  • Individual component:
    noStore()
Same as data cache N/A
⚠️ **GitHub.com Fallback** ⚠️