From b0760c87c27f97a1452733b3fad5f882c7c24967 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Fri, 11 Aug 2023 11:53:58 -0600 Subject: [PATCH] docs updates --- docs/file-conventions/routes-files.md | 1 - docs/guides/styling.md | 1 - docs/index.md | 6 +- docs/pages/api-development-strategy.md | 1 - docs/pages/v2.md | 4 +- docs/route/error-boundary.md | 1 - docs/route/meta-v2.md | 11 - docs/route/meta.md | 1 - docs/tutorials/blog.md | 3 +- docs/tutorials/contacts.md | 1453 ++++++++++++++++++++++++ 10 files changed, 1460 insertions(+), 22 deletions(-) delete mode 100644 docs/route/meta-v2.md create mode 100644 docs/tutorials/contacts.md diff --git a/docs/file-conventions/routes-files.md b/docs/file-conventions/routes-files.md index 3e929dde060..b0ea354534a 100644 --- a/docs/file-conventions/routes-files.md +++ b/docs/file-conventions/routes-files.md @@ -1,6 +1,5 @@ --- title: Route File Naming -new: true --- # Route File Naming diff --git a/docs/guides/styling.md b/docs/guides/styling.md index a6799aafeaf..57ac311cbb2 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -1,6 +1,5 @@ --- title: Styling -new: true --- # Styling diff --git a/docs/index.md b/docs/index.md index 12601028b25..bc2471272d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,10 +21,10 @@ npx create-remix@latest

If you're wondering what Remix is, this is the page for you. Learn how Remix is four primary things: a compiler, an HTTP handler, a server framework, and a browser framework.

- + -

Preparing for v2

-

Remix v2 is coming soon! All of the new APIs and behaviors are available already in v1 behind Future Flags. This doc will help you incrementally adopt each one so that when it's time to update to v2, you don't have to change a thing.

+

Upgrading to v2

+

Remix v2 is here! All of the new APIs and behaviors are available already in v1 behind Future Flags. This doc will help you incrementally adopt each one so that when it's time to update to v2, you don't have to change a thing.

