Skip to content

Commit

Permalink
add loader inference (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds authored Jul 14, 2022
1 parent e8d39cf commit 62745f8
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 106 deletions.
16 changes: 4 additions & 12 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
LinksFunction,
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
import type { LinksFunction, LoaderArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
Links,
Expand All @@ -26,15 +22,11 @@ export const meta: MetaFunction = () => ({
viewport: "width=device-width,initial-scale=1",
});

type LoaderData = {
user: Awaited<ReturnType<typeof getUser>>;
};

export const loader: LoaderFunction = async ({ request }) => {
return json<LoaderData>({
export async function loader({ request }: LoaderArgs) {
return json({
user: await getUser(request),
});
};
}

export default function App() {
return (
Expand Down
6 changes: 3 additions & 3 deletions app/routes/healthcheck.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// learn more: https://fly.io/docs/reference/configuration/#services-http_checks
import type { LoaderFunction } from "@remix-run/node";
import type { LoaderArgs } from "@remix-run/node";

import { prisma } from "~/db.server";

export const loader: LoaderFunction = async ({ request }) => {
export async function loader({ request }: LoaderArgs) {
const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");

Expand All @@ -22,4 +22,4 @@ export const loader: LoaderFunction = async ({ request }) => {
console.log("healthcheck ❌", { error });
return new Response("ERROR", { status: 500 });
}
};
}
42 changes: 18 additions & 24 deletions app/routes/join.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
ActionFunction,
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import * as React from "react";
Expand All @@ -12,50 +8,48 @@ import { getUserId, createUserSession } from "~/session.server";
import { createUser, getUserByEmail } from "~/models/user.server";
import { safeRedirect, validateEmail } from "~/utils";

export const loader: LoaderFunction = async ({ request }) => {
export async function loader({ request }: LoaderArgs) {
const userId = await getUserId(request);
if (userId) return redirect("/");
return json({});
};

interface ActionData {
errors: {
email?: string;
password?: string;
};
}

export const action: ActionFunction = async ({ request }) => {
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");

if (!validateEmail(email)) {
return json<ActionData>(
{ errors: { email: "Email is invalid" } },
return json(
{ errors: { email: "Email is invalid", password: null } },
{ status: 400 }
);
}

if (typeof password !== "string" || password.length === 0) {
return json<ActionData>(
{ errors: { password: "Password is required" } },
return json(
{ errors: { email: null, password: "Password is required" } },
{ status: 400 }
);
}

if (password.length < 8) {
return json<ActionData>(
{ errors: { password: "Password is too short" } },
return json(
{ errors: { email: null, password: "Password is too short" } },
{ status: 400 }
);
}

const existingUser = await getUserByEmail(email);
if (existingUser) {
return json<ActionData>(
{ errors: { email: "A user already exists with this email" } },
return json(
{
errors: {
email: "A user already exists with this email",
password: null,
},
},
{ status: 400 }
);
}
Expand All @@ -68,7 +62,7 @@ export const action: ActionFunction = async ({ request }) => {
remember: false,
redirectTo,
});
};
}

export const meta: MetaFunction = () => {
return {
Expand All @@ -79,7 +73,7 @@ export const meta: MetaFunction = () => {
export default function Join() {
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") ?? undefined;
const actionData = useActionData() as ActionData;
const actionData = useActionData<typeof action>();
const emailRef = React.useRef<HTMLInputElement>(null);
const passwordRef = React.useRef<HTMLInputElement>(null);

Expand Down
37 changes: 13 additions & 24 deletions app/routes/login.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
ActionFunction,
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import * as React from "react";
Expand All @@ -11,52 +7,45 @@ import { createUserSession, getUserId } from "~/session.server";
import { verifyLogin } from "~/models/user.server";
import { safeRedirect, validateEmail } from "~/utils";

export const loader: LoaderFunction = async ({ request }) => {
export async function loader({ request }: LoaderArgs) {
const userId = await getUserId(request);
if (userId) return redirect("/");
return json({});
};

interface ActionData {
errors?: {
email?: string;
password?: string;
};
}

export const action: ActionFunction = async ({ request }) => {
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const redirectTo = safeRedirect(formData.get("redirectTo"), "/notes");
const remember = formData.get("remember");

if (!validateEmail(email)) {
return json<ActionData>(
{ errors: { email: "Email is invalid" } },
return json(
{ errors: { email: "Email is invalid", password: null } },
{ status: 400 }
);
}

if (typeof password !== "string" || password.length === 0) {
return json<ActionData>(
{ errors: { password: "Password is required" } },
return json(
{ errors: { email: null, password: "Password is required" } },
{ status: 400 }
);
}

if (password.length < 8) {
return json<ActionData>(
{ errors: { password: "Password is too short" } },
return json(
{ errors: { email: null, password: "Password is too short" } },
{ status: 400 }
);
}

const user = await verifyLogin(email, password);

if (!user) {
return json<ActionData>(
{ errors: { email: "Invalid email or password" } },
return json(
{ errors: { email: "Invalid email or password", password: null } },
{ status: 400 }
);
}
Expand All @@ -67,7 +56,7 @@ export const action: ActionFunction = async ({ request }) => {
remember: remember === "on" ? true : false,
redirectTo,
});
};
}

export const meta: MetaFunction = () => {
return {
Expand All @@ -78,7 +67,7 @@ export const meta: MetaFunction = () => {
export default function LoginPage() {
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") || "/notes";
const actionData = useActionData() as ActionData;
const actionData = useActionData<typeof action>();
const emailRef = React.useRef<HTMLInputElement>(null);
const passwordRef = React.useRef<HTMLInputElement>(null);

Expand Down
10 changes: 5 additions & 5 deletions app/routes/logout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import type { ActionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";

import { logout } from "~/session.server";

export const action: ActionFunction = async ({ request }) => {
export async function action({ request }: ActionArgs) {
return logout(request);
};
}

export const loader: LoaderFunction = async () => {
export async function loader() {
return redirect("/");
};
}
12 changes: 4 additions & 8 deletions app/routes/notes.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import type { LoaderFunction } from "@remix-run/node";
import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react";

import { requireUserId } from "~/session.server";
import { useUser } from "~/utils";
import { getNoteListItems } from "~/models/note.server";

type LoaderData = {
noteListItems: Awaited<ReturnType<typeof getNoteListItems>>;
};

export const loader: LoaderFunction = async ({ request }) => {
export const loader = async ({ request }: LoaderArgs) => {
const userId = await requireUserId(request);
const noteListItems = await getNoteListItems({ userId });
return json<LoaderData>({ noteListItems });
return json({ noteListItems });
};

export default function NotesPage() {
const data = useLoaderData() as LoaderData;
const data = useLoaderData<typeof loader>();
const user = useUser();

return (
Expand Down
18 changes: 7 additions & 11 deletions app/routes/notes/$noteId.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import type { ActionArgs, LoaderArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useCatch, useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
Expand All @@ -7,32 +7,28 @@ import { deleteNote } from "~/models/note.server";
import { getNote } from "~/models/note.server";
import { requireUserId } from "~/session.server";

type LoaderData = {
note: NonNullable<Awaited<ReturnType<typeof getNote>>>;
};

export const loader: LoaderFunction = async ({ request, params }) => {
export async function loader({ request, params }: LoaderArgs) {
const userId = await requireUserId(request);
invariant(params.noteId, "noteId not found");

const note = await getNote({ userId, id: params.noteId });
if (!note) {
throw new Response("Not Found", { status: 404 });
}
return json<LoaderData>({ note });
};
return json({ note });
}

export const action: ActionFunction = async ({ request, params }) => {
export async function action({ request, params }: ActionArgs) {
const userId = await requireUserId(request);
invariant(params.noteId, "noteId not found");

await deleteNote({ userId, id: params.noteId });

return redirect("/notes");
};
}

export default function NoteDetailsPage() {
const data = useLoaderData() as LoaderData;
const data = useLoaderData<typeof loader>();

return (
<div>
Expand Down
23 changes: 8 additions & 15 deletions app/routes/notes/new.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,39 @@
import type { ActionFunction } from "@remix-run/node";
import type { ActionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
import * as React from "react";

import { createNote } from "~/models/note.server";
import { requireUserId } from "~/session.server";

type ActionData = {
errors?: {
title?: string;
body?: string;
};
};

export const action: ActionFunction = async ({ request }) => {
export async function action({ request }: ActionArgs) {
const userId = await requireUserId(request);

const formData = await request.formData();
const title = formData.get("title");
const body = formData.get("body");

if (typeof title !== "string" || title.length === 0) {
return json<ActionData>(
{ errors: { title: "Title is required" } },
return json(
{ errors: { title: "Title is required", body: null } },
{ status: 400 }
);
}

if (typeof body !== "string" || body.length === 0) {
return json<ActionData>(
{ errors: { body: "Body is required" } },
return json(
{ errors: { title: null, body: "Body is required" } },
{ status: 400 }
);
}

const note = await createNote({ title, body, userId });

return redirect(`/notes/${note.id}`);
};
}

export default function NewNotePage() {
const actionData = useActionData() as ActionData;
const actionData = useActionData<typeof action>();
const titleRef = React.useRef<HTMLInputElement>(null);
const bodyRef = React.useRef<HTMLTextAreaElement>(null);

Expand Down
Loading

0 comments on commit 62745f8

Please sign in to comment.