Skip to content

Commit

Permalink
feat: enhanced error handling and a ton of bugfixes showcased in a ne…
Browse files Browse the repository at this point in the history
…w example app (#111)

This update brings significant improvements, including a brand-new
__Pokémon example app__ designed to demonstrate the framework's enhanced
error handling and caching capabilities and to showcase superb
performance. In addition to the example app, this PR introduces a series
of crucial bug fixes, navigation improvements, and optimizations to both
client-side and server-side behavior.

A new `Form` navigation component has been introduced to simplify
navigation based on form interactions. This component allows developers
to trigger navigation based on form submissions or specific input
values, without requiring additional event handlers or imperative
navigation logic. This enables form-driven navigation patterns where a
user’s input can dictate the next page, improving declarative control
over navigation behavior.

A new mechanism for controlling when client-side navigation triggers
revalidation has been added. Previously, navigation could result in
unnecessary revalidation of server-side rendering, even in cases where
it was not required. This led to excessive network requests and
performance overhead. Now, developers can explicitly define revalidation
strategies, ensuring that only the relevant components are refreshed
based on user interactions or navigation. This fine-grained control
allows for more efficient client-side navigation while maintaining
consistency.

A global error component has been introduced to standardize error
handling across applications. Instead of handling errors in multiple
locations with redundant logic, applications can now define a
centralized error component that gracefully captures and displays
errors. This ensures a consistent error experience for users while also
simplifying maintenance by reducing duplicated error handling logic.
This feature also enables styling unexpected error caught during
server-side rendering.

Several issues related to module resolution when using `npx` have been
addressed. Previously, certain dependencies would not resolve correctly
when executed the react-server CLI via `npx`, causing unexpected
failures when running commands that relied on specific
`@lazarv/react-server` modules. These resolution issues have now been
fixed, ensuring that executing commands via `npx` correctly locates and
loads the necessary modules without manual intervention.

A fix has been applied to server function calls to ensure that
`redirect` responses behave correctly. Previously, certain redirect
scenarios did not properly propagate the response to the client,
resulting in broken navigation flows. This has now been corrected,
allowing server functions to issue redirects that are correctly handled
by the client. #110

Default CORS configuration has been adjusted to address inconsistencies
in handling requests across different origins. The previous
implementation had cases where preflight requests were either
incorrectly blocked or missing required headers. The fixed configuration
usage ensures that cross-origin requests function correctly,
particularly for applications interacting with APIs and remote
components from different domains.

React Server Component network requests have been updated to support
credential transmission. This change ensures that cookies are correctly
included in all relevant RSC network requests. This improvement allows
for more seamless authentication handling without requiring manual
workarounds.

Support for the `"use cache"` directive has been extended to additional
use cases. Previously, this directive was limited in scope and did not
fully cover all expected scenarios, particularly when used in certain
asynchronous server function calls. These fixes ensure that `"use
cache"` now applies more broadly and reliably across different execution
contexts, allowing more efficient caching behavior and reducing
redundant server computations.

Several issues affecting `RemoteComponent` and remote rendering have
been addressed. These fixes ensure that remotely loaded components
behave consistently, resolving previous issues where rendering states
were not properly handled. Remote rendering now correctly handles
component hydration using both sync and async React Server Components,
preventing unexpected behaviour.

File-system based routing has been improved to correctly apply error
boundaries at the appropriate levels, particularly for route outlets.
Previously, some errors were not caught at the intended boundary,
causing them to propagate unintentionally. With this update, errors
encountered within a specific route segment are correctly handled by the
nearest applicable error boundary, ensuring better application stability
and predictable error recovery.

The documentation has been updated to include details on the new APIs
introduced in this release. This includes explanations of the enhanced
error handling system, details on how to use the new `Form` navigation
component, and guidelines for configuring client-side navigation
revalidation. Additionally, examples have been added to clarify best
practices for handling errors at different levels of an application.
  • Loading branch information
lazarv authored Jan 19, 2025
1 parent d09c5f5 commit 3c4e6bc
Show file tree
Hide file tree
Showing 101 changed files with 4,868 additions and 1,362 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
}
}
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
4 changes: 2 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"@lazarv/react-server": "workspace:^",
"@lazarv/react-server-adapter-vercel": "workspace:^",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"@vercel/analytics": "^1.4.1",
"@vercel/speed-insights": "^1.1.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"algoliasearch": "^4.24.0",
"highlight.js": "^11.9.0",
Expand Down
File renamed without changes.
173 changes: 167 additions & 6 deletions docs/src/pages/en/(pages)/router/client-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,25 @@ export default function Home() {
}
```

<Link name="form">
## Form
</Link>

You can navigate to a new page by using the `Form` client component exported by the `@lazarv/react-server/navigation` module. This component will navigate to the current route on form submit using form data as the query parameters.

```jsx
import { Form } from "@lazarv/react-server/navigation";

