Kent C. Dodds

How I Built a

Modern Website in 2021

Let's wake up

Your brain needs this 🧠

What this talk is

  • A high-level tour of a real-world modern website and the tools used to build it.
  • Genuine

What this talk is not

  • Contrived

Let's
Get
STARTED!

What kentcdodds.com is

General stats

  • 27k lines of code
  • PM, Designer, Illustrator, UI Dev + me + some other contributors (/credits)

Tech overview

Client

Server

// look mah! No useEffect, isLoading, or isError!
export const loader: LoaderFunction = async ({request}) => {
  return json<LoaderData>({
    teamsInOrder: shuffle(teams),
  })
}


export default function NewAccount() {
  const data = useLoaderData<LoaderData>()

  return (
    // some UI stuff...

          <fieldset className="contents">
            <legend className="sr-only">Team</legend>
            {data.teamsInOrder.map(teamOption => (
              <TeamOption
                key={teamOption}
                team={teamOption}
                error={actionData?.errors.team}
                selected={formValues.team === teamOption}
              />
            ))}
          </fieldset>

    // more UI stuff
  )
}
// note filtering out the stuff we don't need and the declarative error handling

type LoaderData = {season: CWKSeason}

export const loader: LoaderFunction = async ({
  params,
  request,
}) => {
  // getSeasonListItems hits the simplecast API and filters out
  // all the extra stuff we don't need for this route
  const seasons = await getSeasonListItems({request})
  const seasonNumber = Number(params.season)
  const season = seasons.find(s => s.seasonNumber === seasonNumber)
  if (!season) {
    throw new Response(`No season for ${params.season}`, {status: 404})
  }

  return json<LoaderData>({season}, {
    headers: {
      'Cache-Control': 'public, max-age=600',
    },
  })
}

export default function ChatsSeason() {
  const {season} = useLoaderData<LoaderData>() // <-- autocompleted
  return <stuff />
}

// handles unexpected errors
export function ErrorBoundary({error}: {error: Error}) {
  console.error(error)
  return <ServerError />
}

// handles our thrown 404 response
export function CatchBoundary() {
  const caught = useCatch()
  const params = useParams()
  console.error('CatchBoundary', caught)
  if (caught.status === 404) {
    return (
      <Grid nested className="mt-3">
        <div className="col-span-full md:col-span-5">
          <H3>{`Season not found`}</H3>
          <Paragraph>{`Are you sure ${
            params.season ? `season ${params.season}` : 'this season'
          } exists?`}</Paragraph>
        </div>
        <div className="md:col-span-start-6 col-span-full md:col-span-5">
          <MissingSomething className="rounded-lg" aspectRatio="3:4" />
        </div>
      </Grid>
    )
  }
  throw new Error(`Unhandled error: ${caught.status}`)
}
  • fetch Request/Response
  • <Form />
  • <link rel="modulepreload" />
  • <link rel="prefetch" as="fetch" />

CSS

import type {LinksFunction} from 'remix'
import aboutStyles from '~/styles/routes/about.css'

export const links: LinksFunction = () => {
  return [{rel: 'stylesheet', href: aboutStyles}]
}

export default function AboutScreen() {
  return <stuff />
}

Client-side server state cache management & Nested Routing

  1. Loaders run in parallel
  2. Remix loads only what's needed
  3. Mutations trigger invalidation of all loaders
  4. Context for shared UI state & Remix for shared server state
  5. No <Layout /> component

=

+

Migrations

  • Version Controlled
  • Auto-generated
  • Escape hatch: Overrideable

TypeScript

// source
const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    firstName: true,
  },
})
// types
const users: Array<{
  id: string
  email: string
  firstName: string
}>
// source
const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    firstName: true,
    team: true, // <-- new field
  },
})
// types
const users: Array<{
  id: string
  email: string
  firstName: string
  team: Team // <-- field appears!
}>
// source
const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    firstName: true,
    team: true,
    postReads: {
      select: {
        postSlug: true,
      },
    },
  },
})
// types
const users: Array<{
  id: string
  email: string
  firstName: string
  team: Team
  postReads: Array<{
    postSlug: string
  }>
}>

You also get:

distinct, include, skip, take, orderBy, where, etc.

All with autocomplete/TypeSafety 🤯

type LoaderData = Awaited<ReturnType<typeof getLoaderData>>

async function getLoaderData() {
  const users = await prisma.user.findMany({
    select: {
      id: true,
      email: true,
      firstName: true,
      team: true,
      postReads: {
        select: {
          postSlug: true,
        },
      },
    },
  })
  return {users}
}

export const loader: LoaderFunction = async ({request}) => {
  return json(await getLoaderData())
}

