Kent C. Dodds
Your brian needs this 🧠
Client
Server
type LoaderData = Await<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>
)
}
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 />
}
node .
node --require ./mocks .
MSW to the rescue!
Caching to the rescue!
// 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
}
type CacheMetadata = {
createdTime: number
maxAge: number | null
}
type VNUP<Value> =
| Value
| null
| undefined
| Promise<Value | null | undefined>
async function cachified<
Value,
Cache extends {
name: string
get: (key: string) => VNUP<{
metadata: CacheMetadata
value: Value
}>
set: (
key: string,
value: {
metadata: CacheMetadata
value: Value
},
) => unknown | Promise<unknown>
del: (key: string) => unknown | Promise<unknown>
},
>(options: {
key: string
cache: Cache
getFreshValue: () => Promise<Value>
checkValue?: (value: Value) => boolean | string
forceFresh?: boolean
request?: Request
fallbackToCache?: boolean
timings?: Timings
timingType?: string
maxAge?: number
}): Promise<Value> {
// do the stuff
}
<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
"
/>
<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
"
/>
=
+
-- prisma migrate dev --name user_roles
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'MEMBER');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "Role" DEFAULT E'MEMBER';
-- Manually written stuff:
-- Update all users to be members:
update "User" set role = E'MEMBER';
-- update me@kentcdodds.com to be ADMIN:
update "User" set role = E'ADMIN' where email = 'me@kentcdodds.com';
-- make role required
ALTER TABLE "User" ALTER COLUMN "role" SET NOT NULL;
-- prisma migrate dev --name init
-- CreateEnum
CREATE TYPE "Team" AS ENUM ('BLUE', 'RED', 'YELLOW');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"email" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"team" "Team" NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"expirationDate" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Call" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"base64" TEXT NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PostRead" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"postSlug" TEXT NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Session" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Call" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostRead" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- prisma migrate dev --name call_kent_episode_id
-- AlterTable
ALTER TABLE "Call" ADD COLUMN "episodeId" TEXT;
-- prisma migrate dev --name remove_episode_id_and_add_keywords
/*
Warnings:
- You are about to drop the column `episodeId` on the `Call` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Call" DROP COLUMN "episodeId",
ADD COLUMN "keywords" TEXT DEFAULT E'', -- first autofill all keywords with an empty string
ALTER COLUMN "keywords" SET NOT NULL; -- then set it to not null
// 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 🤯