Skip to content

chore: Renames document to render context to app context #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/src/content/docs/core/react-server-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export default function MyClientComponent() {
React Server Components run on the server, they can easily fetch data and make it part of the payload that's sent to the client.

```tsx title="src/app/pages/todos/TodoPage.tsx" "async"
export async function Todos({ ctx }) {
const todos = await db.todo.findMany({ where: { userId: ctx.user.id } });
export async function Todos({ appContext }) {
const todos = await db.todo.findMany({ where: { userId: appContext.user.id } });
return (
<ol>
{todos.map((todo) => (
Expand All @@ -39,19 +39,19 @@ export async function Todos({ ctx }) {
);
}

export async function TodoPage({ ctx }) {
export async function TodoPage({ appContext }) {
return (
<div>
<h1>Todos</h1>
<Suspense fallback={<div>Loading...</div>}>
<Todos ctx={ctx} />
<Todos appContext={appContext} />
</Suspense>
</div>
);
}

---
The `TodoPage` component is a server component. It it rendered by a route, so it receives the `ctx` object. We pass this to the `Todos` component, which is also a server component,
The `TodoPage` component is a server component. It it rendered by a route, so it receives the `appContext` object. We pass this to the `Todos` component, which is also a server component,
and renders the todos.
---
```
Expand All @@ -69,9 +69,9 @@ Allow you to execute code on the server from a client component.
```tsx title="@/pages/todos/functions.tsx" mark={1}
"use server";

export async function addTodo(formData: FormData, { ctx }: RouteOptions) {
export async function addTodo(formData: FormData, { appContext }: RouteOptions) {
const title = formData.get("title");
await db.todo.create({ data: { title, userId: ctx.user.id } });
await db.todo.create({ data: { title, userId: appContext.user.id } });
}

---
Expand Down
53 changes: 26 additions & 27 deletions docs/src/content/docs/core/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import { route } from "@redwoodjs/sdk/router";

export default defineApp([
// Middleware
function middleware({ request, env, ctx }) { /* Modify context */ },
function middleware({ request, env, ctx }) { /* Modify context */ },
function middleware({ request, env, appContext }) { /* Modify context */ },
function middleware({ request, env, appContext }) { /* Modify context */ },
// Request Handlers
route("/", function handler({ request, env, ctx }) {
route("/", function handler({ request, env, appContext }) {
return new Response("Hello, world!")
}),
route("/ping", function handler({ request, env, ctx }) {
route("/ping", function handler({ request, env, appContext }) {
return new Response("Pong!")
}),
]);
Expand Down Expand Up @@ -87,7 +87,7 @@ The request handler is a function, or array of functions (See [Interruptors](#in
import { route } from "@redwoodjs/sdk/router";

defineApp([
route("/a-standard-response", ({ request, params, env, ctx }) => {
route("/a-standard-response", ({ request, params, env, appContext }) => {
return new Response("Hello, world!")
}),
route('/a-jsx-response', () => {
Expand All @@ -100,7 +100,7 @@ The request handler function takes in the following options:
1. `request`: The request object.
2. `params`: The matched parameters from the request URL.
3. `env`: The Cloudflare environment.
4. `ctx`: The context object (See [Middleware & Context](#middleware-context)).
4. `appContext`: The context object (See [Middleware & `AppContext`](#middleware--app-context)).
5. `cf:` Cloudflare's [Execution Context API](https://developers.cloudflare.com/workers/runtime-apis/context/) methods, e.g. `waitUntil()`

Return values:
Expand All @@ -118,9 +118,9 @@ import { defineApp } from "@redwoodjs/sdk/worker";
import { route } from "@redwoodjs/sdk/router";
import { EditBlogPage } from "src/pages/blog/EditBlogPage";

function isAuthenticated({ request, env, ctx }) {
function isAuthenticated({ request, env, appContext }) {
// Ensure that this user is authenticated
if (!ctx.user) {
if (!appContext.user) {
return new Response("Unauthorized", { status: 401 })
}
}
Expand All @@ -134,9 +134,9 @@ For the `/blog/:slug/edit` route, the `isAuthenticated` function will be execute
---
```

## Middleware & Context
## Middleware & `AppContext`

Context is a mutable object that is passed to each request handler, interruptors, and React Server Functions. It's used to share data between the different parts of your application. You populate the context on a per-request basis via Middleware.
`appContext` is a mutable object that is passed to each request handler, interruptors, and React Server Functions. It's used to share data between the different parts of your application. You populate the context on a per-request basis via Middleware.

Middleware runs before the request is matched to a route. You can specify multiple middleware functions, they'll be executed in the order they are defined.

Expand All @@ -146,53 +146,52 @@ import { route } from "@redwoodjs/sdk/router";