export default function UsersPage() {
  const data = useLoaderData<LoaderData>()
  return (
    <div>
      <h1>Users</h1>
      <ul>
        {/* all this auto-completes and type checks!! */}
        {data.users.map(user => (
          <li key={user.id}>
            <div>{user.firstName}</div>
          </li>
        ))}
      </ul>
    </div>
  )
}

Local Development

node .
node --require ./mocks .

MSW to the rescue!

Caching

Caching to the rescue!

Image Optimization with Cloudinary   

<img
  {...otherProps}
  src="https://res.cloudinary.com/kentcdodds-com/image/upload/w_1517,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80"
  srcset="
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_280,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80   280w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_560,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80   560w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_840,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80   840w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_1100,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 1100w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_1650,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 1650w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_2500,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 2500w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_2100,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 2100w,
    https://res.cloudinary.com/kentcdodds-com/image/upload/w_3100,q_auto,f_auto,b_rgb:e6e9ee/unsplash/photo-1459262838948-3e2de6c1ec80 3100w
  "
  sizes="
    (max-width:1023px) 80vw,
    (min-width:1024px) and (max-width:1620px) 67vw,
    1100px
  "
/>
---
title: Don't Solve Problems, Eliminate Them
date: 2021-05-11
description:
  How eliminating problems can drastically simplify your codebases and life
categories:
  - productivity
meta:
  keywords:
    - react
    - remix
bannerCloudinaryId: unsplash/photo-1459262838948-3e2de6c1ec80
bannerCredit: Photo by [Jordan Whitt](https://unsplash.com/photos/EerxztHCjM8)
bannerAlt: koala sleeping on tree branch 
---

Humans are natural problem solvers. The fact that we've survived as long as a
species as we have is evidence of that.

... etc.
<meta
  name="twitter:image"
  content="
    https://res.cloudinary.com/kentcdodds-com/image/upload
    /$th_1256,$tw_2400,$gw_$tw_div_24,$gh_$th_div_12
    /co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_14,h_$gh,x_$gw_mul_1.5,y_$gh_mul_1.3,l_text:kentcdodds.com:Matter-Regular.woff2_50:Checkout%2520this%2520article
    /co_white,c_fit,g_north_west,w_$gw_mul_13.5,h_$gh_mul_7,x_$gw_mul_1.5,y_$gh_mul_2.3,l_text:kentcdodds.com:Matter-Regular.woff2_110:Don't%2520Solve%2520Problems%252C%2520Eliminate%2520Them
    /c_fit,g_north_west,r_max,w_$gw_mul_4,h_$gh_mul_3,x_$gw,y_$gh_mul_8,l_kent:profile-transparent
    /co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_5.5,h_$gh_mul_4,x_$gw_mul_4.5,y_$gh_mul_9,l_text:kentcdodds.com:Matter-Regular.woff2_70:Kent%20C.%20Dodds
    /co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_9,x_$gw_mul_4.5,y_$gh_mul_9.8,l_text:kentcdodds.com:Matter-Regular.woff2_40:kentcdodds.com%252Fblog
    /c_fill,ar_3:4,r_12,g_east,h_$gh_mul_10,x_$gw,l_unsplash:photo-1459262838948-3e2de6c1ec80
    /c_fill,w_$tw,h_$th/kentcdodds.com/social-background.png
  "
/>

Architecture overview

📖 kcd.im/2021-site

📖 kcd.im/2021-site

Authentication ✨

Caching Implementation

// here's an example of the cachified credits.yml
// that powers the /credits page:
async function getPeople({
  request,
  forceFresh,
}: {
  request?: Request
  forceFresh?: boolean
}) {
  const allPeople = await cachified({
    cache: redisCache,
    key: 'content:data:credits.yml',
    request,
    forceFresh,
    maxAge: 1000 * 60 * 60 * 24 * 30,
    getFreshValue: async () => {
      const creditsString = await downloadFile(
        'content/data/credits.yml',
      )
      const rawCredits = YAML.parse(creditsString)
      if (!Array.isArray(rawCredits)) {
        console.error('Credits is not an array', rawCredits)
        throw new Error('Credits is not an array.')
      }

      return rawCredits.map(mapPerson).filter(typedBoolean)
    },
    checkValue: (value: unknown) => Array.isArray(value),
  })
  return allPeople
}

📖 kcd.im/2021-site

Thank you!

📖 kcd.im/2021-site

How I Built a Modern Website in 2021

By Kent C. Dodds

How I Built a Modern Website in 2021

I just released a complete rewrite of my website. This isn't your regular developer blog though. You can actually log in, record some audio for a podcast, choose a "team" to be a part of, connect with discord, and much more. I'm using some of the coolest modern tech around including React, Remix, Prisma, Postgres, Redis, Fly.io, tailwind, TypeScript, and more! I want to take you on a tour of some of the highlights and talk about some of the problems I faced and decisions I had to make while building a brand new modern website in 2021.

  • 4,325