export default function Home() {
return (
<Form>
<input name="name" />
<button type="submit">Submit</button>
</Form>
);
}
```

<Link name="outlet">
## Outlet
</Link>
Expand Down Expand Up @@ -216,11 +235,11 @@ Using both `defer` and `url` props on the `ReactServerComponent` you can achieve

The `useClient` hook returns an object with the following properties:

- `navigate(url: string, options: { rollback?: number })`: A function that navigates to a new page. The `rollback` option allows you to cache the current page for a specified amount of time.
- `replace(url: string, options: { rollback?: number })`: A function that replaces the current page with a new page. The `rollback` option allows you to cache the current page for a specified amount of time.
- `prefetch(url: string, options: { ttl?: number })`: A function that prefetches a page. The `ttl` option allows you to cache the page for a specified amount of time.
- `refresh()`: A function that refreshes the current page.

- `navigate(url: string, options: { outlet?: string; push?: boolean; rollback?: number; signal?: AbortSignal; fallback?: React.ReactNode; Component?: React.ReactNode })`: A function that navigates to a new page or the specified outlet. The `rollback` option allows you to cache the current page for a specified amount of time.
- `replace(url: string, options: { outlet?: string; rollback?: number; signal?: AbortSignal; fallback?: React.ReactNode; Component?: React.ReactNode })`: A function that replaces the current page with a new page or the content of the specified outlet. The `rollback` option allows you to cache the current page for a specified amount of time.
- `prefetch(url: string, options: { outlet?: string; ttl?: number; signal?: AbortSignal })`: A function that prefetches a page or the content of the specified outlet. The `ttl` option allows you to cache the page for a specified amount of time.
- `refresh(outlet?: string, options: { signal?: AbortSignal; fallback?: React.ReactNode; Component?: React.ReactNode })`: A function that refreshes the current page or the content of the specified outlet.
- `abort(outlet?: string, reason?: unknown)`: A function that aborts navigation of the specified outlet.
You can use these functions for programmatic navigation.

```jsx
Expand All @@ -246,4 +265,146 @@ export default function Home() {
</div>
);
}
```
```

<Link name="use-outlet">
## useOutlet
</Link>

The `useOutlet` hook returns a set of functions to interact with the current outlet. These functions are the same as the ones returned by the `useClient` hook, but you don't need to specify the `outlet` option. In the example below, the `navigate` function is scoped to the current outlet, not the entire page. But if the current outlet is the root outlet, the `navigate` function will navigate to the entire page.

```jsx
"use client";

import { useOutlet } from "@lazarv/react-server/client";

export default function Home() {
const { navigate } = useOutlet();

return (
<div>
<button onClick={() => navigate("/about")}>About</button>
</div>
);
}
```

<Link name="fallback">
### Fallback
</Link>

The `fallback` option on the `Link` and `Refresh` components allows you to specify a fallback component to render until the outlet starts to render the React Server Component. This can be useful when you want to render a loading indicator or a skeleton while the React Server Component is being rendered immediately, without waiting for the React Server Component request to reach the server.

```jsx
import { Link } from "@lazarv/react-server/navigation";

export default function Home() {
return (
<div>
<Link to="/about" target="content" fallback={<div>Loading...</div>}>About</Link>
</div>
);
}
```

You can also specify the `fallback` option when navigating programmatically with the `navigate` or `replace` functions.

```jsx
import { useClient } from "@lazarv/react-server/client";

export default function Home() {
const { navigate } = useClient();

return (
<div>
<button onClick={() => navigate("/about", { fallback: <div>Loading...</div> })}>About</button>
</div>
);
}
```

<Link name="component">
### Component
</Link>

The `Component` option on the `Link` and `Refresh` components allows you to specify a React component to render. This can be useful when you want to render a React component directly in the outlet instead of making a request to the server.

```jsx
import { Link } from "@lazarv/react-server/navigation";

export default function Home() {
return (
<div>
<Link to="/about" target="content" Component={<div>About</div>}>About</Link>
</div>
);
}
```

You can also specify the `Component` option when navigating programmatically with the `navigate` or `replace` functions.

```jsx
import { useClient } from "@lazarv/react-server/client";

export default function Home() {
const { navigate } = useClient();

return (
<div>
<button onClick={() => navigate("/about", { Component: <div>About</div> })}>About</button>
</div>
);
}
```

<Link name="revalidate">
### Using revalidate
</Link>

You can use the `revalidate` prop on the `Link`, `Refresh` and `Form` components to control client-side caching of the page or content of the specified outlet. The `revalidate` prop can accept a number which represents the number of milliseconds the page or outlet will be cached for the target URL. After the timeout expires, the page will be removed from the cache and the next time the user navigates to the page, the page will be rendered again by fetching the content from the server rendering the React Server Component.