defineApp([
sessionMiddleware,
async function getUserMiddleware({ request, env, ctx }) {
if (ctx.session.userId) {
ctx.user = await db.user.find({ where: { id: ctx.session.userId } });
async function getUserMiddleware({ request, env, appContext }) {
if (appContext.session.userId) {
appContext.user = await db.user.find({ where: { id: appContext.session.userId } });
}
},
route("/hello", [
function ({ ctx }) {
if (!ctx.user) {
function ({ appContext }) {
if (!appContext.user) {
return new Response("Unauthorized", { status: 401 });
}
},
function ({ ctx }) {
return new Response(`Hello ${ctx.user.username}!`);
function ({ appContext }) {
return new Response(`Hello ${appContext.user.username}!`);
},
]),
]);

---
The `ctx` object:
The `appContext` object:

1. `sessionMiddleware` is a function that is used to populate the `ctx.session` object
2. `getUserMiddleware` is a middleware function that is used to populate the `ctx.user` object
1. `sessionMiddleware` is a function that is used to populate the `appContext.session` object
2. `getUserMiddleware` is a middleware function that is used to populate the `appContext.user` object
3. `"/hello"` is a an array of route handlers that are executed when "/hello" is matched:
- if the user is not authenticated the request will be interrupted and a 401 Unauthorized response will be returned
- if the user is authenticated the request will be passed to the next request handler and `"Hello {ctx.user.username}!"` will be returned
- if the user is authenticated the request will be passed to the next request handler and `"Hello {appContext.user.username}!"` will be returned

---
```

## Documents

Documents are how you define the "shell" of your application's html: the `<html>`, `<head>`, `<meta>` tags, scripts, stylesheets, `<body>`, and where in the `<body>` your actual page content is rendered. In RedwoodSDK, documents are defined using the `document` function in `defineApp`.
Documents are how you define the "shell" of your application's html: the `<html>`, `<head>`, `<meta>` tags, scripts, stylesheets, `<body>`, and where in the `<body>` your actual page content is rendered. In RedwoodSDK, you tell it which document to use with the `render()` function in `defineApp`. In other words, you're asking RedwoodSDK to "render" the document.

```tsx title="src/worker.tsx" "document"
import { defineApp } from "@redwoodjs/sdk/worker";
import { route, document } from "@redwoodjs/sdk/router";
import { route, render } from "@redwoodjs/sdk/router";

import { Document } from "@/pages/Document";
import { HomePage } from "@/pages/HomePage";

export default defineApp([
document(
Document, [route("/", HomePage)]
)]);
render(Document, [route("/", HomePage)])
]);

---
The `document` function takes a React component and an array of route handlers. The document will be applied to all the routes that are passed to it.
The `render` function takes a React component and an array of route handlers. The document will be applied to all the routes that are passed to it.

This component will be rendered on the server side when the page loads. When defining this component, you'd add:
* Your application's stylesheets and scripts
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/core/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export const Document = ({ rw, children }) => (
</html>
);

export default defineApp<Context>([
export default defineApp<AppContext>([
// ...
document(Document, [
render(Document, [
// ...
]),
]);
Expand Down
20 changes: 10 additions & 10 deletions docs/src/content/docs/getting-started/first-project.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ import { Home } from 'src/pages/Home';

...

export default defineApp<Context>([
({ ctx }) => {
// setup ctx here
ctx;
export default defineApp<AppContext>([
({ appContext }) => {
// setup appContext here
appContext;
},
document(Document, [
render(Document, [
index([
Home,
]),
Expand Down Expand Up @@ -114,12 +114,12 @@ You can add additional routes by:
```tsx
import { About } from "src/pages/About";

export default defineApp<Context>([
({ ctx }) => {
// setup ctx here
ctx;
export default defineApp<AppContext>([
({ appContext }) => {
// setup appContext here
appContext;
},
document(Document, [index([Home]), route("/about", About)]),
render(Document, [index([Home]), route("/about", About)]),
]);
```

Expand Down
32 changes: 16 additions & 16 deletions docs/src/content/docs/reference/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Route functions handle incoming requests and return either a Response object or
- `request`: The incoming Request object
- `params`: URL parameters parsed from the route definition
- `env`: CloudFlare environment variables and bindings
- `ctx`: A mutable object for storing request-scoped data
- `appContext`: A mutable object for storing request-scoped data

Here's a basic example:

Expand All @@ -50,7 +50,7 @@ Here's a basic example:
code={`\
import { route } from "@redwoodjs/sdk/router";

route("/", function ({ request, params, env, ctx }) {
route("/", function ({ request, params, env, appContext }) {
return new Response("Hello, world!", { status: 200 });
});`} />

Expand Down Expand Up @@ -82,8 +82,8 @@ import { route } from "@redwoodjs/sdk/router";
export default defineApp([
route("/user/settings", [
// Authentication check
function checkAuth({ ctx }) {
if (!ctx.user) {
function checkAuth({ appContext }) {
if (!appContext.user) {
return Response.redirect("/user/login", 302);
}
},
Expand All @@ -96,7 +96,7 @@ export default defineApp([

The interruptors pattern complements the global middleware system, allowing you to apply route-specific checks without cluttering your main request functions.

Note: All request functions in the chain receive the same route context (`request`, `params`, `env`, `ctx`), making it easy to share data between functions.
Note: All request functions in the chain receive the same route context (`request`, `params`, `env`, `appContext`), making it easy to share data between functions.

## Global Middleware

Expand All @@ -107,7 +107,7 @@ Global middleware allows you to inspect or modify every request and response in
title="src/worker.tsx"
code={`\
export default defineApp([
async function getUserMiddleware({ request, env, ctx }) {
async function getUserMiddleware({ request, env, appContext }) {
const session = getSession({ request, env })
try {
const user = await db.user.findFirstOrThrow({
Expand All @@ -119,13 +119,13 @@ export default defineApp([
id: session?.userId
}
})
ctx.user = user
appContext.user = user
} catch {
ctx.user = null
appContext.user = null
}
},
route('/', function({ ctx }) {
return new Response(\`You are logged in as "\${ctx.user.email}"\`)
route('/', function({ appContext }) {
return new Response(\`You are logged in as "\${appContext.user.email}"\`)
})
])`}
/>
Expand All @@ -134,9 +134,9 @@ In this example, we define a global middleware function `getUserMiddleware` that

1. Retrieves the user's session information
2. Attempts to fetch the user from the database using the session's userId
3. Stores the user object in the request context (`ctx`)
3. Stores the user object in the request context (`appContext`)

The `ctx` object is then available to all subsequent route handlers and middleware functions.
The `appContext` object is then available to all subsequent route handlers and middleware functions.

## JSX

Expand Down Expand Up @@ -180,7 +180,7 @@ By default, RedwoodSDK only renders your component's HTML without `<html>`, `<he
title="src/worker.tsx"
code={`\
import { defineApp } from "@redwoodjs/sdk/worker";
import { route, document } from "@redwoodjs/sdk/router";
import { route, render } from "@redwoodjs/sdk/router";

function Document({ children }) {
return (
Expand All @@ -194,7 +194,7 @@ return (
); }

export default defineApp([
document(Document, [
render(Document, [
route("/", Homepage)
])
]);`} />
Expand Down Expand Up @@ -245,13 +245,13 @@ Here's an example using both helpers:
language="tsx"
code={`\
import { defineApp } from "@redwoodjs/sdk/worker";
import { index, document, prefix } from "@redwoodjs/sdk/router";
import { index, render, prefix } from "@redwoodjs/sdk/router";
import { authRoutes } from 'src/pages/auth/routes';
import { invoiceRoutes } from 'src/pages/invoice/routes';
import HomePage from 'src/pages/Home/HomePage';

export default defineApp([
document(Document, [
render(Document, [
// Define root route using index
index([
HomePage,
Expand Down
8 changes: 4 additions & 4 deletions experiments/ai-stream/src/worker.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { defineApp } from "@redwoodjs/sdk/worker";
import { index, document } from "@redwoodjs/sdk/router";
import { index, render } from "@redwoodjs/sdk/router";
import { Document } from "src/Document";
import { Chat } from "src/pages/Chat/Chat";
import { setCommonHeaders } from "src/headers";

type Context = {};
type AppContext = {};

export default defineApp<Context>([
export default defineApp<AppContext>([
setCommonHeaders(),
document(Document, [index([Chat])]),
render(Document, [index([Chat])]),
]);
7 changes: 2 additions & 5 deletions experiments/billable/docs/Routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,16 @@ const router = defineRoutes([
})
])

router.handle({ request, ctx, env, renderPage })
router.handle({ request, appContext, env, renderPage })
```


# API Reference

- `defineRoutes`
- `route`
- `index`
- `prefix`


# Links

We also include an interface to generate paths in a typesafe way. This allows you to confidently
Expand Down Expand Up @@ -88,15 +86,14 @@ link('/invoice/:id', { id: 1 })

```


## TODO

- Type safety. How do we ensure that the params have types? Maybe the route array has some sort of response... Like the type that it returns is a function that returns a thing... That's interesting.

Ok. That seems like a possible way forward. What else to consider?

- Type casting? Should we consider have the ability to cast things via the router? Seems like an overreach to me.
Loaders. Stick with Suspense boundary. I kinda see the benefit of been able to declare this on the component itself... Or near the component.
Loaders. Stick with Suspense boundary. I kinda see the benefit of been able to declare this on the component itself... Or near the component.

- Don't hide files. I want to be able to follow the request-response cycle in my own code. What does that mean?
- We should expose the express (or something else) part of the framework. The user should invoke a function to pass the request off to Redwood SDK
Expand Down
Loading