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
  • Loading branch information
lazarv committed Jan 18, 2025
1 parent d09c5f5 commit 5ce1508
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 5ce1508

Please sign in to comment.