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
- Loaders run in parallel
- Remix loads only what's needed
- Mutations trigger invalidation of all loaders
- Context for shared UI state & Remix for shared server state
- 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,232