diff --git a/docs/pages/api-development-strategy.md b/docs/pages/api-development-strategy.md index ebd28500d03..25cbdc29b88 100644 --- a/docs/pages/api-development-strategy.md +++ b/docs/pages/api-development-strategy.md @@ -1,7 +1,6 @@ --- title: API Development Strategy description: Remix's strategy to provide a smooth upgrade experience for application developers -new: true --- # API Development Strategy diff --git a/docs/pages/v2.md b/docs/pages/v2.md index 4dddfe485ae..75b6cc5bb46 100644 --- a/docs/pages/v2.md +++ b/docs/pages/v2.md @@ -1,10 +1,10 @@ --- -title: Preparing for v2 +title: Upgrading to v2 order: 1 new: true --- -# Preparing for v2 +# Upgrading to v2 All v2 APIs and behaviors are available in v1 with [Future Flags][future-flags]. They can be enabled one at a time to avoid development disruption of your project. After you have enabled all flags, upgrading to v2 should be a non-breaking upgrade. diff --git a/docs/route/error-boundary.md b/docs/route/error-boundary.md index 3f35fb66676..0a0f9a284bf 100644 --- a/docs/route/error-boundary.md +++ b/docs/route/error-boundary.md @@ -1,6 +1,5 @@ --- title: ErrorBoundary -new: true --- # `ErrorBoundary` diff --git a/docs/route/meta-v2.md b/docs/route/meta-v2.md deleted file mode 100644 index 0e530061cbb..00000000000 --- a/docs/route/meta-v2.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: meta (v2) -toc: false -hidden: true ---- - -# `meta` (v2) - -[The v2 meta API has now been stabilized.][moved] - -[moved]: ./meta diff --git a/docs/route/meta.md b/docs/route/meta.md index 1ca7664a8fd..2ba906ac49d 100644 --- a/docs/route/meta.md +++ b/docs/route/meta.md @@ -1,6 +1,5 @@ --- title: meta -new: true --- # `meta` diff --git a/docs/tutorials/blog.md b/docs/tutorials/blog.md index c6fa7d27a2c..37a73313d7e 100644 --- a/docs/tutorials/blog.md +++ b/docs/tutorials/blog.md @@ -1,9 +1,10 @@ --- title: Blog Tutorial (short) order: 3 +hidden: true --- -# Quickstart +# Blog Tutorial We're going to be short on words and quick on code in this quickstart. If you're looking to see what Remix is all about in 15 minutes, this is it. diff --git a/docs/tutorials/contacts.md b/docs/tutorials/contacts.md new file mode 100644 index 00000000000..4e6818d29c1 --- /dev/null +++ b/docs/tutorials/contacts.md @@ -0,0 +1,1453 @@ +--- +title: Simple App (30m) +order: 1 +--- + +# Simple App Tutorial + +We'll be building a small, but feature-rich app that lets you keep track of your contacts. There's no database or other "production ready" things so we can stay focused on Remix. We expect it to take about 30m if you're following along, otherwise it's a quick read. Check the other tutorials for more in-depth examples. + + + +👉 **Every time you see this it means you need to do something in the app!** + +The rest is just there for your information and deeper understanding. Let's get to it. + +## Setup + +👉 **Generate a basic template** + +```shellscript nonumber +npx create-remix@latest --template ryanflorence/remix-tutorial-template +``` + +This uses a pretty bare-bones template but includes our css and data model so we can focus on Remix. The [Quick Start][quickstart] can familiarize you with the basic setup of a Remix project if you'd like to learn more. + +👉 **Start the app** + +```shellscript nonumber +# cd into the app directory +cd {wherever you put the app} + +# install dependencies if you haven't already +npm install + +# start the server +npm run dev +``` + +You should now see an unstyled screen that looks like this: + + + +## The Root Route + +Note the file at `app/root.tsx`. This is what we call the "Root Route". It's the first component in the UI that renders, so it typically contains the global layout for the page. + +
+ +Expand here to see the root component code + +```jsx filename=src/routes/root.jsx +import { + Links, + LiveReload, + Meta, + Scripts, + ScrollRestoration, + Form, +} from "@remix-run/react"; + +export default function Root() { + return ( + + + + + + + + +
+ +## Adding Stylesheets with `links` + +While there are multiple ways to style your Remix app, we're going to use a plain stylesheet that's already been written to keep things focused on Remix. + +You can import CSS files directly into JavaScript modules. The compiler will fingerprint the asset, save it to your [`assetsBuildDirectory`][assetbuilddir], and provide your module with the publicly accessible href. + +👉 **Import the app styles** + +```jsx filename=app/root.tsx +// existing imports +import appStylesHref from "./app.css"; + +export function links() { + return [{ rel: "stylesheet", href: appStylesHref }]; +} +``` + +Every route can export [`links`][links]. They will be collected and rendered into the `` component we rendered in `app/root.tsx`. + +The app should look something like this now. It sure is nice having a designer who can also write the CSS, isn't it? (Thank you [Jim][jim] 🙏). + + + +## The Contact Route UI + +If you click on one of the sidebar items you'll get the default 404 page. Let's create a route that matches the url `/contacts/1`. + +👉 **Create the contact route module** + +```shellscript nonumber +touch app/routes/contacts.$contactId.tsx +# you might have to escape the $ +touch app/routes/contacts.\$contactId.tsx +``` + +In the Remix [route file convention][routeconvention], `.` will create a `/` in the URL and `$` makes a segment dynamic. We just created a route that will match URLs that look like this: + +- `/contacts/123` +- `/contacts/abc` + +👉 **Add the contact component UI** + +It's just a bunch of elements, feel free to copy/paste. + +```tsx filename=app/routes/contacts.$contactId.tsx +import { Form } from "@remix-run/react"; + +import type { ContactRecord } from "../data"; + +export default function Contact() { + const contact = { + first: "Your", + last: "Name", + avatar: "https://placekitten.com/g/200/200", + twitter: "your_handle", + notes: "Some notes", + favorite: true, + }; + + return ( +
+
+ {`${contact.first} +
+ +
+

+ {contact.first || contact.last ? ( + <> + {contact.first} {contact.last} + + ) : ( + No Name + )}{" "} + +

+ + {contact.twitter && ( +

+ + {contact.twitter} + +

+ )} + + {contact.notes &&

{contact.notes}

} + +
+
+ +
+
{ + let response = confirm( + "Please confirm you want to delete this record." + ); + if (response === false) { + event.preventDefault(); + } + }} + > + +
+
+
+
+ ); +} + +function Favorite({ contact }: { contact: ContactRecord }) { + // yes, this is a `let` for later + let favorite = contact.favorite; + return ( +
+ +
+ ); +} +``` + +Now if we click one of the links or visit `/contacts/1` we get ... nothing new? + +contact route with blank main content + +## Nested Routes and Outlets + +Since Remix is built on top of React Router, it supports nested routing. In order for child routes to render inside of parent layouts, we need to render an [outlet][outlet] in the parent. Let's fix it, open up `app/root.tsx` and render an outlet inside. + +👉 **Render an [``][outlet]** + +```jsx filename=app/root.jsx lines=[1,8-10] +import { Outlet } from "@remix-run/react"; +/* existing imports */ + +export default function Root() { + return ( + + {/* all the other elements */} +
+ +
+ {/* all the other elements */} + + ); +} +``` + +Now the child route should be rendering through the outlet. + +contact route with the main content + +## Client Side Routing + +You may or may not have noticed, but when we click the links in the sidebar, the browser is doing a full document request for the next URL instead of client side routing. + +Client side routing allows our app to update the URL without requesting another document from the server. Instead, the app can immediately render new UI. Let's make it happen with [``][link]. + +👉 **Change the sidebar `` to ``** + +```jsx filename=app/root.jsx lines=[2,13,16] +/* existing imports */ +import { Link } from "@remix-run/react"; + +export default function Root() { + return ( + <> + + + ); +} +``` + +You can open the network tab in the browser devtools to see that it's not requesting documents anymore. + +## Loading Data + +URL segments, layouts, and data are more often than not coupled (tripled?) together. We can see it in this app already: + +| URL Segment | Component | Data | +| ------------------- | ----------- | ------------------ | +| / | `` | list of contacts | +| contacts/:contactId | `` | individual contact | + +Because of this natural coupling, Remix has data conventions to get data into your route components easily. + +There are two APIs we'll be using to load data, [`loader`][loader] and [`useLoaderData`][useloaderdata]. First we'll create and export a loader function in the root route and then render the data. + +👉 **Export a loader from `root.jsx` and render the data** + +```jsx filename=app/root.tsx lines=[2,4,6-9,14,25-46] +/* existing imports */ +import { useLoaderData } from "@remix-run/react"; + +import { getContacts } from "./data"; + +export async function loader() { + const contacts = await getContacts(); + return { contacts }; +} + +/* other code */ + +export default function Root() { + const { contacts } = useLoaderData(); + + return ( + + {/* other code */} + + + {/* other code */} + + ); +} +``` + +That's it! Remix will now automatically keep that data in sync with your UI. The sidebar should now look like this: + + + +## Type Inference + +You may have noticed TypeScript complaining about the `contact` type inside the map. We can add a quick annotation to get type inference about our data with `typeof loader`: + +```tsx filename=app/root.tsx +export default function Root() { + let { contacts } = useLoaderData(); + // ... +} +``` + +## URL Params in Loaders + +👉 **Click on one of the sidebar links** + +We should be seeing our old static contact page again, with one difference: the URL now has a real ID for the record. + + + +Remember the `$contactId` part of the file name at `routes/contacts.$contactId.tsx`? These dynamic segments will match dynamic (changing) values in that position of the URL. We call these values in the URL "URL Params", or just "params" for short. + +These [`params`][params] are passed to the loader with keys that match the dynamic segment. For example, our segment is named `$contactId` so the value will be passed as `params.contactId`. + +These params are most often used to find a record by ID. Let's try it out. + +👉 **Add a loader to the contact page and access data with `useLoaderData`** + +```tsx filename=app/routes/contacts.$contactId.tsx lines=[1,3,5-7,11] +import { Form, useLoaderData } from "@remix-run/react"; + +import { getContact } from "../data"; + +export async function loader({ params }) { + let contact = await getContact(params.contactId); + return contact; +} + +export default function Contact() { + const contact = useLoaderData(); + // existing code +} +``` + + + +## Validating Params and Throwing Responses + +TypeScript is very upset with us, let's make it happy and see what that forces us to consider: + +```tsx filename=app/routes/contacts.$contactId.tsx lines=[2,3,7-8,14] +import { Form, useLoaderData } from "@remix-run/react"; +import invariant from "tiny-invariant"; +import type { LoaderArgs } from "@remix-run/node"; + +import { getContact } from "../data"; + +export async function loader({ params }: LoaderArgs) { + invariant(params.contactId, "Missing contactId param"); + let contact = await getContact(params.contactId); + return contact; +} + +export default function Contact() { + const contact = useLoaderData(); + // existing code +} +``` + +First problem this highlights is we might have gotten the param's name wrong between the file name and the code (maybe you changed the name of the file!). Invariant is a handy function for throwing an error with a custom message when you anticipated a potential issue with your code. + +Next, the `useLoaderData()` now knows that we got a contact or `null` (maybe there is no contact with that ID). This potential `null` is cumbersome for our component code and the TS errors are flying around still. + +We could account for the possibility of the contact being not found in component code, but the webby thing to do is send a proper 404. We can do that in the loader and solve all of our problems at once. + +```tsx filename=app/routes/contacts.$contactId.tsx lines=[4-6] +export async function loader({ params }: LoaderArgs) { + invariant(params.contactId, "Missing contactId param"); + let contact = await getContact(params.contactId); + if (!contact) { + throw new Response("Not Found", { status: 404 }); + } + return contact; +} +``` + +Now, if the user isn't found, code execution down this path stops and Remix renders the error path instead. Components in Remix can focus only on the happy path 😁 + +## Data Mutations + +We'll create our first contact in a second, but first let's talk about HTML. + +Remix emulates HTML Form navigation as the data mutation primitive, which used to be the only way prior to the JavaScript cambrian explosion. Don't be fooled by the simplicity! Forms in Remix give you the UX capabilities of client rendered apps with the simplicity of the "old school" web model. + +While unfamiliar to some web developers, HTML forms actually cause a navigation in the browser, just like clicking a link. The only difference is in the request: links can only change the URL while forms can also change the request method (GET vs POST) and the request body (POST form data). + +Without client side routing, the browser will serialize the form's data automatically and send it to the server as the request body for POST, and as URLSearchParams for GET. Remix does the same thing, except instead of sending the request to the server, it uses client side routing and sends it to a route [`action`][action]. + +We can test this out by clicking the "New" button in our app. + + + +Remix sends a 405 because there is no code on the server to handle this form navigation. + +## Creating Contacts + +We'll create new contacts by exporting an `action` in our root route. When the user clicks the "new" button, the form will POST to the root route action. + +👉 **Create the `action`** + +```jsx filename=src/routes/root.jsx lines=[2,4-7] +// existing code +import { getContacts, createEmptyContact } from "../data"; + +export async function action() { + const contact = await createEmptyContact(); + return { contact }; +} + +/* other code */ +``` + +That's it! Go ahead and click the "New" button and you should see a new record pop into the list 🥳 + + + +The `createEmptyContact` method just creates an empty contact with no name or data or anything. But it does still create a record, promise! + +> 🧐 Wait a sec ... How did the sidebar update? Where did we call the `action`? Where's the code to refetch the data? Where are `useState`, `onSubmit` and `useEffect`?! + +This is where the "old school web" programming model shows up. [`
`][form] prevents the browser from sending the request to the server and sends it to your route `action` instead with `fetch`. + +In web semantics, a POST usually means some data is changing. By convention, Remix uses this as a hint to automatically revalidate the data on the page after the action finishes. + +In fact, since it's all just HTML and HTTP, you could disable JavaScript and the whole thing will still work. Instead of Remix serializing the form and making a `fetch` to your server, the browser will serialize the form and make a document request. From there Remix will render the page server side and send it down. It's the same UI in the end either way. + +We'll keep JavaScript around though because we're gonna make a better user experience than spinning favicons and static documents. + +## Updating Data + +Let's add a way to fill the information for our new record. + +Just like creating data, you update data with [``][form]. Let's make a new route at `routes/contacts.$contactId_.edit.tsx`. + +👉 **Create the edit component** + +```shellscript nonumber +touch app/routes/contacts.\$contactId_.edit.tsx +``` + +Note the weird `_` in `$contactId_`. By default, routes will automatically nest inside routes with the same prefixed name. Adding a trialing `_` tells the route to **not** nest inside `routes/contacts.$contactId.tsx`. Read more in the [Route File Naming][routeconvention] guide. + +👉 **Add the edit page UI** + +Nothing we haven't seen before, feel free to copy/paste: + +```tsx filename=app/routes/contacts.$contactId.edit.tsx +import { Form, useLoaderData } from "@remix-run/react"; +import invariant from "tiny-invariant"; +import type { LoaderArgs } from "@remix-run/node"; + +import { getContact } from "../data"; + +export async function loader({ params }: LoaderArgs) { + invariant(params.contactId, "Missing contactId param"); + let contact = await getContact(params.contactId); + if (!contact) { + throw new Response("Not found", { status: 404 }); + } + return contact; +} + +export default function EditContact() { + const contact = useLoaderData(); + + return ( + +

+ Name + + +

+ + +