From efb25a0b9ea3855a734c0e8e5e7043a823831f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Thu, 17 Aug 2023 21:58:20 +0200 Subject: [PATCH] docs: small updates --- docs/discussion/00-introduction.md | 16 +- docs/discussion/01-runtimes.md | 8 +- docs/discussion/02-routes.md | 111 ++++---- docs/discussion/03-data-flow.md | 44 +-- docs/discussion/04-server-vs-client.md | 30 ++- docs/discussion/05-react-router.md | 10 +- docs/discussion/06-progressive-enhancement.md | 8 +- docs/discussion/07-pending-ui.md | 34 +-- docs/discussion/08-state-management.md | 60 +++-- docs/file-conventions/-client.md | 7 +- docs/file-conventions/-server.md | 7 +- docs/file-conventions/entry.client.md | 4 +- docs/file-conventions/remix-config.md | 14 +- docs/file-conventions/routes.md | 253 +++++++++--------- docs/guides/14-error-handling.md | 16 -- docs/guides/contributing.md | 38 +-- docs/guides/gotchas.md | 11 +- docs/guides/manual-mode.md | 10 +- docs/guides/templates.md | 23 +- docs/guides/v2.md | 240 +++++++++-------- docs/index.md | 6 +- docs/other-api/serve.md | 28 +- docs/route/action.md | 7 +- docs/route/meta.md | 5 +- docs/route/should-revalidate.md | 6 +- docs/start/community.md | 2 +- docs/start/future-flags.md | 6 +- docs/styling/bundling.md | 16 +- docs/styling/css-imports.md | 3 +- docs/styling/css-in-js.md | 13 +- docs/styling/css.md | 72 +++-- docs/styling/postcss.md | 73 ++--- docs/styling/tailwind.md | 13 +- docs/styling/vanilla-extract.md | 2 +- docs/tutorials/blog.md | 30 +-- 35 files changed, 648 insertions(+), 578 deletions(-) delete mode 100644 docs/guides/14-error-handling.md diff --git a/docs/discussion/00-introduction.md b/docs/discussion/00-introduction.md index 4aa60b3a0f3..6871a15da5d 100644 --- a/docs/discussion/00-introduction.md +++ b/docs/discussion/00-introduction.md @@ -7,7 +7,7 @@ order: 0 These discussion topics are intended to be read in order. It gives you a linear path to Remix mastery instead of bouncing around from one doc to another. While useful independently, topics may refer to code and concepts from previous chapters. -Built on top of [React Router][reactrouter], Remix is four things: +Built on top of [React Router][react-router], Remix is four things: 1. A compiler 2. A server-side HTTP handler @@ -44,7 +44,7 @@ app.all( ); ``` -Express (or Node.js) is the actual server, Remix is just a handler on that server. The `"@remix-run/express"` package is called an adapter. Remix handlers are server agnostic. Adapters make them work for a specific server by converting the server's request/response API into the Fetch API on the way in, and then adapting the Fetch Response coming from Remix into the server's response API. Here's some pseudo code of what an adapter does: +Express (or Node.js) is the actual server, Remix is just a handler on that server. The `"@remix-run/express"` package is called an adapter. Remix handlers are server agnostic. Adapters make them work for a specific server by converting the server's request/response API into the Fetch API on the way in, and then adapting the Fetch Response coming from Remix into the server's response API. Here's some pseudocode of what an adapter does: ```ts export function createRequestHandler({ build }) { @@ -138,7 +138,7 @@ export async function action({ request }: ActionArgs) { You can actually use Remix as just a server-side framework without using any browser JavaScript at all. The route conventions for data loading with `loader`, mutations with `action` and HTML forms, and components that render at URLs, can provide the core feature set of a lot of web projects. -In this way, **Remix scales down**. Not every page in your application needs a bunch of JavaScript in the browser and not every user interaction requires any extra flair than the browser's default behaviors. In Remix you can build it the simple way first, and then scale up without changing the fundamental model. Additionally, the majority of the app works before JavaScript loads in the browser, which makes Remix apps resilient to choppy network conditions by design. +In this way, **Remix scales down**. Not every page in your application needs a bunch of JavaScript in the browser and not every user interaction requires any extra flair than the browser's default behaviors. In Remix, you can build it the simple way first, and then scale up without changing the fundamental model. Additionally, the majority of the app works before JavaScript loads in the browser, which makes Remix apps resilient to choppy network conditions by design. If you're not familiar with traditional back-end web frameworks, you can think of Remix routes as React components that are already their own API route and already know how to load and submit data to themselves on the server. @@ -146,7 +146,7 @@ If you're not familiar with traditional back-end web frameworks, you can think o Once Remix has served the document to the browser, it "hydrates" the page with the browser build's JavaScript modules. This is where we talk a lot about Remix "emulating the browser". -When the user clicks a link, instead of making a round trip to the server for the entire document and all of the assets, Remix simply fetches the data for the next page and updates the UI. This has many performance benefits over making a full-document request: +When the user clicks a link, instead of making a round trip to the server for the entire document and all the assets, Remix simply fetches the data for the next page and updates the UI. This has many performance benefits over making a full-document request: 1. Assets don't need to be re-downloaded (or pulled from cache) 2. Assets don't need to be parsed by the browser again @@ -158,7 +158,7 @@ This approach also has UX benefits like not resetting the scroll position of a s Remix can also prefetch all resources for a page when the user is about to click a link. The browser framework knows about the compiler's asset manifest. It can match the URL of the link, read the manifest, and then prefetch all data, JavaScript modules, and even CSS resources for the next page. This is how Remix apps feel fast even when networks are slow. -Remix then provides client side APIs so you can create rich user experiences without changing the fundamental model of HTML and browsers. +Remix then provides client side APIs, so you can create rich user experiences without changing the fundamental model of HTML and browsers. Taking our route module from before, here are a few small, but useful UX improvements to the form that you can only do with JavaScript in the browser: @@ -166,7 +166,7 @@ Taking our route module from before, here are a few small, but useful UX improve 2. Focus the input when server-side form validation fails 3. Animate in the error messages -```tsx nocopy lines=[4-6,8-12,23-26,30-32] +```tsx lines=[4-6,8-12,23-26,30-32] nocopy export default function Projects() { const projects = useLoaderData(); const actionData = useActionData(); @@ -213,7 +213,7 @@ Because Remix reaches into the controller level of the backend, it can do this s And while it doesn't reach as far back into the stack as server-side frameworks like Rails and Laravel, it does reach way farther up the stack into the browser to make the transition from the back end to the front end seamless. -For example. Building a plain HTML form and server-side handler in a back-end heavy web framework is just as easy to do as it is in Remix. But as soon as you want to cross over into an experience with animated validation messages, focus management, and pending UI, it requires a fundamental change in the code. Typically people build an API route and then bring in a splash of client-side JavaScript to connect the two. With Remix you simply add some code around the existing "server side view" without changing how it works fundamentally. +For example. Building a plain HTML form and server-side handler in a back-end heavy web framework is just as easy to do as it is in Remix. But as soon as you want to cross over into an experience with animated validation messages, focus management, and pending UI, it requires a fundamental change in the code. Typically, people build an API route and then bring in a splash of client-side JavaScript to connect the two. With Remix, you simply add some code around the existing "server side view" without changing how it works fundamentally. We borrowed an old term and called this Progressive Enhancement in Remix. Start small with a plain HTML form (Remix scales down) and then scale the UI up when you have the time and ambition. @@ -224,4 +224,4 @@ We borrowed an old term and called this Progressive Enhancement in Remix. Start [vercel]: https://vercel.com [netlify]: https://netlify.com [arc]: https://arc.codes -[reactrouter]: https://reactrouter.com +[react-router]: https://reactrouter.com diff --git a/docs/discussion/01-runtimes.md b/docs/discussion/01-runtimes.md index 288ba4fe3bb..8f58b46b2ba 100644 --- a/docs/discussion/01-runtimes.md +++ b/docs/discussion/01-runtimes.md @@ -11,7 +11,7 @@ Deploying a Remix application has four layers: 3. A server adapter like `@remix-run/express` 4. A web host or platform -Depending on your web host, you may have fewer layers. For example, deploying to Cloudflare Pages takes care of 2, 3, and 4 all at once. Deploying Remix inside of an express app will have all four, and using the "Remix App Server" combines 2 and 3! +Depending on your web host, you may have fewer layers. For example, deploying to Cloudflare Pages takes care of 2, 3, and 4 all at once. Deploying Remix inside an Express app will have all four, and using the "Remix App Server" combines 2 and 3! You can wire all of these up yourself, or start with a Remix Template. @@ -49,11 +49,11 @@ import { createCookieSessionStorage } from "remix"; ## Adapters -Remix is not an HTTP server, but rather a handler inside of an existing HTTP server. Adapters allow the Remix handler to run inside the HTTP server. Some JavaScript runtimes, especially Node.js, have multiple ways to create an HTTP server. For example, in Node.js you can use Express.js, fastify, or raw `http.createServer`. +Remix is not an HTTP server, but rather a handler inside an existing HTTP server. Adapters allow the Remix handler to run inside the HTTP server. Some JavaScript runtimes, especially Node.js, have multiple ways to create an HTTP server. For example, in Node.js you can use Express.js, fastify, or raw `http.createServer`. Each of these servers has its own Request/Response API. The adapter's job is to convert the incoming request to a Web Fetch Request, run the Remix handler, and then adapt the Web Fetch Response back to the host server's response API. -Here's some pseudo code that illustrates the flow. +Here's some pseudocode that illustrates the flow. ```tsx // import the app build created by `remix build` @@ -92,7 +92,7 @@ See [`@remix-run/serve`][serve] ## Templates -Remix is designed to be incredibly flexible with just enough opinions to connect the UI to the back end but it doesn't bring opinions on the database you use, how you cache data, or where and how your app is deployed. +Remix is designed to be incredibly flexible with just enough opinions to connect the UI to the back end, but it doesn't bring opinions on the database you use, how you cache data, or where and how your app is deployed. Remix templates are starting points for app development with all of these extra opinions baked in, created by the community. diff --git a/docs/discussion/02-routes.md b/docs/discussion/02-routes.md index 9556a16d974..6cd4ccd6826 100644 --- a/docs/discussion/02-routes.md +++ b/docs/discussion/02-routes.md @@ -24,7 +24,7 @@ This strategy, combined with modern browsers' capability to handle multiple conc ## Conventional Route Configuration -Remix introduces a key convention to help streamline the routing process: the `routes` folder. When a developer introduces a file within this folder, Remix inherently understands it as a route. This convention simplifies the process of defining routes, associating them with URLs, and rendering the associated components. +Remix introduces a key convention to help streamline the routing process: the `app/routes` folder. When a developer introduces a file within this folder, Remix inherently understands it as a route. This convention simplifies the process of defining routes, associating them with URLs, and rendering the associated components. Here's a sample directory that uses the routes folder convention: @@ -41,19 +41,19 @@ app/ └── root.tsx ``` -All the routes that start with `concerts.` will be child routes of `concerts.tsx`. +All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx`. -| URL | Matched Route | Layout | -| -------------------------- | ----------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/about` | `about.tsx` | `root.tsx` | -| `/concerts` | `concerts._index.tsx` | `concerts.tsx` | -| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | +| URL | Matched Route | Layout | +| -------------------------- | ---------------------------------- | ------------------------- | +| `/` | `app/routes/_index.tsx` | `app/root.tsx` | +| `/about` | `app/routes/about.tsx` | `app/root.tsx` | +| `/concerts` | `app/routes/concerts._index.tsx` | `app/routes/concerts.tsx` | +| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` | +| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` | ## Conventional Route Folders -For routes that require additional modules or assets, a folder inside of `routes/` with a `route.tsx` file can be used. This method: +For routes that require additional modules or assets, a folder inside of `app/routes` with a `route.tsx` file can be used. This method: - **Co-locates Modules**: It gathers all elements connected to a particular route, ensuring logic, styles, and components are closely knit. - **Simplifies Imports**: With related modules in one place, managing imports becomes straightforward, enhancing code maintainability. @@ -62,68 +62,74 @@ For routes that require additional modules or assets, a folder inside of `routes The same routes from above could instead be organized like this: -``` +```markdown app/ -└── routes/ - ├── _index/ - │   ├── signup-form.tsx - │   └── route.tsx - ├── about/ - │   ├── header.tsx - │   └── route.tsx - ├── concerts/ - │   ├── favorites-cookie.ts - │   └── route.tsx - ├── concerts.$city/ - │   └── route.tsx - ├── concerts._index/ - │   ├── featured.tsx - │   └── route.tsx - └── concerts.trending/ - ├── card.tsx - ├── route.tsx - └── sponsored.tsx +├── routes/ +│ ├── _index/ +│ │ ├── signup-form.tsx +│ │ └── route.tsx +│ ├── about/ +│ │ ├── header.tsx +│ │ └── route.tsx +│ ├── concerts/ +│ │ ├── favorites-cookie.ts +│ │ └── route.tsx +│ ├── concerts.$city/ +│ │ └── route.tsx +│ ├── concerts._index/ +│ │ ├── featured.tsx +│ │ └── route.tsx +│ └── concerts.trending/ +│ ├── card.tsx +│ ├── route.tsx +│ └── sponsored.tsx +└── root.tsx ``` You can read more about the specific patterns in the file names and other features in the [Route File Conventions][route-file-conventions] reference. -Only the folders directly beneath `routes/` will be registered as a route. Deeply nested folders are ignored. The file at `routes/about/header/route.tsx` will not create a route. +Only the folders directly beneath `app/routes` will be registered as a route. Deeply nested folders are ignored. The file at `app/routes/about/header/route.tsx` will not create a route. ```markdown bad lines=[4] -routes -└── about - ├── header - │   └── route.tsx - └── route.tsx +app/ +├── routes/ +│ └── about/ +│ ├── header/ +│ │ └── route.tsx +│ └── route.tsx +└── root.tsx ``` ## Manual Route Configuration -While the `routes/` folder offers a convenient convention for developers, Remix appreciates that one size doesn't fit all. There are times when the provided convention might not align with specific project requirements or a developer's preferences. In such cases, Remix allows for manual route configuration via the `remix.config`. This flexibility ensures developers can structure their application in a way that makes sense for their project. +While the `app/routes` folder offers a convenient convention for developers, Remix appreciates that one size doesn't fit all. There are times when the provided convention might not align with specific project requirements or a developer's preferences. In such cases, Remix allows for manual route configuration via [`remix.config.js`][remix-config]. This flexibility ensures developers can structure their application in a way that makes sense for their project. A common way to structure an app is by top-level features folders. Consider that routes related to a particular theme, like concerts, likely share several modules. Organizing them under a single folder makes sense: -```text + +```markdown app/ -├── about -│   └── route.tsx -├── concerts -│   ├── card.tsx -│   ├── city.tsx -│   ├── favorites-cookie.ts -│   ├── home.tsx -│   ├── layout.tsx -│   ├── sponsored.tsx -│   └── trending.tsx -└── home - ├── header.tsx - └── route.tsx +├── about/ +│ └── route.tsx +├── concerts/ +│ ├── card.tsx +│ ├── city.tsx +│ ├── favorites-cookie.ts +│ ├── home.tsx +│ ├── layout.tsx +│ ├── sponsored.tsx +│ └── trending.tsx +├── home/ +│ ├── header.tsx +│ └── route.tsx +└── root.tsx ``` To configure this structure into the same URLs as the previous examples, you can use the `routes` function in `remix.config.js`: ```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ export default { routes(defineRoutes) { return defineRoutes((route) => { @@ -139,6 +145,7 @@ export default { }; ``` -Remix's route configuration approach blends convention with flexibility. You can use the `routes` folder for an easy, organized way to set up your routes. If you want more control, dislike the file names, or have unique needs, there's `remix.config`. It is expected that many apps forgo the routes folder convention in favor of `remix.config`. +Remix's route configuration approach blends convention with flexibility. You can use the `app/routes` folder for an easy, organized way to set up your routes. If you want more control, dislike the file names, or have unique needs, there's `remix.config.js`. It is expected that many apps forgo the routes folder convention in favor of `remix.config.js`. [route-file-conventions]: ../file-conventions/routes +[remix-config]: ../file-conventions/remix-config diff --git a/docs/discussion/03-data-flow.md b/docs/discussion/03-data-flow.md index 22de4f32753..bc260c36b12 100644 --- a/docs/discussion/03-data-flow.md +++ b/docs/discussion/03-data-flow.md @@ -34,13 +34,16 @@ export async function action() { Route files can export a `loader` function that provides data to the route component. When the user navigates to a matching route, the data is first loaded and then the page is rendered. -```tsx filename=routes/account.tsx lines=[1-7] -export async function loader({ request }) { +```tsx filename=routes/account.tsx lines=[1-2,4-10] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno + +export async function loader({ request }: LoaderArgs) { const user = await getUser(request); - return { + return json({ displayName: user.displayName, email: user.email, - }; + }); } export default function Component() { @@ -56,19 +59,21 @@ export async function action() { The default export of the route file is the component that renders. It reads the loader data with `useLoaderData`: -```tsx lines=[1,11-22] +```tsx lines=[3,13-28] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; -export function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const user = await getUser(request); - return { + return json({ displayName: user.displayName, email: user.email, - }; + }); } export default function Component() { - const user = useLoaderData(); + const user = useLoaderData(); return (

Settings for {user.displayName}

@@ -84,7 +89,7 @@ export default function Component() { ); } -export function action({ request }) { +export async function action() { // ... } ``` @@ -93,19 +98,24 @@ export function action({ request }) { Finally, the action on the route matching the form's action attribute is called when the form is submitted. In this example it's the same route. The values in the form fields will be available on the standard `request.formData()` API. Note the `name` attribute on the inputs is coupled to the `formData.get(fieldName)` getter. -```tsx lines=[25-34] +```tsx lines=[2,33-42] +import type { + ActionArgs, + LoaderArgs, +} from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; -export function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const user = await getUser(request); - return { + return json({ displayName: user.displayName, email: user.email, - }; + }); } export default function Component() { - const user = useLoaderData(); + const user = useLoaderData(); return (

Settings for {user.displayName}

@@ -121,7 +131,7 @@ export default function Component() { ); } -export function action({ request }) { +export async function action({ request }: ActionArgs) { const user = await getUser(request); await updateUser(user.id, { @@ -129,7 +139,7 @@ export function action({ request }) { displayName: formData.get("displayName"), }); - return { ok: true }; + return json({ ok: true }); } ``` diff --git a/docs/discussion/04-server-vs-client.md b/docs/discussion/04-server-vs-client.md index f007a86876d..4620124e31e 100644 --- a/docs/discussion/04-server-vs-client.md +++ b/docs/discussion/04-server-vs-client.md @@ -10,27 +10,37 @@ During the build step, the compiler creates both a server build and a client bui The following route exports and the dependencies used within them are removed from the client build: -- `loader` - `action` - `headers` +- `loader` Consider this route module from the last section: ```tsx filename=routes/settings.tsx +import type { + ActionArgs, + HeadersFunction, + LoaderArgs, +} from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; import { getUser, updateUser } from "../user"; -export function loader({ request }) { +export const headers: HeadersFunction = () => ({ + "Cache-Control": "max-age=300, s-maxage=3600", +}); + +export async function loader({ request }: LoaderArgs) { const user = await getUser(request); - return { + return json({ displayName: user.displayName, email: user.email, - }; + }); } export default function Component() { - const user = useLoaderData(); + const user = useLoaderData(); return (

Settings for {user.displayName}

@@ -46,7 +56,7 @@ export default function Component() { ); } -export function action({ request }) { +export async function action({ request }: ActionArgs) { const user = await getUser(request); await updateUser(user.id, { @@ -54,11 +64,11 @@ export function action({ request }) { displayName: formData.get("displayName"), }); - return { ok: true }; + return json({ ok: true }); } ``` -The server build will contain the entire module in the final bundle. However, the client build will remove the loader and action, along with the dependencies, resulting in this: +The server build will contain the entire module in the final bundle. However, the client build will remove the `action`, `headers` and `loader`, along with the dependencies, resulting in this: ```tsx filename=routes/settings.tsx import { useLoaderData } from "@remix-run/react"; @@ -87,6 +97,6 @@ You can force code out of either the client or the server with the `*.client.tsx While rare, sometimes server code makes it to client bundles because of how the compiler determines the dependencies of a route module, or because you accidentally try to use it in code that needs to ship to the client. You can force it out by adding `*.server.tsx` on the end of the file name. -For example, we could name a module `app/user.server.ts` instead of `app/user.ts` to ensure that the code in that module is never bundled into the client--even if you try to use it in the component. +For example, we could name a module `app/user.server.ts` instead of `app/user.ts` to ensure that the code in that module is never bundled into the client — even if you try to use it in the component. -Additionally, you may depend on client libraries that are unsafe to even bundle on the server--maybe it tries to access `window` by simply being imported. You can likewise remove these modules from the server build by appending `*.client.tsx` to the file name. +Additionally, you may depend on client libraries that are unsafe to even bundle on the server — maybe it tries to access `window` by simply being imported. You can likewise remove these modules from the server build by appending `*.client.tsx` to the file name. diff --git a/docs/discussion/05-react-router.md b/docs/discussion/05-react-router.md index c134243de48..ac16a765ed2 100644 --- a/docs/discussion/05-react-router.md +++ b/docs/discussion/05-react-router.md @@ -4,15 +4,15 @@ title: React Router # React Router -While Remix works as a multi-page app, when JavaScript is loaded, it uses client side routing for a full Single Page App user experience, with all the speed and network efficiency that comes along with it. +While Remix works as a multipage app, when JavaScript is loaded, it uses client side routing for a full Single Page App user experience, with all the speed and network efficiency that comes along with it. -Remix is built on top of [React Router][react-router] and maintained by the same team. This means that you can use all of the features of React Router in your Remix app. +Remix is built on top of [React Router][react-router] and maintained by the same team. This means that you can use all the features of React Router in your Remix app. This also means that the 90% of Remix is really just React Router: a very old, very stable library that is perhaps the largest dependency in the React ecosystem. Remix simply adds a server behind it. ## Importing Components and Hooks -Remix Re-exports all of the components and hooks from React Router DOM, so you don't need to install React Router yourself. +Remix re-exports all the components and hooks from React Router DOM, so you don't need to install React Router yourself. 🚫 Don't do this: @@ -22,7 +22,7 @@ import { useLocation } from "react-router-dom"; ✅ Do this: -```tsx +```tsx good import { useLocation } from "@remix-run/react"; ``` @@ -41,7 +41,7 @@ import { Link } from "react-router-dom"; ✅ Do this: -```tsx +```tsx good import { Link } from "@remix-run/react"; // this will prefetch data and assets diff --git a/docs/discussion/06-progressive-enhancement.md b/docs/discussion/06-progressive-enhancement.md index c2441ec1c9b..602ceea2ea4 100644 --- a/docs/discussion/06-progressive-enhancement.md +++ b/docs/discussion/06-progressive-enhancement.md @@ -74,7 +74,7 @@ When you start to rely on basic features of the web like HTML and URLs, you will Consider the button from before, with no fundamental change to the code, we can pepper in some client side behavior: -```tsx lines=[4,10-12] +```tsx lines=[1,4,7,10-12,14] import { useFetcher } from "@remix-run/react"; export function AddToCart({ id }) { @@ -108,7 +108,7 @@ Another example where progressive enhancement leads to simplicity is with the UR export function SearchBox() { return ( - + ); @@ -117,7 +117,7 @@ export function SearchBox() { This component doesn't need any state management. It just renders a form that submits to `/search`. When JavaScript loads, Remix will intercept the form submission and handle it client side. This allows you to add your own pending UI, or other client side behavior. Here's the next iteration: -```tsx +```tsx lines=[1,4-6,11] import { useNavigation } from "@remix-run/react"; export function SearchBox() { @@ -127,7 +127,7 @@ export function SearchBox() { return (
- + {isSearching ? : } ); diff --git a/docs/discussion/07-pending-ui.md b/docs/discussion/07-pending-ui.md index 8cc213eaffc..6477607b3be 100644 --- a/docs/discussion/07-pending-ui.md +++ b/docs/discussion/07-pending-ui.md @@ -80,7 +80,7 @@ export function ProjectList({ projects }) { Or add a spinner next to it by inspecting params: -```tsx lines=[4,10-12] +```tsx lines=[1,4,10-12] import { useParams } from "@remix-run/react"; export function ProjectList({ projects }) { @@ -106,10 +106,12 @@ While localized indicators on links are nice, they are incomplete. There are man **Busy Indicator**: It's typically best to wait for a record to be created instead of using optimistic UI since things like IDs and other fields are unknown until it completes. Also note this action redirects to the new record from the action. -```tsx filename=app/routes/create-project.tsx lines=[9,17-18,31] -import { redirect } from "@remix-run/node"; +```tsx filename=app/routes/create-project.tsx lines=[2,11,19-20,24,33] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno +import { redirect } from "@remix-run/node"; // or cloudflare/deno +import { useNavigation } from "@remix-run/react"; -export function action({ request }) { +export async function action({ request }: ActionArgs) { const formData = await request.formData(); const project = await createRecord({ name: formData.get("name"), @@ -164,19 +166,17 @@ function CreateProject() { **Optimistic UI**: When the UI simply updates a field on a record, optimistic UI is a great choice. Many, if not most user interactions in a web app tend to be updates, so this is a common pattern. -```tsx lines=[10-12,21,24] +```tsx lines=[6-8,19,22] import { useFetcher } from "@remix-run/react"; function ProjectListItem({ project }) { const fetcher = useFetcher(); - // start with the database state - let starred = project.starred; - - // change to optimistic value if submitting - if (fetcher.formData) { - starred = fetcher.formData.get("starred") === "1"; - } + const starred = fetcher.formData + ? // use to optimistic value if submitting + fetcher.formData.get("starred") === "1" + : // fall back to the database state + project.starred; return ( <> @@ -201,12 +201,13 @@ function ProjectListItem({ project }) { **Skeleton Fallback**: When data is deferred, you can add fallbacks with ``. This allows the UI to render without waiting for the data to load, speeding up the perceived and actual performance of the application. -```tsx lines=[8-11,20-24] -import { defer } from "@remix-run/node"; +```tsx lines=[9-12,21-25] +import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno +import { defer } from "@remix-run/node"; // or cloudflare/deno import { Await } from "@remix-run/react"; import { Suspense } from "react"; -export function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const reviewsPromise = getReviews(params.productId); const product = await getProduct(params.productId); return defer({ @@ -216,7 +217,8 @@ export function loader({ params }) { } export default function ProductRoute() { - const { product, reviews } = useLoaderData(); + const { product, reviews } = + useLoaderData(); return ( <> diff --git a/docs/discussion/08-state-management.md b/docs/discussion/08-state-management.md index 0de26c36f57..70be9c25440 100644 --- a/docs/discussion/08-state-management.md +++ b/docs/discussion/08-state-management.md @@ -22,7 +22,7 @@ In certain scenarios, using these libraries may be warranted. However, with Remi As discussed in [Fullstack Data Flow][fullstack-data-flow] Remix seamlessly bridges the gap between the backend and frontend via mechanisms like loaders, actions, and forms with automatic synchronization through revalidation. This offers developers the ability to directly use server state within components without managing a cache, the network communication, or data revalidation, making most client-side caching redundant. -Here's why using typical React state patterns might be an anti-pattern in Remix: +Here's why using typical React state patterns might be an antipattern in Remix: 1. **Network-related State:** If your React state is managing anything related to the network—such as data from loaders, pending form submissions, or navigational states—it's likely that you're managing state that Remix already manages: @@ -81,9 +81,9 @@ import { export function List() { const navigate = useNavigate(); - const [params] = useSearchParams(); + const [searchParams] = useSearchParams(); const [view, setView] = React.useState( - params.get("view") || "list" + searchParams.get("view") || "list" ); return ( @@ -114,12 +114,12 @@ export function List() { Instead of synchronizing state, you can simply read and set the state in the URL directly with boring ol' HTML forms. -```tsx lines=[5,9-16] -import { Form } from "@remix-run/react"; +```tsx good lines=[5,9-16] +import { Form, useSearchParams } from "@remix-run/react"; export function List() { - const [params] = useSearchParams(); - const view = params.get("view") || "list"; + const [searchParams] = useSearchParams(); + const view = searchParams.get("view") || "list"; return (
@@ -167,7 +167,7 @@ function Sidebar({ children }) { const [isOpen, setIsOpen] = React.useState(false); return (
- @@ -178,7 +178,7 @@ function Sidebar({ children }) { #### Local Storage -To persist state beyond the component lifecycle, browser local storage is a step up. +To persist state beyond the component lifecycle, browser local storage is a step-up. **Pros**: @@ -210,11 +210,7 @@ function Sidebar({ children }) { return (
- @@ -266,17 +262,23 @@ export const prefs = createCookie("prefs"); Next we set up the server action and loader to read and write the cookie: ```tsx +import type { + ActionArgs, + LoaderArgs, +} from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno + import { prefs } from "./prefs-cookie"; // read the state from the cookie -export function loader({ request }) { +export async function loader({ request }: LoaderArgs) { const cookieHeader = request.headers.get("Cookie"); const cookie = await prefs.parse(cookieHeader); - return { sidebarIsOpen: cookie.sidebarIsOpen }; + return json({ sidebarIsOpen: cookie.sidebarIsOpen }); } // write the state to the cookie -export function action({ request }) { +export async function action({ request }: ActionArgs) { const cookieHeader = request.headers.get("Cookie"); const cookie = await prefs.parse(cookieHeader); const formData = await request.formData(); @@ -297,7 +299,7 @@ After the server code is set up, we can use the cookie state in our UI: ```tsx function Sidebar({ children }) { const fetcher = useFetcher(); - let { sidebarIsOpen } = useLoaderData(); + let { sidebarIsOpen } = useLoaderData(); // use optimistic UI to immediately change the UI state if (fetcher.formData?.has("sidebar")) { @@ -434,37 +436,39 @@ export function Signup() { The backend endpoint, `/api/signup`, also performs validation and sends error feedback. Note that some essential validation, like detecting duplicate usernames, can only be done server-side using information the client doesn't have access to. -```tsx -export function signupHandler(request) { +```tsx bad +export async function signupHandler(request: Request) { const errors = await validateSignupRequest(request); if (errors) { - return { ok: false, errors: errors }; + return json({ ok: false, errors: errors }); } await signupUser(request); - return { ok: true, errors: null }; + return json({ ok: true, errors: null }); } ``` Now, let's contrast this with a Remix-based implementation. The action remains consistent, but the component is vastly simplified due to the direct utilization of server state via `useActionData`, and leveraging the network state that Remix inherently manages. -```tsx filename=app/routes/signup.tsx lines=[19-21] +```tsx filename=app/routes/signup.tsx good lines=[21-23] +import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno +import { json } from "@remix-run/node"; // or cloudflare/deno import { - useNavigation, useActionData, + useNavigation, } from "@remix-run/react"; -export function action({ request }) { +export async function action({ request }: ActionArgs) { const errors = await validateSignupRequest(request); if (errors) { - return { ok: false, errors: errors }; + return json({ ok: false, errors: errors }); } await signupUser(request); - return { ok: true, errors: null }; + return json({ ok: true, errors: null }); } export function Signup() { const navigation = useNavigation(); - const actionData = useActionData(); + const actionData = useActionData(); const userNameError = actionData?.errors?.userName; const passwordError = actionData?.errors?.password; diff --git a/docs/file-conventions/-client.md b/docs/file-conventions/-client.md index 4642d409121..d72574721f6 100644 --- a/docs/file-conventions/-client.md +++ b/docs/file-conventions/-client.md @@ -4,7 +4,7 @@ title: "*.client.ts extension" # `*.client.ts` -While uncommon, you may have a file or dependency that needs uses module side-effects in the browser. You can use `*.client.ts` on file names to force them out of server bundles. +While uncommon, you may have a file or dependency that uses module side effects in the browser. You can use `*.client.ts` on file names to force them out of server bundles. ```ts filename=feature-check.client.ts // this would break the server @@ -22,6 +22,7 @@ console.log(supportsVibrationAPI); // client: true | false ``` -See [Route Module][routemodule] for more information. +See [Route Module][route-module] for more information. -[routemodule]: ../route/route-module + +[route-module]: ../route/route-module diff --git a/docs/file-conventions/-server.md b/docs/file-conventions/-server.md index 4175f8b66e5..51094468666 100644 --- a/docs/file-conventions/-server.md +++ b/docs/file-conventions/-server.md @@ -4,8 +4,9 @@ title: "*.server.ts extension" # `*.server.ts` -While not always necessary, you can use `*.server.ts` on file names to force them out of client bundles. Usually the compiler is fine, but if you've got a server dependency with module side-effects, move it into a `your-name.server.ts` file to ensure it is removed from client bundles. +While not always necessary, you can use `*.server.ts` on file names to force them out of client bundles. Usually the compiler is fine, but if you've got a server dependency with module side effects, move it into a `your-name.server.ts` file to ensure it is removed from client bundles. -See [Route Module][routemodule] for more information. +See [Route Module][route-module] for more information. -[routemodule]: ../route/route-module + +[route-module]: ../route/route-module diff --git a/docs/file-conventions/entry.client.md b/docs/file-conventions/entry.client.md index 4ec8f2492ae..343e755b98b 100644 --- a/docs/file-conventions/entry.client.md +++ b/docs/file-conventions/entry.client.md @@ -7,11 +7,11 @@ toc: false By default, Remix will handle hydrating your app on the client for you. If you want to customize this behavior, you can run `npx remix reveal` to generate a `app/entry.client.tsx` (or `.jsx`) that will take precedence. This file is the entry point for the browser and is responsible for hydrating the markup generated by the server in your [server entry module][server-entry-module], however you can also initialize any other client-side code here. -Typically this module uses `ReactDOM.hydrateRoot` to hydrate the markup that was already generated on the server in your [server entry module][server-entry-module]. +Typically, this module uses `ReactDOM.hydrateRoot` to hydrate the markup that was already generated on the server in your [server entry module][server-entry-module]. Here's a basic example: -```tsx +```tsx filename=app/entry.client.tsx import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; diff --git a/docs/file-conventions/remix-config.md b/docs/file-conventions/remix-config.md index 292a8c75168..1a24d885bc4 100644 --- a/docs/file-conventions/remix-config.md +++ b/docs/file-conventions/remix-config.md @@ -61,6 +61,13 @@ The URL prefix of the browser build with a trailing slash. Defaults to Whether to process CSS using [PostCSS][postcss] if a PostCSS config file is present. Defaults to `true`. +```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ +module.exports = { + postcss: false, +}; +``` + ## routes A function for defining custom routes, in addition to those already defined @@ -183,7 +190,8 @@ The platform the server build is targeting, which can either be `"neutral"` or Whether to support [Tailwind functions and directives][tailwind-functions-and-directives] in CSS files if `tailwindcss` is installed. Defaults to `true`. -```tsx +```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { tailwind: false, }; @@ -226,9 +234,9 @@ There are a few conventions that Remix uses you should be aware of. [an-awesome-visualization]: https://remix-routing-demo.netlify.app [remix-dev]: ../other-api/dev#remix-dev [app-directory]: #appDirectory -[css-side-effect-imports]: ../guides/styling#css-side-effect-imports +[css-side-effect-imports]: ../styling/css-imports [postcss]: https://postcss.org [tailwind-functions-and-directives]: https://tailwindcss.com/docs/functions-and-directives [jspm]: https://github.com/jspm/jspm-core [esbuild-plugins-node-modules-polyfill]: https://www.npmjs.com/package/esbuild-plugins-node-modules-polyfill -[port]: ../other-api/dev-v2#options-1 +[port]: ../other-api/dev#options-1 diff --git a/docs/file-conventions/routes.md b/docs/file-conventions/routes.md index f254996ac4d..d165313c63b 100644 --- a/docs/file-conventions/routes.md +++ b/docs/file-conventions/routes.md @@ -4,7 +4,7 @@ title: Route File Naming # Route File Naming -While you can configure routes in [remix.config.js][remix-config], most routes are created with this file system convention. Add a file, get a route. +While you can configure routes in [`remix.config.js`][remix-config], most routes are created with this file system convention. Add a file, get a route. Please note that you can use either `.js`, `.jsx`, `.ts` or `.tsx` file extensions. We'll stick with `.tsx` in the examples to avoid duplication. @@ -51,7 +51,7 @@ export default function Root() { ## Basic Routes -Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index-route] for the [root route][root-route]. +Any JavaScript or TypeScript files in the `app/routes` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index-route] for the [root route][root-route]. ```markdown lines=[3-4] @@ -62,10 +62,10 @@ app/ └── root.tsx ``` -| URL | Matched Routes | -| -------- | -------------- | -| `/` | `_index.tsx` | -| `/about` | `about.tsx` | +| URL | Matched Routes | +| -------- | ----------------------- | +| `/` | `app/routes/_index.tsx` | +| `/about` | `app/routes/about.tsx` | Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested-routing]. @@ -75,7 +75,7 @@ Adding a `.` to a route filename will create a `/` in the URL. ```markdown lines=[5-7] -app/ + app/ ├── routes/ │ ├── _index.tsx │ ├── about.tsx @@ -85,11 +85,11 @@ app/ └── root.tsx ``` -| URL | Matched Route | -| -------------------------- | ----------------------------- | -| `/concerts/trending` | `concerts.trending.tsx` | -| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` | -| `/concerts/san-diego` | `concerts.san-diego.tsx` | +| URL | Matched Route | +| -------------------------- | ---------------------------------------- | +| `/concerts/trending` | `app/routes/concerts.trending.tsx` | +| `/concerts/salt-lake-city` | `app/routes/concerts.salt-lake-city.tsx` | +| `/concerts/san-diego` | `app/routes/concerts.san-diego.tsx` | The dot delimiter also creates nesting, see the [nesting section][nested-routes] for more information. @@ -99,7 +99,7 @@ Usually your URLs aren't static but data-driven. Dynamic segments allow you to m ```markdown lines=[5] -app/ + app/ ├── routes/ │ ├── _index.tsx │ ├── about.tsx @@ -108,16 +108,16 @@ app/ └── root.tsx ``` -| URL | Matched Route | -| -------------------------- | ----------------------- | -| `/concerts/trending` | `concerts.trending.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | -| `/concerts/san-diego` | `concerts.$city.tsx` | +| URL | Matched Route | +| -------------------------- | ---------------------------------- | +| `/concerts/trending` | `app/routes/concerts.trending.tsx` | +| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | +| `/concerts/san-diego` | `app/routes/concerts.$city.tsx` | Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders][loader] and [actions][action]. ```tsx -export function loader({ params }: LoaderArgs) { +export async function loader({ params }: LoaderArgs) { return fakeDb.getAllConcertsForCity(params.city); } ``` @@ -127,7 +127,7 @@ You'll note the property name on the `params` object maps directly to the name o Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name: ```tsx -export function loader({ params }: LoaderArgs) { +export async function loader({ params }: LoaderArgs) { return fake.db.getConcerts({ date: params.date, city: params.city, @@ -144,8 +144,8 @@ Nested Routing is the general idea of coupling segments of the URL to component You create nested routes with [dot delimiters][dot-delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes: -```markdown -app/ +```markdown lines=[5-8] + app/ ├── routes/ │ ├── _index.tsx │ ├── about.tsx @@ -156,15 +156,15 @@ app/ └── root.tsx ``` -All the routes that start with `concerts.` will be child routes of `concerts.tsx` and render inside the parent route's [outlet][outlet]. +All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx` and render inside the parent route's [outlet][outlet]. -| URL | Matched Route | Layout | -| -------------------------- | ----------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/about` | `about.tsx` | `root.tsx` | -| `/concerts` | `concerts._index.tsx` | `concerts.tsx` | -| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | +| URL | Matched Route | Layout | +| -------------------------- | ---------------------------------- | ------------------------- | +| `/` | `app/routes/_index.tsx` | `app/root.tsx` | +| `/about` | `app/routes/about.tsx` | `app/root.tsx` | +| `/concerts` | `app/routes/concerts._index.tsx` | `app/routes/concerts.tsx` | +| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` | +| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` | Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly. @@ -184,7 +184,7 @@ Sometimes you want the URL to be nested, but you don't want the automatic layout ```markdown lines=[8] -app/ + app/ ├── routes/ │ ├── _index.tsx │ ├── about.tsx @@ -195,14 +195,14 @@ app/ └── root.tsx ``` -| URL | Matched Route | Layout | -| -------------------------- | ----------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/concerts/mine` | `concerts_.mine.tsx` | `root.tsx` | -| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | +| URL | Matched Route | Layout | +| -------------------------- | ---------------------------------- | ------------------------- | +| `/` | `app/routes/_index.tsx` | `app/root.tsx` | +| `/concerts/mine` | `app/routes/concerts_.mine.tsx` | `app/root.tsx` | +| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` | +| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` | -Note that `/concerts/mine` does not nest with `concerts.tsx` anymore, but `root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting. +Note that `/concerts/mine` does not nest with `app/routes/concerts.tsx` anymore, but `app/root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting. Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting. @@ -214,7 +214,7 @@ Sometimes you want to share a layout with a group of routes without adding any p ```markdown lines=[3-5] -app/ + app/ ├── routes/ │ ├── _auth.login.tsx │ ├── _auth.register.tsx @@ -225,12 +225,12 @@ app/ └── root.tsx ``` -| URL | Matched Route | Layout | -| -------------------------- | -------------------- | -------------- | -| `/` | `_index.tsx` | `root.tsx` | -| `/login` | `_auth.login.tsx` | `_auth.tsx` | -| `/register` | `_auth.register.tsx` | `_auth.tsx` | -| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` | +| URL | Matched Route | Layout | +| -------------------------- | ------------------------------- | ------------------------- | +| `/` | `app/routes/_index.tsx` | `app/root.tsx` | +| `/login` | `app/routes/_auth.login.tsx` | `app/routes/_auth.tsx` | +| `/register` | `app/routes/_auth.register.tsx` | `app/routes/_auth.tsx` | +| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` | Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL. @@ -240,7 +240,7 @@ Wrapping a route segment in parentheses will make the segment optional. ```markdown lines=[3-5] -app/ + app/ ├── routes/ │ ├── ($lang)._index.tsx │ ├── ($lang).$productId.tsx @@ -248,15 +248,15 @@ app/ └── root.tsx ``` -| URL | Matched Route | -| -------------------------- | ------------------------ | -| `/` | `($lang)._index.tsx` | -| `/categories` | `($lang).categories.tsx` | -| `/en/categories` | `($lang).categories.tsx` | -| `/fr/categories` | `($lang).categories.tsx` | -| `/american-flag-speedo` | `($lang)._index.tsx` | -| `/en/american-flag-speedo` | `($lang).$productId.tsx` | -| `/fr/american-flag-speedo` | `($lang).$productId.tsx` | +| URL | Matched Route | +| -------------------------- | ----------------------------------- | +| `/` | `app/routes/($lang)._index.tsx` | +| `/categories` | `app/routes/($lang).categories.tsx` | +| `/en/categories` | `app/routes/($lang).categories.tsx` | +| `/fr/categories` | `app/routes/($lang).categories.tsx` | +| `/american-flag-speedo` | `app/routes/($lang)._index.tsx` | +| `/en/american-flag-speedo` | `app/routes/($lang).$productId.tsx` | +| `/fr/american-flag-speedo` | `app/routes/($lang).$productId.tsx` | You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code. @@ -266,7 +266,7 @@ While [dynamic segments][dynamic-segments] match a single path segment (the stuf ```markdown lines=[4,6] -app/ + app/ ├── routes/ │ ├── _index.tsx │ ├── $.tsx @@ -275,19 +275,19 @@ app/ └── root.tsx ``` -| URL | Matched Route | -| -------------------------------------------- | ------------- | -| `/` | `_index.tsx` | -| `/beef/and/cheese` | `$.tsx` | -| `/files` | `files.$.tsx` | -| `/files/talks/remix-conf_old.pdf` | `files.$.tsx` | -| `/files/talks/remix-conf_final.pdf` | `files.$.tsx` | -| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` | +| URL | Matched Route | +| -------------------------------------------- | ------------------------ | +| `/` | `app/routes/_index.tsx` | +| `/beef/and/cheese` | `app/routes/$.tsx` | +| `/files` | `app/routes/files.$.tsx` | +| `/files/talks/remix-conf_old.pdf` | `app/routes/files.$.tsx` | +| `/files/talks/remix-conf_final.pdf` | `app/routes/files.$.tsx` | +| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `app/routes/files.$.tsx` | Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. ```tsx filename=app/routes/files.$.tsx -export function loader({ params }) { +export async function loader({ params }: LoaderArgs) { const filePath = params["*"]; return fake.getFileInfo(filePath); } @@ -297,13 +297,13 @@ export function loader({ params }) { If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. -| Filename | URL | -| ------------------------------- | ------------------- | -| `routes/sitemap[.]xml.tsx` | `/sitemap.xml` | -| `routes/[sitemap.xml].tsx` | `/sitemap.xml` | -| `routes/weird-url.[_index].tsx` | `/weird-url/_index` | -| `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` | -| `routes/[[so-weird]].tsx` | `/[so-weird]` | +| Filename | URL | +| ----------------------------------- | ------------------- | +| `app/routes/sitemap[.]xml.tsx` | `/sitemap.xml` | +| `app/routes/[sitemap.xml].tsx` | `/sitemap.xml` | +| `app/routes/weird-url.[_index].tsx` | `/weird-url/_index` | +| `app/routes/dolla-bills-[$].tsx` | `/dolla-bills-$` | +| `app/routes/[[so-weird]].tsx` | `/[so-weird]` | ## Folders for Organization @@ -313,62 +313,68 @@ Routes can also be folders with a `route.tsx` file inside defining the route mod Consider these routes: -``` -routes/ - _landing._index.tsx - _landing.about.tsx - _landing.tsx - app._index.tsx - app.projects.tsx - app.tsx - app_.projects.$id.roadmap.tsx + +```markdown + app/ +├── routes/ +│ ├── _landing._index.tsx +│ ├── _landing.about.tsx +│ ├── _landing.tsx +│ ├── app._index.tsx +│ ├── app.projects.tsx +│ ├── app.tsx +│ └── app_.projects.$id.roadmap.tsx +└── root.tsx ``` Some, or all of them can be folders holding their own `route` module inside. -``` -routes/ - _landing._index/ - route.tsx - scroll-experience.tsx - _landing.about/ - employee-profile-card.tsx - get-employee-data.server.tsx - route.tsx - team-photo.jpg - _landing/ - header.tsx - footer.tsx - route.tsx - app._index/ - route.tsx - stats.tsx - app.projects/ - get-projects.server.tsx - project-card.tsx - project-buttons.tsx - route.tsx - app/ - primary-nav.tsx - route.tsx - footer.tsx - app_.projects.$id.roadmap/ - route.tsx - chart.tsx - update-timeline.server.tsx - contact-us.tsx + +```markdown +app/ +├── routes/ +│ ├── _landing._index/ +│ │ ├── route.tsx +│ │ └── scroll-experience.tsx +│ ├── _landing.about/ +│ │ ├── employee-profile-card.tsx +│ │ ├── get-employee-data.server.tsx +│ │ ├── route.tsx +│ │ └── team-photo.jpg +│ ├── _landing/ +│ │ ├── footer.tsx +│ │ ├── header.tsx +│ │ └── route.tsx +│ ├── app._index/ +│ │ ├── route.tsx +│ │ └── stats.tsx +│ ├── app.projects/ +│ │ ├── get-projects.server.tsx +│ │ ├── project-buttons.tsx +│ │ ├── project-card.tsx +│ │ └── route.tsx +│ ├── app/ +│ │ ├── footer.tsx +│ │ ├── primary-nav.tsx +│ │ └── route.tsx +│ ├── app_.projects.$id.roadmap/ +│ │ ├── chart.tsx +│ │ ├── route.tsx +│ │ └── update-timeline.server.tsx +│ └── contact-us.tsx +└── root.tsx ``` Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example: ``` # these are the same route: -routes/app.tsx -routes/app/route.tsx +app/routes/app.tsx +app/routes/app/route.tsx # as are these -routes/app._index.tsx -routes/app._index/route.tsx +app/routes/app._index.tsx +app/routes/app._index/route.tsx ``` ## Scaling @@ -380,25 +386,22 @@ Our general recommendation for scale is to make every route a folder and put the ## More Flexibility -While we like this file convention, we recognize that at a certain scale many organizations won't like it. You can always define your routes programmatically in the [remix config][remix-config]. +While we like this file convention, we recognize that at a certain scale many organizations won't like it. You can always define your routes programmatically in [`remix.config.js`][remix-config]. There's also the [Flat Routes][flat-routes] third-party package with configurable options beyond the defaults in Remix. [loader]: ../route/loader [action]: ../route/action [outlet]: ../components/outlet -[routing-guide]: ../guides/routing +[routing-guide]: ../discussion/02-routes [root-route]: #root-route -[resource-route]: ../guides/resource-routes -[routeconvention-v2]: ./route-files-v2 -[flatroutes-rfc]: https://github.com/remix-run/remix/discussions/4482 -[root-route]: #root-route -[index-route]: ../guides/routing#index-routes -[nested-routing]: ../guides/routing#what-is-nested-routing + +[index-route]: ../discussion/02-routes#index-routes + +[nested-routing]: ../discussion/02-routes#what-is-nested-routing [nested-routes]: #nested-routes [remix-config]: ./remix-config#routes [dot-delimiters]: #dot-delimiters [dynamic-segments]: #dynamic-segments -[remix-config]: ./remix-config#routes [flat-routes]: https://github.com/kiliman/remix-flat-routes [an-awesome-visualization]: https://interactive-remix-routing-v2.netlify.app/ diff --git a/docs/guides/14-error-handling.md b/docs/guides/14-error-handling.md deleted file mode 100644 index d4043d12ef3..00000000000 --- a/docs/guides/14-error-handling.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Error Handling -hidden: true ---- - -# Error Handling - -- unexpected - - automatically handled - - granular w/ route boundaries - - granular w/ `` boundaries -- expected - - 404s - - 401s - - 503s - - can send data! diff --git a/docs/guides/contributing.md b/docs/guides/contributing.md index 44e5fff87c2..f622d0944a3 100644 --- a/docs/guides/contributing.md +++ b/docs/guides/contributing.md @@ -16,7 +16,7 @@ This document will familiarize you with our development process as well as how t All contributors sending a Pull Request need to sign the Contributor License Agreement (CLA) that explicitly assigns ownership of the contribution to us. -When you start a pull request, the remix-cla-bot will prompt you to review the CLA and sign it by adding your name to [contributors.yml][contributorsyaml] +When you start a pull request, the remix-cla-bot will prompt you to review the CLA and sign it by adding your name to [contributors.yml][contributors-yaml] [Read the CLA][cla] @@ -42,7 +42,7 @@ If you have an idea for a new feature, please don't send a Pull Request, but fol 3. The Admins assign an **Owner** to the issue. - Owners are responsible for shipping the feature including all decisions for APIs, behavior, and implementation. - Owners organize the work with other contributors for larger issues. - - Owners may be contributors from inside or outside of the Remix team. + - Owners may be contributors from inside or outside the Remix team. 4. Owners create an **RFC** from the Proposal and development can begin. 5. Pairing is highly encouraged, particularly at the start. @@ -63,7 +63,7 @@ Bug fix PRs without a test case might be closed immediately (some things are har If you think you've found a bug but don't have the time to send a PR, please follow these guidelines: -1. Create a minimal reproduction of the issue somewhere like Stackblitz, Replit, Codesandbox, etc. that we can visit and observe the bug: +1. Create a minimal reproduction of the issue somewhere like Stackblitz, Replit, CodeSandbox, etc. that we can visit and observe the bug: - [https://remix.new][https-remix-new] makes this really easy @@ -82,8 +82,8 @@ You can always check in on Remix development in our live-streamed planning meeti - Proposals are not “rejected”, only “accepted” onto the Roadmap. - Contributors can continue to up-vote and comment on Proposals, they will bubble up for a future review if it’s getting new activity. - The Remix Admin team may lock Proposals for any reason. -- The meeting will be live streamed on the [Remix YouTube channel][youtube]. - - Everyone is invited to the [Discord][discord] #roadmap-livestream-chat while the meeting is in progress. +- The meeting will be livestreamed on the [Remix YouTube channel][youtube]. + - Everyone is invited to the [Discord][discord] `#roadmap-livestream-chat` while the meeting is in progress. - Remix Collaborators are invited to attend. ### Issue Tracking @@ -93,7 +93,7 @@ If a Roadmap Issue is expected to be large (involving multiple tasks, authors, P - The original issue will remain on the Roadmap project to see general progress. - The subtasks will be tracked on the temporary project. - When the work is complete, the temporary project will be archived. -- The Owner is responsible for populating the sub-project with issues and splitting the work up into shippable chunks of work. +- The Owner is responsible for populating the subproject with issues and splitting the work up into shippable chunks of work. - Build / feature flags are encouraged over long-running branches. ### RFCs @@ -104,7 +104,7 @@ If a Roadmap Issue is expected to be large (involving multiple tasks, authors, P ### Support for Owners -- Owners will be added to the `#collaborators` private channel on [discord][discord] to get help with architecture and implementation. This channel is private to help keep noise to a minimum so Admins don't miss messages and owners can get unblocked. Owners can also discuss these questions in any channel or anywhere! +- Owners will be added to the `#collaborators` private channel on [Discord][discord] to get help with architecture and implementation. This channel is private to help keep noise to a minimum so Admins don't miss messages and owners can get unblocked. Owners can also discuss these questions in any channel or anywhere! - Admins will actively work with owners to ensure their issues and projects are organized (correct status, links to related issues, etc.), documented, and moving forward. - An issue's Owner may be reassigned if progress is stagnating. @@ -137,13 +137,13 @@ To help keep the repositories clean and organized, Collaborators will take the f Before you can contribute to the codebase, you will need to fork the repo. This will look a bit different depending on what type of contribution you are making: -The following steps will get you setup to contribute changes to this repo: +The following steps will get you set up to contribute changes to this repo: 1. Fork the repo (click the Fork button at the top right of [this page][this-page]). 2. Clone your fork locally. - ```bash + ```shellscript nonumber # in a terminal, cd to parent directory where you want your clone to be, then git clone https://github.com//remix.git cd remix @@ -180,7 +180,7 @@ The integration tests and the primary tests can be run in parallel using `npm-ru We also support watch plugins for project, file, and test filtering. To filter things down, you can use a combination of `--testNamePattern`, `--testPathPattern`, and `--selectProjects`. For example: -``` +```shellscript nonumber yarn test:primary --selectProjects react --testPathPattern transition --testNamePattern "initial values" ``` @@ -206,11 +206,11 @@ It's often really useful to be able to interact with a real app while developing To generate a new playground, simply run: -```sh +```shellscript nonumber yarn playground:new ``` -Where the name of the playground is optional and defaults to `playground-${Date.now()}`. Then you can `cd` into the directory that's generated for you and run `npm run dev`. In another terminal window have `yarn watch` running and you're ready to work on whatever Remix features you like with live reload magic 🧙‍♂️ +Where the name of the playground is optional and defaults to `playground-${Date.now()}`. Then you can `cd` into the directory that's generated for you and run `npm run dev`. In another terminal window have `yarn watch` running, and you're ready to work on whatever Remix features you like with live reload magic 🧙‍♂️ The playground generated from `yarn playground:new` is based on a template in `scripts/playground/template`. If you'd like to change anything about the template, you can create a custom one in `scripts/playground/template.local` which is `.gitignored` so you can customize it to your heart's content. @@ -218,7 +218,7 @@ The playground generated from `yarn playground:new` is based on a template in `s Before running the tests, you need to run a build. After you build, running `yarn test` from the root directory will run **every** package's tests. If you want to run tests for a specific package, use `yarn test --selectProjects `: -```bash +```shellscript nonumber # Test all packages yarn test @@ -235,21 +235,21 @@ This repo maintains separate branches for different purposes. They will look som - dev > code under active development between stable releases ``` -There may be other branches for various features and experimentation, but all of the magic happens from these branches. +There may be other branches for various features and experimentation, but all the magic happens from these branches. ## How do nightly releases work? -Nightly releases will run the action files from the `main` branch as scheduled workflows will always use the latest commit to the default branch, signified by [this comment on the nightly action file][nightly-action-comment] and the explicit branch appended to the reusable workflows in the [postrelease action][postrelease-action], however they checkout the `dev` branch during their set up as that's where we want our nightly releases to be cut from. From there, we check if the git SHA is the same and only cut a new nightly if something has changed. +Nightly releases will run the action files from the `main` branch as scheduled workflows will always use the latest commit to the default branch, signified by [this comment on the nightly action file][nightly-action-comment] and the explicit branch appended to the reusable workflows in the [postrelease action][postrelease-action], however they check out the `dev` branch during their setup as that's where we want our nightly releases to be cut from. From there, we check if the git SHA is the same and only cut a new nightly if something has changed. -## End to end testing +## End-to-end testing For every release of Remix (stable, experimental, nightly, and pre-releases), we will do a complete end-to-end test of Remix apps on each of our official adapters from `create-remix`, all the way to deploying them to production. We do this by utilizing the default [templates][templates] and the CLIs for Fly, and Arc. We'll then run some simple Cypress assertions to make sure everything is running properly for both development and the deployed app. [proposals]: https://github.com/remix-run/remix/discussions/categories/proposals [roadmap]: https://github.com/orgs/remix-run/projects/5 -[youtube]: https://www.youtube.com/c/Remix-Run/streams +[youtube]: https://www.youtube.com/@Remix-Run/streams [discord]: https://rmx.as/discord -[contributorsyaml]: https://github.com/remix-run/remix/blob/main/contributors.yml +[contributors-yaml]: https://github.com/remix-run/remix/blob/main/contributors.yml [cla]: https://github.com/remix-run/remix/blob/main/CLA.md [examples-repository]: https://github.com/remix-run/examples [this-page]: https://github.com/remix-run/remix @@ -260,5 +260,5 @@ For every release of Remix (stable, experimental, nightly, and pre-releases), we [vscode-playwright]: https://playwright.dev/docs/intro#using-the-vs-code-extension [nightly-action-comment]: https://github.com/remix-run/remix/blob/main/.github/workflows/nightly.yml#L8-L12 [postrelease-action]: https://github.com/remix-run/remix/blob/main/.github/workflows/postrelease.yml -[templates]: /templates +[templates]: ./templates [https-remix-new]: https://remix.new diff --git a/docs/guides/gotchas.md b/docs/guides/gotchas.md index 65765a560f2..e5d886330a8 100644 --- a/docs/guides/gotchas.md +++ b/docs/guides/gotchas.md @@ -154,8 +154,6 @@ if (typeof document === "undefined") { This will work for all JS environments (Node.js, Deno, Workers, etc.). -[esbuild]: https://esbuild.github.io/ - ## Browser extensions injecting code You may run into this warning in the browser: @@ -170,9 +168,9 @@ Check out the page in incognito mode, the warning should disappear. ## CSS bundle being incorrectly tree-shaken -When using [CSS bundling features][css-bundling] in combination with `export *` (e.g. when using an index file like `components/index.ts` that re-exports from all sub-directories) you may find that styles from the re-exported modules are missing from the build output. +When using [CSS bundling features][css-bundling] in combination with `export *` (e.g. when using an index file like `components/index.ts` that re-exports from all subdirectories) you may find that styles from the re-exported modules are missing from the build output. -This is due to an [issue with esbuild's CSS tree shaking][esbuild-css-tree-shaking-issue]. As a workaround, you should use named re-exports instead. +This is due to an [issue with `esbuild`'s CSS tree shaking][esbuild-css-tree-shaking-issue]. As a workaround, you should use named re-exports instead. ```diff -export * from "./Button"; @@ -183,7 +181,7 @@ Note that, even if this issue didn't exist, we'd still recommend using named re- ## Writing to Sessions in Loaders -Typically you should only write to sessions in actions, but there are occasions where it makes sense in loaders (anonymous users, navigation tracking, etc.) +Typically, you should only write to sessions in actions, but there are occasions where it makes sense in loaders (anonymous users, navigation tracking, etc.) While multiple loaders can _read_ from the same session, _writing_ to a session in loaders can cause problems. @@ -193,6 +191,7 @@ Additionally, sessions are built on cookies which come from the browser's reques If you need to write to a session in a loader, ensure the loader doesn't share that session with any other loaders. +[esbuild]: https://esbuild.github.io [remix-upload-handlers-like-unstable-create-file-upload-handler-and-unstable-create-memory-upload-handler]: ../utils/parse-multipart-form-data#uploadhandler -[css-bundling]: ../guides/styling#css-bundling +[css-bundling]: ../styling/bundling [esbuild-css-tree-shaking-issue]: https://github.com/evanw/esbuild/issues/1370 diff --git a/docs/guides/manual-mode.md b/docs/guides/manual-mode.md index dd21d4d63b0..ddbfd36e934 100644 --- a/docs/guides/manual-mode.md +++ b/docs/guides/manual-mode.md @@ -7,11 +7,11 @@ toc: false By default, `remix dev` drives like an automatic. It keeps your app server up-to-date with the latest code changes by automatically restarting the app server whenever file changes are detected in your app code. -This is a simple approach that stays out of your way and we think will work well for most apps. +This is a simple approach that stays out of your way, and we think will work well for most apps. But if app server restarts are slowing you down, you can take the wheel and drive `remix dev` like a manual: -```sh +```shellscript nonumber remix dev --manual -c "node ./server.js" ``` @@ -36,7 +36,7 @@ Check out our video ["Mental model for the new dev flow 🧠"][mental-model] for Previously, we referred to the Remix compiler as the "new dev server" or the "v2 dev server". Technically, `remix dev` is a thin layer around the Remix compiler that _does_ include a tiny server with a single endpoint (`/ping`) for coordinating hot updates. But thinking of `remix dev` as a "dev server" is unhelpful and wrongly implies that it is replacing your app server in dev. -Rather than replacing your app server, `remix dev` runs your app server _alongside_ the Remix compiler so you get the best of both worlds: +Rather than replacing your app server, `remix dev` runs your app server _alongside_ the Remix compiler, so you get the best of both worlds: - Hot updates managed by the Remix compiler - Real production code paths running in dev within your app server @@ -121,7 +121,7 @@ The `require` cache keys are _absolute paths_ so make sure you resolve your serv ### 1.b ESM: `import` cache busting Unlike CJS, ESM doesn't give you direct access to the import cache. -To workaround this, you can use a timestamp query parameter to force ESM to treat the import as a new module. +To work around this, you can use a timestamp query parameter to force ESM to treat the import as a new module. ```js import * as fs from "node:fs"; @@ -162,7 +162,7 @@ In the future, Remix may pre-bundle your dependencies to keep the import cache s ### 2. Detecting server code changes -Now that you have a way to bust the import cache for CJS or ESM, its time to put that to use by dynamically updating the server build within your app server. +Now that you have a way to bust the import cache for CJS or ESM, it's time to put that to use by dynamically updating the server build within your app server. To detect when the server code changes, you can use a file watcher like [chokidar][chokidar]: ```js diff --git a/docs/guides/templates.md b/docs/guides/templates.md index d213a753a7e..a414688316f 100644 --- a/docs/guides/templates.md +++ b/docs/guides/templates.md @@ -12,13 +12,13 @@ When using [`create-remix`][create-remix] to generate a new project, you can cho If you run `create-remix` without providing the `--template` option, you'll get a basic template using the [Remix App Server][remix-app-server]. -```sh +```shellscript nonumber npx create-remix@latest ``` If you are not interested in using TypeScript, you can install the simpler Javascript template instead: -```sh +```shellscript nonumber npx create-remix --template remix-run/remix/templates/remix-javascript ``` @@ -28,7 +28,7 @@ This is a great place to start if you're just looking to try out Remix for the f When a template is closer to being a production-ready application, to the point that it provides opinions about the CI/CD pipeline, database and hosting platform, the Remix community refers to these templates as "stacks". -There are several official stacks provided but you can also make your own (read more below). +There are several official stacks provided, but you can also make your own (read more below). [Read the feature announcement blog post][read-the-feature-announcement-blog-post] and [watch Remix Stacks videos on YouTube][watch-remix-stacks-videos-on-you-tube]. @@ -50,7 +50,7 @@ What you're left with is everything completely set up for you to just get to wor You can use these stacks by proving the `--template` option when running `create-remix`, for example: -```sh +```shellscript nonumber npx create-remix@latest --template remix-run/blues-stack ``` @@ -62,7 +62,7 @@ You can [browse the list of community stacks on GitHub.][remix-stack-topic] Community stacks can be used by passing the GitHub username/repo combo to the `--template` option when running `create-remix`, for example: -```sh +```shellscript nonumber npx create-remix@latest --template :username/:repo ``` @@ -78,7 +78,7 @@ We also provide a [community-driven examples repository,][examples] with each ex You can use these templates and examples by passing a GitHub shorthand to the `--template` option when running `create-remix`, for example: -```sh +```shellscript nonumber npx create-remix@latest --template remix-run/examples/basic ``` @@ -93,7 +93,7 @@ Some hosting providers maintain their own Remix templates. For more information, If your template is in a private GitHub repo, you can pass a GitHub token via the `--token` option: -```sh +```shellscript nonumber npx create-remix@latest --template your-private/repo --token yourtoken ``` @@ -103,7 +103,7 @@ The [token just needs `repo` access][repo access token]. You can provide a local directory or tarball on disk to the `--template` option, for example: -```sh +```shellscript nonumber npx create-remix@latest --template /my/remix-stack npx create-remix@latest --template /my/remix-stack.tar.gz npx create-remix@latest --template file:///Users/michael/my-remix-stack.tar.gz @@ -130,11 +130,10 @@ You could even use `remix.init/index.js` to ask further questions to the develop After the init script has been run, the `remix.init` folder gets deleted, so you don't need to worry about it cluttering up the finished codebase. -Do note that consumers can opt out of running the remix.init script. To do so manually, they'll need to run `remix init`. +Do note that consumers can opt out of running the `remix.init` script. To do so manually, they'll need to run `remix init`. -[create-remix]: /other-api/create-remix - -[remix-app-server]: [/other-api/serve] +[create-remix]: ../other-api/create-remix +[remix-app-server]: ../other-api/serve [repo access token]: https://github.com/settings/tokens/new?description=Remix%20Private%20Stack%20Access&scopes=repo [inquirer]: https://npm.im/inquirer [read-the-feature-announcement-blog-post]: /blog/remix-stacks diff --git a/docs/guides/v2.md b/docs/guides/v2.md index 75b6cc5bb46..b8e798e893d 100644 --- a/docs/guides/v2.md +++ b/docs/guides/v2.md @@ -14,7 +14,7 @@ All v2 APIs and behaviors are available in v1 with [Future Flags][future-flags]. You can keep using the old convention with `@remix-run/v1-route-convention` even after upgrading to v2 if you don't want to make the change right now (or ever, it's just a convention, and you can use whatever file organization you prefer). -```sh +```shellscript nonumber npm i @remix-run/v1-route-convention ``` @@ -46,40 +46,44 @@ module.exports = { A routes folder that looks like this in v1: -```txt bad -routes -├── __auth -│ ├── login.tsx -│ ├── logout.tsx -│ └── signup.tsx -├── __public -│ ├── about-us.tsx -│ ├── contact.tsx -│ └── index.tsx -├── dashboard -│ ├── calendar -│ │ ├── $day.tsx + +```markdown bad +app/ +├── routes/ +│ ├── __auth/ +│ │ ├── login.tsx +│ │ ├── logout.tsx +│ │ └── signup.tsx +│ ├── __public/ +│ │ ├── about-us.tsx +│ │ ├── contact.tsx │ │ └── index.tsx -│ ├── projects -│ │ ├── $projectId -│ │ │ ├── collaborators.tsx -│ │ │ ├── edit.tsx -│ │ │ ├── index.tsx -│ │ │ ├── settings.tsx -│ │ │ └── tasks.$taskId.tsx -│ │ ├── $projectId.tsx -│ │ └── new.tsx -│ ├── calendar.tsx -│ ├── index.tsx -│ └── projects.tsx -├── __auth.tsx -├── __public.tsx -└── dashboard.projects.$projectId.print.tsx +│ ├── dashboard/ +│ │ ├── calendar +│ │ │ ├── $day.tsx +│ │ │ └── index.tsx +│ │ ├── projects/ +│ │ │ ├── $projectId/ +│ │ │ │ ├── collaborators.tsx +│ │ │ │ ├── edit.tsx +│ │ │ │ ├── index.tsx +│ │ │ │ ├── settings.tsx +│ │ │ │ └── tasks.$taskId.tsx +│ │ │ ├── $projectId.tsx +│ │ │ └── new.tsx +│ │ ├── calendar.tsx +│ │ ├── index.tsx +│ │ └── projects.tsx +│ ├── __auth.tsx +│ ├── __public.tsx +│ └── dashboard.projects.$projectId.print.tsx +└── root.tsx ``` Becomes this with `v2_routeConvention`: -``` + +```markdown good routes ├── _auth.login.tsx ├── _auth.logout.tsx @@ -101,7 +105,8 @@ routes ├── dashboard.projects.$projectId.tsx ├── dashboard.projects.new.tsx ├── dashboard.projects.tsx -└── dashboard_.projects.$projectId.print.tsx +├── dashboard_.projects.$projectId.print.tsx +└── root.tsx ``` Note that parent routes are now grouped together instead of having dozens of routes between them (like the auth routes). Routes with the same path but not the same nesting (like `dashboard` and `dashboard_`) also group together. @@ -110,27 +115,30 @@ With the new convention, any route can be a directory with a `route.tsx` file in For example, we can move `_public.tsx` to `_public/route.tsx` and then co-locate modules the route uses: -```txt -routes -├── _auth.tsx -├── _public -│ ├── footer.tsx -│ ├── header.tsx -│ └── route.tsx -├── _public._index.tsx -├── _public.about-us.tsx -└── etc. + +```markdown +app/ +├── routes/ +│ ├── _auth.tsx +│ ├── _public/ +│ │ ├── footer.tsx +│ │ ├── header.tsx +│ │ └── route.tsx +│ ├── _public._index.tsx +│ ├── _public.about-us.tsx +│ └── etc. +└── root.tsx ``` For more background on this change, see the [original "flat routes" proposal][flat-routes]. ## Route `headers` -In Remix v2, the behavior for route `headers` functions is changing slightly. You can opt-into this new behavior ahead of time via the `future.v2_headers` flag in `remix.config.js`. +In Remix v2, the behavior for route `headers` functions has changed slightly. You can opt-into this new behavior ahead of time via the `future.v2_headers` flag in `remix.config.js`. -In v1, Remix would only use the result of the leaf "rendered" route `headers` function. It was your responsibility to add a `headers` function to every potential leaf and merge in `parentHeaders` accordingly. This can get tedious quickly and is also easy to forget to add a `headers` function when you add a new route, even if you want it to just share the same headers from it's parent. +In v1, Remix would only use the result of the leaf "rendered" route `headers` function. It was your responsibility to add a `headers` function to every potential leaf and merge in `parentHeaders` accordingly. This can get tedious quickly and is also easy to forget to add a `headers` function when you add a new route, even if you want it to just share the same headers from its parent. -In v2, Remix will use the deepest `headers` function that it finds in the rendered routes. This more easily allows you to share headers across routes from a common ancestor. Then as needed you can add `headers` functions to deeper routes if they require specific behavior. +In v2, Remix now uses the deepest `headers` function that it finds in the rendered routes. This more easily allows you to share headers across routes from a common ancestor. Then as needed you can add `headers` functions to deeper routes if they require specific behavior. ## Route `meta` @@ -148,7 +156,7 @@ You can update your `meta` exports with the `@remix-run/v1-meta` package to cont Using the `metaV1` function, you can pass in the `meta` function's arguments and the same object it currently returns. This function will use the same merging logic to merge the leaf route's meta with its **direct parent route** meta before converting it to an array of meta descriptors usable in v2. -```tsx filename=app/routes/v1-route.tsx +```tsx bad filename=app/routes/v1-route.tsx export function meta() { return { title: "...", @@ -158,7 +166,7 @@ export function meta() { } ``` -```tsx filename=app/routes/v2-route.tsx +```tsx filename=app/routes/v2-route.tsx good import { metaV1 } from "@remix-run/v1-meta"; export function meta(args) { @@ -186,7 +194,7 @@ export function meta(args) { Becomes: -```tsx filename=app/routes/v2-route.tsx +```tsx filename=app/routes/v2-route.tsx good import { getMatchesData } from "@remix-run/v1-meta"; export function meta(args) { @@ -197,7 +205,7 @@ export function meta(args) { #### Updating to the new `meta` -```tsx filename=app/routes/v1-route.tsx +```tsx bad filename=app/routes/v1-route.tsx export function meta() { return { title: "...", @@ -207,7 +215,7 @@ export function meta() { } ``` -```tsx filename=app/routes/v2-route.tsx +```tsx filename=app/routes/v2-route.tsx good export function meta() { return [ { title: "..." }, @@ -231,7 +239,7 @@ export function meta() { Note that in v1 the objects returned from nested routes were all merged, you will need to manage the merge yourself now with `matches`: -```tsx filename=app/routes/v2-route.tsx +```tsx filename=app/routes/v2-route.tsx good export function meta({ matches }) { const rootMeta = matches[0].meta; const title = rootMeta.find((m) => m.title); @@ -256,18 +264,20 @@ export function meta({ matches }) { } ``` -The [meta v2][meta-v2] docs have more tips on merging route meta. +The [meta][meta] docs have more tips on merging route meta. ## `CatchBoundary` and `ErrorBoundary` ```js filename=remix.config.js /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { - future: {}, + future: { + v2_errorBoundary: true, + }, }; ``` -In v1, a thrown `Response` will render the closest `CatchBoundary` while all other unhandled exceptions render the `ErrorBoundary`. In v2 there is no `CatchBoundary` and all unhandled exceptions will render the `ErrorBoundary`, response or otherwise. +In v1, a thrown `Response` rendered the closest `CatchBoundary` while all other unhandled exceptions rendered the `ErrorBoundary`. In v2 there is no `CatchBoundary` and all unhandled exceptions will render the `ErrorBoundary`, response or otherwise. Additionally, the error is no longer passed to `ErrorBoundary` as props but is accessed with the `useRouteError` hook. @@ -348,7 +358,7 @@ module.exports = { }; ``` -Multiple APIs return the `formMethod` of a submission. In v1 they return a lowercase version of the method but in v2 they return the UPPERCASE version. This is to bring it in line with HTTP and `fetch` specifications. +Multiple APIs return the `formMethod` of a submission. In v1 they returned a lowercase version of the method but in v2 they return the UPPERCASE version. This is to bring it in line with HTTP and `fetch` specifications. ```tsx function Something() { @@ -448,7 +458,7 @@ function Component() { In Remix v1, GET submissions such as `
` or `submit({}, { method: 'get' })` went from `idle -> submitting -> idle` in `transition.state`. This is not quite semantically correct since even though you're "submitting" a form, you're performing a GET navigation and only executing loaders (not actions). Functionally, it's no different from a `` or `navigate()` except that the user may be specifying the search param values via inputs. -In v2, GET submissions are more accurately reflected as loading navigations and thus go `idle -> loading -> idle` to align `navigation.state` with the behavior of normal links. If your GET submission came from a `` or `submit()`, then `useNavigation.form*` will be populated so you can differentiate if needed. +In v2, GET submissions are more accurately reflected as loading navigations and thus go `idle -> loading -> idle` to align `navigation.state` with the behavior of normal links. If your GET submission came from a `` or `submit()`, then `useNavigation.form*` will be populated, so you can differentiate if needed. ## `useFetcher` @@ -527,13 +537,13 @@ function Component() { In Remix v1, GET submissions such as `` or `fetcher.submit({}, { method: 'get' })` went from `idle -> submitting -> idle` in `fetcher.state`. This is not quite semantically correct since even though you're "submitting" a form, you're performing a GET request and only executing a loader (not an action). Functionally, it's no different from a `fetcher.load()` except that the user may be specifying the search param values via inputs. -In v2, GET submissions are more accurately reflected as loading requests and thus go `idle -> loading -> idle` to align `fetcher.state` with the behavior of normal fetcher loads. If your GET submission came from a `` or `fetcher.submit()`, then `fetcher.form*` will be populated so you can differentiate if needed. +In v2, GET submissions are more accurately reflected as loading requests and thus go `idle -> loading -> idle` to align `fetcher.state` with the behavior of normal fetcher loads. If your GET submission came from a `` or `fetcher.submit()`, then `fetcher.form*` will be populated, so you can differentiate if needed. ## Links `imagesizes` and `imagesrcset` -Route `links` properties should all be the React camelCase values instead of HTML lowercase values. These two values snuck in as lowercase in v1. In v2 only the camelCase versions will be valid: +Route `links` properties should all be the React camelCase values instead of HTML lowercase values. These two values snuck in as lowercase in v1. In v2 only the camelCase versions are valid: -```tsx filename=app/routes/v1-route.tsx +```tsx bad filename=app/routes/v1-route.tsx export const links: LinksFunction = () => { return [ { @@ -546,7 +556,7 @@ export const links: LinksFunction = () => { }; ``` -```tsx filename=app/routes/v2-route.tsx +```tsx filename=app/routes/v2-route.tsx good export const links: V2_LinksFunction = () => { return [ { @@ -599,7 +609,7 @@ Remix used to create more than a single module for the server, but it now create ## `serverBuildTarget` -Instead of specifying a build target, use the [Remix Config][remix-config] options to generate the server build your server target expects. This change allows Remix to deploy to more JavaScript runtimes, servers, and hosts without Remix source code needing to know about them. +Instead of specifying a build target, use the [`remix.config.js`][remix-config] options to generate the server build your server target expects. This change allows Remix to deploy to more JavaScript runtimes, servers, and hosts without Remix source code needing to know about them. The following configurations should replace your current `serverBuildTarget`: @@ -681,17 +691,18 @@ module.exports = { ## `serverModuleFormat` -The default server module output format will be changing from `cjs` to `esm`. +The default server module output format has changed from `cjs` to `esm`. -In your `remix.config.js`, you should specify either `serverModuleFormat: "cjs"` to retain existing behavior, or `serverModuleFormat: "esm"`, to opt into the future behavior. +In your `remix.config.js`, you should specify either `serverModuleFormat: "cjs"` to retain existing behavior, or `serverModuleFormat: "esm"`, to opt into the new behavior. ## `serverNodeBuiltinsPolyfill` -Polyfills for Node.js built-in modules will no longer be provided by default for non-Node.js server platforms. +Polyfills for Node.js built-in modules are longer be provided by default for non-Node.js server platforms. -If you are targeting a non-Node.js server platform and want to opt into the future default behavior, in `remix.config.js` you should first remove all server polyfills by providing an empty object for `serverNodeBuiltinsPolyfill.modules`: +If you are targeting a non-Node.js server platform and want to opt into the new default behavior, in `remix.config.js` you should first remove all server polyfills by providing an empty object for `serverNodeBuiltinsPolyfill.modules`: ```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { serverNodeBuiltinsPolyfill: { modules: {}, @@ -702,6 +713,7 @@ module.exports = { You can then reintroduce any polyfills (or blank polyfills) as required. ```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { serverNodeBuiltinsPolyfill: { modules: { @@ -767,13 +779,14 @@ module.exports = { ## `remix dev` -For configuration options, see the [`remix dev` docs][v2-dev-config]. +For configuration options, see the [`remix dev` docs][dev-docs]. ### `remix-serve` If you are using the Remix App Server (`remix-serve`), enable `v2_dev`: ```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { future: { v2_dev: true, @@ -791,58 +804,59 @@ or follow these steps: 1. Enable `v2_dev`: -```js filename=remix.config.js -module.exports = { - future: { - v2_dev: true, - }, -}; -``` + ```js filename=remix.config.js + /** @type {import('@remix-run/dev').AppConfig} */ + module.exports = { + future: { + v2_dev: true, + }, + }; + ``` 2. Update `scripts` in `package.json`: -- Replace any `remix watch` with `remix dev` -- Remove redundant `NODE_ENV=development` -- Use `-c` / `--command` to run your app server + - Replace any `remix watch` with `remix dev` + - Remove redundant `NODE_ENV=development` + - Use `-c` / `--command` to run your app server -For example: + For example: -```diff filename=package.json -{ - "scripts": { -- "dev:remix": "cross-env NODE_ENV=development remix watch", -- "dev:server": "cross-env NODE_ENV=development node ./server.js" -+ "dev": "remix dev -c 'node ./server.js'", - } -} -``` + ```diff filename=package.json + { + "scripts": { + - "dev:remix": "cross-env NODE_ENV=development remix watch", + - "dev:server": "cross-env NODE_ENV=development node ./server.js" + + "dev": "remix dev -c 'node ./server.js'", + } + } + ``` 3. Send a "ready" message to the Remix compiler once your app is running -```ts filename=server.js lines=[1-2,11] -import { broadcastDevReady } from "@remix-run/node"; -// import { logDevReady } from "@remix-run/cloudflare" // use `logDevReady` if using CloudFlare + ```ts filename=server.js lines=[1-2,11] + import { broadcastDevReady } from "@remix-run/node"; + // import { logDevReady } from "@remix-run/cloudflare" // use `logDevReady` if using CloudFlare -const BUILD_DIR = path.join(process.cwd(), "build"); + const BUILD_DIR = path.join(process.cwd(), "build"); -// ... code setting up your server goes here ... + // ... code setting up your server goes here ... -const port = 3000; -app.listen(port, async () => { - console.log(`👉 http://localhost:${port}`); - broadcastDevReady(await import(BUILD_DIR)); -}); -``` + const port = 3000; + app.listen(port, async () => { + console.log(`👉 http://localhost:${port}`); + broadcastDevReady(await import(BUILD_DIR)); + }); + ``` 4. (Optional) `--manual` -If you were relying on `require` cache purging, you can keep doing so by using the `--manual` flag: + If you were relying on `require` cache purging, you can keep doing so by using the `--manual` flag: -```sh -remix dev --manual -c 'node ./server.js' -``` + ```shellscript nonumber + remix dev --manual -c 'node ./server.js' + ``` -Check out the [manual mode guide][manual-mode] for more details. + Check out the [manual mode guide][manual-mode] for more details. ## `installGlobals` @@ -858,7 +872,7 @@ installGlobals(); Source map support is now a responsibility of the app server. If you are using `remix-serve`, nothing is required. If you are using your own app server, you will need to install [`source-map-support`][source-map-support] yourself. -```sh +```shellscript nonumber npm i source-map-support ``` @@ -872,48 +886,48 @@ sourceMapSupport.install(); The `@remix-run/netlify` runtime adapter has been deprecated in favor of [`@netlify/remix-adapter`][official-netlify-adapter] & [`@netlify/remix-edge-adapter`][official-netlify-edge-adapter] -and will be removed in Remix v2. Please update your code by changing all `@remix-run/netlify` imports to +and is now removed as of Remix v2. Please update your code by changing all `@remix-run/netlify` imports to `@netlify/remix-adapter`.\ Keep in mind that `@netlify/remix-adapter` requires `@netlify/functions@^1.0.0`, which is a breaking change compared to the current supported `@netlify/functions` versions in `@remix-run/netlify`. -Due to the removal of this adapter, we will also be removing our [Netlify template][netlify-template] in favor of the +Due to the removal of this adapter, we also removed our [Netlify template][netlify-template] in favor of the [official Netlify template][official-netlify-template]. ## Vercel adapter -The `@remix-run/vercel` runtime adapter has been deprecated in favor of out of the box Vercel functionality and will -be removed in Remix v2. Please update your code by removing `@remix-run/vercel` & `@vercel/node` from your +The `@remix-run/vercel` runtime adapter has been deprecated in favor of out of the box Vercel functionality and is now +removed as of Remix v2. Please update your code by removing `@remix-run/vercel` & `@vercel/node` from your `package.json`, removing your `server.js`/`server.ts` file, and removing the `server` & `serverBuildPath` options from your `remix.config.js`. -Due to the removal of this adapter, we will also be removing our [Vercel template][vercel-template] in favor of the +Due to the removal of this adapter, we also removed our [Vercel template][vercel-template] in favor of the [official Vercel template][official-vercel-template]. ## Built-in PostCSS/Tailwind support -In v2, these tools will be automatically used within the Remix compiler if PostCSS and/or Tailwind configuration files are present in your project. +In v2, these tools are automatically used within the Remix compiler if PostCSS and/or Tailwind configuration files are present in your project. If you have a custom PostCSS and/or Tailwind setup outside of Remix that you'd like to maintain when migrating to v2, you can disable these features in your `remix.config.js`. ```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { postcss: false, tailwind: false, }; ``` -[future-flags]: ./api-development-strategy +[future-flags]: ../start/future-flags [remix-config]: ../file-conventions/remix-config [flat-routes]: https://github.com/remix-run/remix/discussions/4482 -[meta-v2]: ../route/meta-v2 +[meta]: ../route/meta [meta-v2-rfc]: https://github.com/remix-run/remix/discussions/4462 [meta-v2-matches]: #the-matches-argument [v1-16-release-notes]: https://github.com/remix-run/remix/releases/tag/remix%401.16.0 -[v2-dev-config]: ../other-api/dev-v2 [templates]: https://github.com/remix-run/remix/tree/main/templates -[v2-dev-docs]: ../other-api/dev-v2 -[manual-mode]: ../guides/manual-mode +[dev-docs]: ../other-api/dev +[manual-mode]: ./manual-mode [source-map-support]: https://www.npmjs.com/package/source-map-support [official-netlify-adapter]: https://github.com/netlify/remix-compute/tree/main/packages/remix-adapter [official-netlify-edge-adapter]: https://github.com/netlify/remix-compute/tree/main/packages/remix-edge-adapter diff --git a/docs/index.md b/docs/index.md index bc2471272d3..69dc940a952 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ hidden: true # Remix Docs -```sh +```shellscript nonumber npx create-remix@latest ``` @@ -24,7 +24,7 @@ npx create-remix@latest

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.

+

Remix v2 is here! All 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.

@@ -64,5 +64,3 @@ npx create-remix@latest - -[git-hub]: https://github.com/remix-run/remix diff --git a/docs/other-api/serve.md b/docs/other-api/serve.md index 1f688ce35b4..0bb3ae67459 100644 --- a/docs/other-api/serve.md +++ b/docs/other-api/serve.md @@ -7,7 +7,15 @@ order: 3 Remix is designed for you to own your server, but if you don't want to set one up you can use the Remix App Server instead. It's a production-ready, but basic Node.js server built with Express. If you find you want to customize it, use the `@remix-run/express` adapter instead. -```sh +## `HOST` environment variable + +You can configure the hostname for your Express app via `process.env.HOST` and that value will be passed to the internal [`app.listen`][express-listen] method when starting the server. + +```shellscript nonumber +HOST=127.0.0.1 npx remix-serve build/ +``` + +```shellscript nonumber remix-serve ``` @@ -15,18 +23,10 @@ remix-serve You can change the port of the server with an environment variable. -```sh +```shellscript nonumber PORT=4000 npx remix-serve ``` -## `HOST` environment variable - -You can configure the hostname for your Express app via `process.env.HOST` and that value will be passed to the internal [`app.listen`][express-listen] method when starting the server. - -```sh -HOST=127.0.0.1 npx remix-serve build/ -``` - ## Development Environment Depending on `process.env.NODE_ENV`, the server will boot in development or production mode. @@ -35,7 +35,7 @@ The `server-build-path` needs to point to the `serverBuildDirectory` defined in Because only the build artifacts (`build/`, `public/build/`) need to be deployed to production, the `remix.config.js` is not guaranteed to be available in production, so you need to tell Remix where your server build is with this option. -In development, `remix-serve` will ensure the latest code is run by purging the require cache for every request. This has some effects on your code you might need to be aware of: +In development, `remix-serve` will ensure the latest code is run by purging the `require` cache for every request. This has some effects on your code you might need to be aware of: - Any values in the module scope will be "reset" @@ -72,10 +72,10 @@ In development, `remix-serve` will ensure the latest code is run by purging the } ``` - If you need to write your code in a way that has these types of module side-effects, you should set up your own [@remix-run/express][remix-run-express] server and a tool in development like pm2-dev or nodemon to restart the server on file changes instead. + If you need to write your code in a way that has these types of module side effects, you should set up your own [@remix-run/express][remix-run-express] server and a tool in development like pm2-dev or nodemon to restart the server on file changes instead. In production this doesn't happen. The server boots up and that's the end of it. -[remix-run-express]: adapter#createrequesthandler -[remember]: ./dev-v2#keeping-in-memory-server-state-across-rebuilds +[remix-run-express]: ./adapter#createrequesthandler +[remember]: ../guides/manual-mode#keeping-in-memory-server-state-across-rebuilds [express-listen]: https://expressjs.com/en/api.html#app.listen diff --git a/docs/route/action.md b/docs/route/action.md index 710911297de..236c91121a8 100644 --- a/docs/route/action.md +++ b/docs/route/action.md @@ -6,7 +6,7 @@ title: action Watch the 📼 Remix Singles: Data Mutations with Form + action and Multiple Forms and Single Button Mutations -A route `action` is a server only function to handle data mutations and other actions. If a non-GET request is made to your route (POST, PUT, PATCH, DELETE) then the action is called before the loaders. +A route `action` is a server only function to handle data mutations and other actions. If a non-`GET` request is made to your route (`POST`, `PUT`, `PATCH`, `DELETE`) then the action is called before the loaders. Actions have the same API as loaders, the only difference is when they are called. This enables you to co-locate everything about a data set in a single route module: the data read, the component that renders the data, and the data writes: @@ -44,7 +44,7 @@ export default function Todos() { } ``` -When a POST is made to a URL, multiple routes in your route hierarchy will match the URL. Unlike a GET to loaders, where all of them are called to build the UI, _only one action is called_. +When a `POST` is made to a URL, multiple routes in your route hierarchy will match the URL. Unlike a `GET` to loaders, where all of them are called to build the UI, _only one action is called_. The route called will be the deepest matching route, unless the deepest matching route is an "index route". In this case, it will post to the parent route of the index (because they share the same URL, the parent wins). @@ -65,4 +65,5 @@ See also: [form]: ../components/form [form action]: ../components/form#action -[index query param]: ../guides/routing#what-is-the-index-query-param + +[index query param]: ../discussion/02-routes#what-is-the-index-query-param diff --git a/docs/route/meta.md b/docs/route/meta.md index 2ba906ac49d..1b0a149f8b0 100644 --- a/docs/route/meta.md +++ b/docs/route/meta.md @@ -250,8 +250,9 @@ If you can't avoid the merge problem with global meta or index routes, we've cre [links-export]: ./links [use-matches]: ../hooks/use-matches [merging-metadata-across-the-route-hierarchy]: #merging-with-parent-meta -[url-params]: ../guides/routing#dynamic-segments +[url-params]: ../file-conventions/routes#dynamic-segments [root-route]: ../file-conventions/root -[index-route]: ../guides/routing#index-routes + +[index-route]: ../discussion/02-routes#index-routes [matches]: #matches [merge-meta]: https://gist.github.com/ryanflorence/ec1849c6d690cfbffcb408ecd633e069 diff --git a/docs/route/should-revalidate.md b/docs/route/should-revalidate.md index f64c90af7ae..be40d183112 100644 --- a/docs/route/should-revalidate.md +++ b/docs/route/should-revalidate.md @@ -221,9 +221,9 @@ export async function loader({ params }: LoaderArgs) { There are a lot of ways to do this, and the rest of the code in the app matters, but ideally you don't think about the UI you're trying to optimize (the search params changing) but instead look at the values your loader cares about. In our case, it only cares about the projectId, so we can check two things: - did the params stay the same? -- was it a GET and not a mutation? +- was it a `GET` and not a mutation? -If the params didn't change, and we didn't do a POST, then we know our loader will return the same data it did last time, so we can opt out of the revalidation when the child route changes the search params. +If the params didn't change, and we didn't do a `POST`, then we know our loader will return the same data it did last time, so we can opt out of the revalidation when the child route changes the search params. ```tsx filename=app/routes/$projectId.tsx export function shouldRevalidate({ @@ -243,4 +243,4 @@ export function shouldRevalidate({ } ``` -[url-params]: ../guides/routing#dynamic-segments +[url-params]: ../file-conventions/routes#dynamic-segments diff --git a/docs/start/community.md b/docs/start/community.md index 2f58b4a89e8..64ce25690e8 100644 --- a/docs/start/community.md +++ b/docs/start/community.md @@ -28,7 +28,7 @@ To that end, please keep in mind [our code of conduct][our-code-of-conduct]. - [Moulton][moulton] - Community Remix newsletter -- [Releases on GitHub][releases-on-git-hub] - Not a bad idea to subscribe to Remix releases so you know what's coming. +- [Releases on GitHub][releases-on-git-hub] - Not a bad idea to subscribe to Remix releases, so you know what's coming. [our-code-of-conduct]: https://github.com/remix-run/remix/blob/main/CODE_OF_CONDUCT.md [remix-discord-server]: https://rmx.as/discord diff --git a/docs/start/future-flags.md b/docs/start/future-flags.md index 711d41993d2..678419faf1b 100644 --- a/docs/start/future-flags.md +++ b/docs/start/future-flags.md @@ -14,7 +14,8 @@ In our approach to software development, we aim to achieve the following goals f We introduce new features into the current release with a future flag in remix.config that looks something like `unstable_someFeature`. -```tsx filename=remix.config.js +```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ export default { future: { unstable_someFeature: true, @@ -32,7 +33,8 @@ export default { When we introduce breaking changes, we do so within the context of the current major version, and we hide them behind future flags. For instance, if we're in `v2`, a breaking change might be placed under a future flag named `v3_somethingDifferent`. -```tsx filename=remix.config.js +```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ export default { future: { v3_someFeature: true, diff --git a/docs/styling/bundling.md b/docs/styling/bundling.md index 46198e4a980..f895ed19244 100644 --- a/docs/styling/bundling.md +++ b/docs/styling/bundling.md @@ -18,25 +18,27 @@ Remix does not insert the CSS bundle into the page automatically so that you hav To get access to the CSS bundle, first install the `@remix-run/css-bundle` package. -```sh +```shellscript nonumber npm install @remix-run/css-bundle ``` -Then, import `cssBundleHref` and add it to a link descriptor—most likely in `root.tsx` so that it applies to your entire application. +Then, import `cssBundleHref` and add it to a link descriptor—most likely in `app/root.tsx` so that it applies to your entire application. + +```tsx filename=app/root.tsx +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno -```tsx filename=root.tsx lines=[1,5-7] import { cssBundleHref } from "@remix-run/css-bundle"; -export const links = () => { - return [{ rel: "stylesheet", href: cssBundleHref }]; -}; +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: cssBundleHref }, +]; ``` With this link tag inserted into the page, you're now ready to start using the various CSS bundling features built into Remix. ## Limitations -Avoid using `export *` due to an [issue with esbuild's CSS tree shaking][esbuild-css-tree-shaking-issue]. +Avoid using `export *` due to an [issue with `esbuild`'s CSS tree shaking][esbuild-css-tree-shaking-issue]. [esbuild-css-tree-shaking-issue]: https://github.com/evanw/esbuild/issues/1370 [css-side-effect-imports]: ./css-imports diff --git a/docs/styling/css-imports.md b/docs/styling/css-imports.md index f2df52fb739..68e6e8ee787 100644 --- a/docs/styling/css-imports.md +++ b/docs/styling/css-imports.md @@ -4,7 +4,7 @@ title: CSS Imports # CSS Side Effect Imports -Some NPM packages use side effect imports of plain CSS files (e.g. `import "./styles.css"`) to declare the CSS dependencies of JavaScript files. If you want to consume one of these packages, first ensure you've set up \[CSS bundling]\[css-bundling] in your application. +Some NPM packages use side effect imports of plain CSS files (e.g. `import "./styles.css"`) to declare the CSS dependencies of JavaScript files. If you want to consume one of these packages, first ensure you've set up [CSS bundling][css-bundling] in your application. For example, a module may have source code like this: @@ -30,4 +30,5 @@ module.exports = { }; ``` +[css-bundling]: ./bundling [server-dependencies-to-bundle]: ../file-conventions/remix-config#serverdependenciestobundle diff --git a/docs/styling/css-in-js.md b/docs/styling/css-in-js.md index de5fee34a7f..0fd5221acc1 100644 --- a/docs/styling/css-in-js.md +++ b/docs/styling/css-in-js.md @@ -6,7 +6,7 @@ title: CSS in JS Most CSS-in-JS approaches aren't recommended to be use in Remix because they require your app to render completely before you know what the styles are. This is a performance issue and prevents streaming features like [`defer`][defer]. -Here's some sample code to show how you might use Styled Components with Remix (you can also \[find a runnable example in the Remix examples repository]\[styled-components-example]): +Here's some sample code to show how you might use Styled Components with Remix (you can also [find a runnable example in the Remix examples repository][styled-components-example]): 1. First you'll need to put a placeholder in your root component to control where the styles are inserted. @@ -46,9 +46,9 @@ Here's some sample code to show how you might use Styled Components with Remix ( } ``` -2. Your `entry.server.tsx` will look something like this: +2. Your `app/entry.server.tsx` will look something like this: - ```tsx filename=entry.server.tsx lines=[7,16,19-24,26-27] + ```tsx filename=app/entry.server.tsx lines=[7,16,19-24,26-27] import type { AppLoadContext, EntryContext, @@ -86,8 +86,11 @@ Here's some sample code to show how you might use Styled Components with Remix ( } ``` -Other CSS-in-JS libraries will have a similar setup. If you've got a CSS framework working well with Remix, please \[contribute an example]\[examples]! +Other CSS-in-JS libraries will have a similar setup. If you've got a CSS framework working well with Remix, please [contribute an example][examples]! -NOTE: You may run into hydration warnings when using Styled Components. Hopefully \[this issue]\[styled-components-issue] will be fixed soon. +NOTE: You may run into hydration warnings when using Styled Components. Hopefully [this issue][styled-components-issue] will be fixed soon. [defer]: ../utils/defer +[styled-components-example]: https://github.com/remix-run/examples/tree/main/styled-components +[examples]: https://github.com/remix-run/examples +[styled-components-issue]: https://github.com/styled-components/styled-components/issues/3660 diff --git a/docs/styling/css.md b/docs/styling/css.md index d369cee02b6..a6b5b7ea497 100644 --- a/docs/styling/css.md +++ b/docs/styling/css.md @@ -4,7 +4,7 @@ title: Regular CSS # Regular CSS -Remix helps you scale an app with regular CSS with nested routes and `links`. +Remix helps you scale an app with regular CSS with nested routes and [`links`][links]. CSS Maintenance issues can creep into a web app for a few reasons. It can get difficult to know: @@ -19,27 +19,33 @@ Remix alleviates these issues with route-based stylesheets. Nested routes can ea Each route can add style links to the page, for example: ```tsx filename=app/routes/dashboard.tsx +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import styles from "~/styles/dashboard.css"; -export function links() { - return [{ rel: "stylesheet", href: styles }]; -} +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, +]; ``` ```tsx filename=app/routes/dashboard.accounts.tsx +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import styles from "~/styles/accounts.css"; -export function links() { - return [{ rel: "stylesheet", href: styles }]; -} +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, +]; ``` ```tsx filename=app/routes/dashboard.sales.tsx +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import styles from "~/styles/sales.css"; -export function links() { - return [{ rel: "stylesheet", href: styles }]; -} +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, +]; ``` Given these routes, this table shows which CSS will apply at specific URLs: @@ -58,7 +64,7 @@ Websites large and small usually have a set of shared components used throughout #### Shared stylesheet -The first approach is very simple. Put them all in a `shared.css` file included in `app/root.tsx`. That makes it easy for the components themselves to share CSS code (and your editor to provide intellisense for things like \[custom properties]\[custom-properties]), and each component already needs a unique module name in JavaScript anyway, so you can scope the styles to a unique class name or data attribute: +The first approach is very simple. Put them all in a `shared.css` file included in `app/root.tsx`. That makes it easy for the components themselves to share CSS code (and your editor to provide intellisense for things like [custom properties][custom-properties]), and each component already needs a unique module name in JavaScript anyway, so you can scope the styles to a unique class name or data attribute: ```css filename=app/styles/shared.css /* scope with class names */ @@ -107,10 +113,12 @@ Note that these are not routes, but they export `links` functions as if they wer } ``` -```tsx filename=app/components/button/index.tsx lines=[1,3-5] +```tsx filename=app/components/button/index.tsx lines=[1,3,5-7] +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import styles from "./styles.css"; -export const links = () => [ +export const links: LinksFunction = () => [ { rel: "stylesheet", href: styles }, ]; @@ -131,12 +139,14 @@ And then a `` that extends it: } ``` -```tsx filename=app/components/primary-button/index.tsx lines=[1,6,13] +```tsx filename=app/components/primary-button/index.tsx lines=[3,8,15] +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import { Button, links as buttonLinks } from "../button"; import styles from "./styles.css"; -export const links = () => [ +export const links: LinksFunction = () => [ ...buttonLinks(), { rel: "stylesheet", href: styles }, ]; @@ -157,35 +167,37 @@ Because these buttons are not routes, and therefore not associated with a URL se Consider that `app/routes/_index.tsx` uses the primary button component: -```tsx filename=app/routes/_index.tsx lines=[1-4,9] +```tsx filename=app/routes/_index.tsx lines=[3-6,10] +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import { PrimaryButton, links as primaryButtonLinks, } from "~/components/primary-button"; import styles from "~/styles/index.css"; -export function links() { - return [ - ...primaryButtonLinks(), - { rel: "stylesheet", href: styles }, - ]; -} +export const links: LinksFunction = () => [ + ...primaryButtonLinks(), + { rel: "stylesheet", href: styles }, +]; ``` Now Remix can prefetch, load, and unload the styles for `button.css`, `primary-button.css`, and the route's `index.css`. An initial reaction to this is that routes have to know more than you want them to. Keep in mind that each component must be imported already, so it's not introducing a new dependency, just some boilerplate to get the assets. For example, consider a product category page like this: -```tsx filename=app/routes/$category.tsx lines=[1-5] +```tsx filename=app/routes/$category.tsx lines=[3-7] +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + import { AddFavoriteButton } from "~/components/add-favorite-button"; import { ProductDetails } from "~/components/product-details"; import { ProductTile } from "~/components/product-tile"; import { TileGrid } from "~/components/tile-grid"; import styles from "~/styles/$category.css"; -export function links() { - return [{ rel: "stylesheet", href: styles }]; -} +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, +]; export default function Category() { const products = useLoaderData(); @@ -244,7 +256,7 @@ While that's a bit of boilerplate it enables a lot: - Co-located styles with your components - The only CSS ever loaded is the CSS that's used on the current page - When your components aren't used by a route, their CSS is unloaded from the page -- Remix will prefetch the CSS for the next page with \[``]\[link] +- Remix will prefetch the CSS for the next page with [``][link] - When one component's styles change, browser and CDN caches for the other components won't break because they are all have their own URLs. - When a component's JavaScript changes but its styles don't, the cache is not broken for the styles @@ -283,7 +295,7 @@ export const CopyToClipboard = React.forwardRef( CopyToClipboard.displayName = "CopyToClipboard"; ``` -Not only will this make the asset high priority in the network tab, but Remix will turn that `preload` into a `prefetch` when you link to the page with \[``]\[link], so the SVG background is prefetched, in parallel, with the next route's data, modules, stylesheets, and any other preloads. +Not only will this make the asset high priority in the network tab, but Remix will turn that `preload` into a `prefetch` when you link to the page with [``][link], so the SVG background is prefetched, in parallel, with the next route's data, modules, stylesheets, and any other preloads. ### Link Media Queries @@ -314,3 +326,7 @@ export const links: LinksFunction = () => { ]; }; ``` + +[links]: ../route/links +[custom-properties]: https://developer.mozilla.org/en-US/docs/Web/CSS/--* +[link]: ../components/link diff --git a/docs/styling/postcss.md b/docs/styling/postcss.md index d0f30a5f169..b490c7bc927 100644 --- a/docs/styling/postcss.md +++ b/docs/styling/postcss.md @@ -8,7 +8,7 @@ title: PostCSS For example, to use [Autoprefixer][autoprefixer], first install the PostCSS plugin. -```sh +```shellscript nonumber npm install -D autoprefixer ``` @@ -22,7 +22,7 @@ module.exports = { }; ``` -If you're using [Vanilla Extract][vanilla-extract-2], since it's already playing the role of CSS preprocessor, you may want to apply a different set of PostCSS plugins relative to other styles. To support this, you can export a function from your PostCSS config file which is given a context object that lets you know when Remix is processing a Vanilla Extract file. +If you're using [Vanilla Extract][vanilla-extract], since it's already playing the role of CSS preprocessor, you may want to apply a different set of PostCSS plugins relative to other styles. To support this, you can export a function from your PostCSS config file which is given a context object that lets you know when Remix is processing a Vanilla Extract file. ```js filename=postcss.config.cjs module.exports = (ctx) => { @@ -42,7 +42,7 @@ module.exports = (ctx) => { You can use CSS preprocessors like LESS and SASS. Doing so requires running an additional build process to convert these files to CSS files. This can be done via the command line tools provided by the preprocessor or any equivalent tool. -Once converted to CSS by the preprocessor, the generated CSS files can be imported into your components via the \[Route Module `links` export]\[route-module-links] function, or included via \[side effect imports]\[css-side-effect-imports] when using \[CSS bundling]\[css-bundling], just like any other CSS file in Remix. +Once converted to CSS by the preprocessor, the generated CSS files can be imported into your components via the [Route Module `links` export][route-module-links] function, or included via [side effect imports][css-side-effect-imports] when using [CSS bundling][css-bundling], just like any other CSS file in Remix. To ease development with CSS preprocessors you can add npm scripts to your `package.json` that generate CSS files from your SASS or LESS files. These scripts can be run in parallel alongside any other npm scripts that you run for developing a Remix application. @@ -50,53 +50,56 @@ An example using SASS. 1. First you'll need to install the tool your preprocess uses to generate CSS files. -```sh -npm add -D sass -``` + ```shellscript nonumber + npm add -D sass + ``` 2. Add an npm script to your `package.json`'s `scripts` section that uses the installed tool to generate CSS files. -```json filename=package.json -{ - // ... - "scripts": { - // ... - "sass": "sass --watch app/:app/" - } - // ... -} -``` + ```jsonc filename=package.json + { + // ... + "scripts": { + // ... + "sass": "sass --watch app/:app/" + } + // ... + } + ``` -The above example assumes SASS files will be stored somewhere in the `app` folder. + The above example assumes SASS files will be stored somewhere in the `app` folder. -The `--watch` flag included above will keep `sass` running as an active process, listening for changes to or for any new SASS files. When changes are made to the source file, `sass` will regenerate the CSS file automatically. Generated CSS files will be stored in the same location as their source files. + The `--watch` flag included above will keep `sass` running as an active process, listening for changes to or for any new SASS files. When changes are made to the source file, `sass` will regenerate the CSS file automatically. Generated CSS files will be stored in the same location as their source files. 3. Run the npm script. -```sh -npm run sass -``` + ```shellscript nonumber + npm run sass + ``` -This will start the `sass` process. Any new SASS files, or changes to existing SASS files, will be detected by the running process. + This will start the `sass` process. Any new SASS files, or changes to existing SASS files, will be detected by the running process. -You might want to use something like `concurrently` to avoid needing two terminal tabs to generate your CSS files and also run `remix dev`. + You might want to use something like `concurrently` to avoid needing two terminal tabs to generate your CSS files and also run `remix dev`. -```sh -npm add -D concurrently -``` + ```shellscript nonumber + npm add -D concurrently + ``` -```json filename=package.json -{ - "scripts": { - "dev": "concurrently \"npm run sass\" \"remix dev\"" - } -} -``` + ```json filename=package.json + { + "scripts": { + "dev": "concurrently \"npm run sass\" \"remix dev\"" + } + } + ``` -Running `npm run dev` will run the specified commands in parallel in a single terminal window. + Running `npm run dev` will run the specified commands in parallel in a single terminal window. [postcss]: https://postcss.org [autoprefixer]: https://github.com/postcss/autoprefixer +[vanilla-extract]: ./vanilla-extract +[route-module-links]: ../route/links +[css-side-effect-imports]: ./css-imports +[css-bundling]: ./bundling [postcss-preset-env]: https://preset-env.cssdb.org [esbuild-css-tree-shaking-issue]: https://github.com/evanw/esbuild/issues/1370 -[vanilla-extract-2]: #vanilla-extract diff --git a/docs/styling/tailwind.md b/docs/styling/tailwind.md index f65fcf025b7..adbd47ee56b 100644 --- a/docs/styling/tailwind.md +++ b/docs/styling/tailwind.md @@ -8,19 +8,19 @@ Perhaps the most popular way to style a Remix application in the community is to Remix supports tailwind automatically if `tailwind.config.js` is present in the root of your project. You can disable it in [Remix Config][remix-config] -Tailwind has the benefits of inline-style co-location for developer ergonomics and is able to generate a CSS file for Remix to import. The generated CSS file generally caps out to a reasonable size, even for large applications. Load that file into the `root.tsx` links and be done with it. +Tailwind has the benefits of inline-style co-location for developer ergonomics and is able to generate a CSS file for Remix to import. The generated CSS file generally caps out to a reasonable size, even for large applications. Load that file into the `app/root.tsx` links and be done with it. If you don't have any CSS opinions, this is a great approach. To use Tailwind, first install it as a dev dependency: -```sh +```shellscript nonumber npm install -D tailwindcss ``` Then initialize a config file: -```sh +```shellscript nonumber npx tailwindcss init --ts ``` @@ -62,7 +62,7 @@ export const links: LinksFunction = () => [ With this setup in place, you can also use [Tailwind's functions and directives][tailwind-functions-and-directives] anywhere in your CSS. Note that Tailwind will warn that no utility classes were detected in your source files if you never used it before. -Tailwind doesn't compile CSS for older browsers by default, so if you'd like to achieve this using a PostCSS-based tool like \[Autoprefixer]\[autoprefixer], you'll need to leverage Remix's [built-in PostCSS support][built-in-post-css-support]. When using both PostCSS and Tailwind, the Tailwind plugin will be automatically included if it's missing, but you can also choose to manually include the Tailwind plugin in your PostCSS config instead if you prefer. +Tailwind doesn't compile CSS for older browsers by default, so if you'd like to achieve this using a PostCSS-based tool like [Autoprefixer][autoprefixer], you'll need to leverage Remix's [built-in PostCSS support][built-in-post-css-support]. When using both PostCSS and Tailwind, the Tailwind plugin will be automatically included if it's missing, but you can also choose to manually include the Tailwind plugin in your PostCSS config instead if you prefer. If you're using VS Code, it's recommended you install the [Tailwind IntelliSense extension][tailwind-intelli-sense-extension] for the best developer experience. @@ -75,8 +75,9 @@ Tailwind replaces its import statements with inlined CSS but this can result in Alternatively, you can use [PostCSS][built-in-post-css-support] with the [postcss-import] plugin to process imports before passing them to esbuild. [tailwind]: https://tailwindcss.com +[remix-config]: ../file-conventions/remix-config#tailwind [tailwind-functions-and-directives]: https://tailwindcss.com/docs/functions-and-directives -[tailwind-intelli-sense-extension]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss +[autoprefixer]: https://github.com/postcss/autoprefixer [built-in-post-css-support]: ./postcss +[tailwind-intelli-sense-extension]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss [postcss-import]: https://github.com/postcss/postcss-import -[remix-config]: ../file-conventions/remix-config#tailwind diff --git a/docs/styling/vanilla-extract.md b/docs/styling/vanilla-extract.md index 18d36a7a176..59e8861229f 100644 --- a/docs/styling/vanilla-extract.md +++ b/docs/styling/vanilla-extract.md @@ -10,7 +10,7 @@ To use the built-in Vanilla Extract support, first ensure you've set up [CSS bun Then, install Vanilla Extract's core styling package as a dev dependency. -```sh +```shellscript nonumber npm install -D @vanilla-extract/css ``` diff --git a/docs/tutorials/blog.md b/docs/tutorials/blog.md index 37a73313d7e..f60f3bb2b8a 100644 --- a/docs/tutorials/blog.md +++ b/docs/tutorials/blog.md @@ -32,7 +32,7 @@ If you want to follow this tutorial locally on your own computer, it is importan 💿 Initialize a new Remix project. We'll call ours "blog-tutorial" but you can call it something else if you'd like. -```sh +```shellscript nonumber npx create-remix@latest --template remix-run/indie-stack blog-tutorial ``` @@ -51,7 +51,7 @@ We're using [the Indie stack][the-indie-stack], which is a full application read 💿 Let's start the dev server: -```sh +```shellscript nonumber npm run dev ``` @@ -94,7 +94,7 @@ Back in the browser go ahead and click the link. You should see a 404 page since 💿 Create a new file at `app/routes/posts._index.tsx` -```sh +```shellscript nonumber touch app/routes/posts._index.tsx ``` @@ -202,7 +202,7 @@ A solid practice is to create a module that deals with a particular concern. In 💿 Create `app/models/post.server.ts` -```sh +```shellscript nonumber touch app/models/post.server.ts ``` @@ -270,7 +270,7 @@ model Post { 💿 Let's generate a migration file for our schema changes, which will be required if you deploy your application rather than just running in dev mode locally. This will also update our local database and TypeScript definitions to match the schema change. We'll name the migration "create post model". -```sh +```shellscript nonumber npx prisma migrate dev --name "create post model" ``` @@ -364,7 +364,7 @@ Instead of creating a route for every single one of our posts, we can use a "dyn 💿 Create a dynamic route at `app/routes/posts.$slug.tsx` -```sh +```shellscript nonumber touch app/routes/posts.$slug.tsx ``` @@ -490,7 +490,7 @@ Now let's get that markdown parsed and rendered to HTML to the page. There are a 💿 Parse the markdown into HTML -```sh +```shellscript nonumber npm add marked # additionally, if using typescript npm add @types/marked -D @@ -554,7 +554,7 @@ Put that anywhere in the component. I stuck it right under the `

`. 💿 Create an admin route at `app/routes/posts.admin.tsx`: -```sh +```shellscript nonumber touch app/routes/posts.admin.tsx ``` @@ -608,7 +608,7 @@ Let's fill in that placeholder with an index route for admin. Hang with us, we'r 💿 Create an index route for `posts.admin.tsx`'s child routes -```sh +```shellscript nonumber touch app/routes/posts.admin._index.tsx ``` @@ -681,7 +681,7 @@ Maybe this will help, let's add the `/posts/admin/new` route and see what happen 💿 Create the `app/routes/posts.admin.new.tsx` file -```sh +```shellscript nonumber touch app/routes/posts.admin.new.tsx ``` @@ -1014,9 +1014,9 @@ That's it for today! Here are some bits of homework to implement if you want to **Update/Delete posts:** make a `posts.admin.$slug.tsx` page for your posts. This should open an edit page for the post that allows you to update the post or even delete it. The links are already there in the sidebar, but they return 404! Create a new route that reads the posts, and puts them into the fields. All the code you need is already in `app/routes/posts.$slug.tsx` and `app/routes/posts.admin.new.tsx`. You just gotta put it together. -**Optimistic UI:** You know how when you favorite a tweet, the heart goes red instantly and if the tweet is deleted it reverts back to empty? That's Optimistic UI: assume the request will succeed, and render what the user will see if it does. So your homework is to make it so when you hit "Create" it renders the post in the left nav and renders the "Create a New Post" link (or if you add update/delete do it for those too). You'll find this ends up being easier than you think even if it takes you a second to arrive there (and if you've implemented this pattern in the past, you'll find Remix makes this much easier). Learn more from [the Optimistic UI guide][the-optimistic-ui-guide]. +**Optimistic UI:** You know how when you favorite a tweet, the heart goes red instantly and if the tweet is deleted it reverts back to empty? That's Optimistic UI: assume the request will succeed, and render what the user will see if it does. So your homework is to make it so when you hit "Create" it renders the post in the left nav and renders the "Create a New Post" link (or if you add update/delete do it for those too). You'll find this ends up being easier than you think even if it takes you a second to arrive there (and if you've implemented this pattern in the past, you'll find Remix makes this much easier). Learn more from [the Pending UI guide][the-pending-ui-guide]. -**Authenticated users only:** Another cool bit of homework you could do is make it so only authenticated users can create posts. You've already got authentication all set up for you thanks to the Indie Stack. Tip: if you want to make it so you're the only one who can make posts, simply check the user's email in your loaders and actions and if it's not yours redirect them [somewhere][somewhere] 😈 +**Authenticated users only:** Another cool bit of homework you could do is make it so only authenticated users can create posts. You've already got authentication all set up for you thanks to the Indie Stack. Tip: if you want to make it, so you're the only one who can make posts, simply check the user's email in your loaders and actions and if it's not yours redirect them [somewhere][somewhere] 😈 **Customize the app:** If you're happy with Tailwind CSS, keep it around, otherwise, check [the styling guide][the-styling-guide] to learn of other options. Remove the `Notes` model and routes, etc. Whatever you want to make this thing yours. @@ -1030,17 +1030,17 @@ We hope you love Remix! 💿 👋 [node-js]: https://nodejs.org [npm]: https://www.npmjs.com [vs-code]: https://code.visualstudio.com -[the-stacks-docs]: /pages/stacks +[the-stacks-docs]: ../guides/templates#stacks [the-indie-stack]: https://github.com/remix-run/indie-stack [fly-io]: https://fly.io [http-localhost-3000]: http://localhost:3000 [screenshot-of-the-app-showing-the-blog-post-link]: https://user-images.githubusercontent.com/1500684/160208939-34fe20ed-3146-4f4b-a68a-d82284339c47.png [tailwind]: https://tailwindcss.com -[the-styling-guide]: ../guides/styling +[the-styling-guide]: ../styling/tailwind [prisma]: https://prisma.io [http-localhost-3000-posts-admin]: http://localhost:3000/posts/admin [mdn-request]: https://developer.mozilla.org/en-US/docs/Web/API/Request [mdn-request-form-data]: https://developer.mozilla.org/en-US/docs/Web/API/Request/formData [disable-java-script]: https://developer.chrome.com/docs/devtools/javascript/disable -[the-optimistic-ui-guide]: /guides/optimistic-ui +[the-pending-ui-guide]: ../discussion/07-pending-ui [somewhere]: https://www.youtube.com/watch?v=dQw4w9WgXcQ