By default, the `revalidate` prop is not set, which means that the page or outlet will not be cached at all, and every navigation will fetch the content from the server.

```jsx
import { Link } from "@lazarv/react-server/navigation";

export default function Home() {
return (
<div>
<Link to="/about" revalidate={5000}>About</Link>
</div>
);
}
```

You can disable revalidation by passing `false` to the `revalidate` prop. By disabling revalidation, the page or outlet will be cached indefinitely.

```jsx
import { Link } from "@lazarv/react-server/navigation";

export default function Home() {
return (
<div>
<Link to="/about" revalidate={false}>About</Link>
</div>
);
}
```

You can also fully customize the logic for revalidation by passing a function to the `revalidate` prop. The function will receive a context object including the `outlet`, the target `url` and the `timestamp` of the cached content. The function should return a boolean indicating whether the content should be revalidated.

```jsx
"use client";

import { Link } from "@lazarv/react-server/navigation";

export default function Home() {
return (
<div>
<Link to="/about" revalidate={async ({ outlet, url, timestamp }) => {
return Math.random() > 0.5;
}}>About</Link>
</div>
);
}
```
30 changes: 29 additions & 1 deletion docs/src/pages/en/(pages)/router/error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,32 @@ export default function FallbackError({ error }) {
}
```

> **Warning:** you can define an error boundary, error fallback or loading component only for layouts, not for pages.
> **Warning:** you can define an error boundary, error fallback or loading component only for layouts, not for pages.
<Link name="global">
## Global error component
</Link>

By default, the global error component is the first `react-server.error.jsx` or `react-server.error.tsx` file found from the root of your app. This component will be used to render the error component when an error occurs during the rendering of a page and no error component is defined for the error.

Your global error component will be used for all errors that are not handled by a more specific error component. The error component will receive the error as a prop.

```jsx
// src/app/react-server.error.tsx
export default function GlobalError({ error }: { error: Error }) {
return <div>{error.message}</div>;
}
```

Optionally, you can specify the global error component in the `react-server.config.json` file. You only need to specify the path to the file when you want to use a file that is not named `react-server.error.jsx` or `react-server.error.tsx`, like `global-error.tsx` or similar, or when you want to use a file specifically for the global error component and not the first the framework can find with the default name.

```jsx
// react-server.config.json
{
"globalErrorComponent": "src/app/react-server.error.tsx"
}
```

Your global error component can be a React Server Component or a client component. When it's a client component, it will be rendered on the client side using a React error boundary, while if it's a React Server Component, it will be rendered on the server side only.

> **Warning:** you can't reset a global error boundary! The page needs to be reloaded to reset the error boundary. Use the `Refresh` component to reload the page using the React Server Component payload. It's recommended to use the `noCache` prop on the `Refresh` component to avoid caching issues.
4 changes: 2 additions & 2 deletions examples/mantine/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();

return (
<html lang="en" data-mantine-color-scheme="light">
<html lang="en" data-mantine-color-scheme="light" suppressHydrationWarning>
<head>
<ColorSchemeScript />
</head>
<body>
<body suppressHydrationWarning>
<MantineProvider theme={theme}>
<ModalsProvider>
<Notifications />
Expand Down
4 changes: 2 additions & 2 deletions examples/photos/src/app/(root).layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function Layout({
modal: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<title>Photos</title>
Expand All @@ -20,7 +20,7 @@ export default function Layout({
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<body suppressHydrationWarning>
<GithubCorner />
{children}
<ReactServerComponent outlet="modal">{modal}</ReactServerComponent>
Expand Down
26 changes: 26 additions & 0 deletions examples/pokemon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@lazarv/react-server-example-pokemon",
"private": true,
"description": "React Server Pokemon example application",
"scripts": {
"dev": "react-server",
"build": "react-server build",
"start": "react-server start"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@lazarv/react-server": "workspace:^",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "6.0.0-alpha.18"
}
}
6 changes: 6 additions & 0 deletions examples/pokemon/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
3 changes: 3 additions & 0 deletions examples/pokemon/react-server.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"root": "src/app"
}
16 changes: 16 additions & 0 deletions examples/pokemon/src/app/(404).[...slug].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { status } from "@lazarv/react-server";
import { Link } from "@lazarv/react-server/navigation";

export default function NotFound() {
status(404);

return (
<div className="fixed inset-0 flex flex-col gap-4 items-center justify-center">
<h1 className="text-4xl font-bold">Not Found</h1>
<p className="text-lg">The page you are looking for does not exist.</p>
<Link to="/" root noCache>
Go back to the home page
</Link>
</div>
);
}
Loading

0 comments on commit 3c4e6bc

Please sign in to comment.