Skip to content

davidde/ethblox

Repository files navigation

ΞthBlox

Ethereum Blockexplorer Static Site

This is the Alchemy Ethereum Developer Bootcamp Week 3 project, which I've been using to learn Next.js, and how its client/server components impact static site generation. Typescript and TailwindCSS were a first for me too, so overall it's been a very instructive experience.

The starter code and assignment are available here.
I migrated it to Next.js, bootstrapped with create-next-app.

Most prominent technologies used:

Running the project locally

Important

To make the Alchemy API calls actually work, you will need an Alchemy API key, which you can get freely by creating an Alchemy account. Put the key into an .env file in the root of the project, without quotes:

NEXT_PUBLIC_ALCHEMY_API_KEY=xyz

The NEXT_PUBLIC_ prefix is required to make it accessible to the browser.

  • Clone this repo, and cd into it in your terminal:
    git clone https://github.com/davidde/ethblox; cd ethblox
  • Install the project's packages:
    npm install
  • Then start the development server:
    npm run dev
    This will start the Next.js development server on port 3005 (set in package.json > scripts > dev), so open http://localhost:3005 in the browser to see the result.

Warning

The development server will not fully reflect the production state of the application, since we're using output: export for full static site generation for Github Pages. The hot reload is still useful for quick iteration and feedback, but it is still necessary to frequently check the actual static site output with serve out after building with npm run build (install serve with npm install -g serve). It is also necessary to set the LOCAL_BUILD environment variable before building, otherwise the incorrect basePath will be applied in nextConfig, and all asset files (including CSS!) will 404.

For example, in PowerShell:

$env:LOCAL_BUILD='true'; npm run build; serve out

The cross-env version of the above command is specified for npm run start, so you can just use npm run start to start the production application, regardless of platform. Note that this obviously isn't "live", and will need to be repeated after code changes.

  • Start the "production" static site:
    # Requires the `serve` package:
    # npm install -g serve
    npm run start

Observations on static sites vs server-side rendering

This project was started as a Next.js application with server-side rendering hosted on Vercel, and later converted to a static site with output: export (see some notes on the process). I'm listing some important nuances here for reference:

  • Dynamic routes like [network]/address/[hash]/0x... are not supported on static sites (since it needs to generate an actual .html file for every page):
    • For [network] we can implement generateStaticParams() to statically generate the dynamic route [network] at build time, since we only need mainnet and sepolia network parameters.
    • However, for [hash] this is not possible, since it isn't known at build time, and we can't generate pages for all possible hashes. As a consequence, this dynamic route needs to be switched to use URL parameters like mainnet/address?hash=0x....
    • To make the above URL params work, we need useSearchParams() to read the URL's query string. This in turn requires use client, so we also need to convert those pages to client components. This means removing async from the component, and as a result requires us to move all data fetching into useEffect() hooks.
  • If after all that you manage to get things to compile, you might notice you don't get live data everywhere, some pages simply don't update on reload... This is because those pages haven't been converted to client components yet! When a server component gets compiled as static site, all server-side data fetching gets baked into the output! This means you can refresh as much as you want, the data will always remain the same as what it was when the project was built. So, the solution?
    Convert everything to client components!

DataState Library

Since this project is handling a lot of async data that has to be fetched from API providers, I co-developed the DataState library to make all of this simpler, more intuitive, and without the usual bloat that comes with client-side data fetching.

The library mainly consists of a DataState Algebraic Data Type, which always is in either of 3 states:

  • LoadingState: The data is still being fetched.
  • ErrorState: The fetching process ran into some kind of error. The error field contains the Error object.
  • ValueState: The fetch succeeded, and the resulting data is contained inside the value field of the DataState.

More specifically, the DataState consists of a Root, which contains the actual data fields, and DataStateMethods that extend the Root object into a full DataState.

Here are the Root definitions for reference:

export type LoadingRoot = {
  status: 'loading';
  value: undefined;
  error: undefined | null;
  loading: true;
};
export type ValueRoot<T> = {
  status: 'value';
  value: T;
  error: undefined;
  loading: false;
};
export type ErrorRoot = {
  status: 'error';
  value: undefined;
  error: Error;
  loading: false;
};

export type Root<T> = LoadingRoot | ValueRoot<T> | ErrorRoot;

Usage

A DataState is initialized with useDataState(), which takes a fetching function and its arguments as input. E.g.:

const blockData = useDataState<Block>({
  fetcher: (alchemy, num) => alchemy.core.getBlock(num),
  args: [alchemy, blockNumber],
});

Once initialized, it can be used directly inside the JSX return statement of the component, e.g.:

return (
  <div>
    <span>Recipient of block reward:</span>
    <blockData.Render
      className='w-[11rem]'
      loadingPulseColor='bg-(--link-color)'
    >
      {
        (data) =>
          <PopoverLink
            href={`/${props.network}/address?hash=${data.miner}`}
            content={truncateAddress(data.miner, 20)}
            popover={data.miner}
            className='left-[-37%] top-[-2.6rem] w-78 py-1.5 px-2.5'
          />
      }
    </blockData.Render>
  </div>
);

As shown above, every DataState has a .Render() function that receives the fetched data as input, allowing the use of the fetched data directly inside the JSX code:

  • Obviously, the Render function only renders the actual data when it is present. If not present, it will render either an ErrorIndicator indicating the error that occurred, or a LoadingIndicator or LoadingPulse component when the data is still being fetched.
  • All of this is very configurable; see the RenderConfig type in src/lib/data-state/types/index.ts on line 107. The RenderConfig only consists of optional fields to configure the Render() function's output. This means the <blockData.Render> component's props are all optional, and you can pick and choose only the ones that you need.

About

Ethereum Blockchain Explorer as Static Site using Next.js, Typescript and TailwindCSS

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •