Skip to content

Commit

Permalink
feat: Adding public and protected outlets (#195)
Browse files Browse the repository at this point in the history
* More stuff

* More stuff

* More stuff

* More stuff

* More stuff

* Added changeset file

* Fix CI
  • Loading branch information
patricklafrance authored Sep 12, 2024
1 parent 704b495 commit 98e4839
Show file tree
Hide file tree
Showing 35 changed files with 757 additions and 406 deletions.
75 changes: 75 additions & 0 deletions .changeset/brave-eagles-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
"@squide/react-router": minor
"@squide/firefly": minor
"@squide/core": minor
---

Replaced the `ManagedRoutes` placeholder by the `PublicRoutes` and `ProtectedRoutes` placeholder.

Before:

```tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
element: <RootLayout />,
children: [
ManagedRoutes
]
}, {
hoist: true
});
};
```

Now:

```tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
element: <RootLayout />,
children: [
PublicRoutes,
ProtectedRoutes
]
}, {
hoist: true
});
};
```

Or:

```tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
element: <RootLayout />,
children: [
PublicRoutes,
{
element: <AuthenticationBoundary />,
children: [
{
element: <AuthenticatedLayout />,
children: [
ProtectedRoutes
]
}
]
}
]
}, {
hoist: true
});
};
```

This release also includes a new `runtime.registerPublicRoute()` function.
29 changes: 13 additions & 16 deletions docs/getting-started/create-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,10 @@ export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
};
```

And an [hoisted route](../reference/runtime/runtime-class.md#register-an-hoisted-route) to render the `RootLayout` and the [ManagedRoutes](../reference/routing/managedRoutes.md) placeholder:
And an [hoisted route](../reference/runtime/runtime-class.md#register-an-hoisted-route) to render the `RootLayout` with the [PublicRoutes](../reference/routing/publicRoutes.md) and [ProtectedRoutes](../reference/routing/protectedRoutes.md) placeholders:

```tsx !#8,12,15 host/src/register.tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
```tsx !#8,11,12,15 host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";
import { RootLayout } from "./RootLayout.tsx";

Expand All @@ -244,9 +244,9 @@ export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
// Pathless route to declare a root layout.
element: <RootLayout />,
children: [
// Placeholder to indicate where managed routes (routes that are not hoisted or nested)
// will be rendered.
ManagedRoutes
// Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
PublicRoutes,
ProtectedRoutes
]
}, {
hoist: true
Expand All @@ -260,7 +260,7 @@ export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
```

!!!info
The [ManagedRoutes](../reference/routing/managedRoutes.md) placeholder indicates where routes that are neither hoisted or nested with a [parentPath](../reference/runtime/runtime-class.md#register-nested-navigation-items) or [parentName](../reference/runtime/runtime-class.md#register-a-named-route) option will be rendered. In this example, the homepage route is considered as a managed route and will be rendered under the `ManagedRoutes` placeholder.
The [PublicRoutes](../reference/routing/publicRoutes.md) and [ProtectedRoutes](../reference/routing/protectedRoutes.md) placeholders indicates where routes that are neither hoisted or nested with a [parentPath](../reference/runtime/runtime-class.md#register-nested-navigation-items) or [parentName](../reference/runtime/runtime-class.md#register-a-named-route) option will be rendered. In this example, the homepage route is considered as a protected route and will be rendered under the `ProtectedRoutes` placeholder.
!!!

Finally, update the bootstrapping code to [register](../reference/registration/registerLocalModules.md) the newly created local module:
Expand Down Expand Up @@ -310,30 +310,27 @@ export function NotFoundPage() {

Then, register the newly created component as the `*` route:

```tsx !#8,19-24 host/src/register.tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
```tsx !#18-21 host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";
import { NotFoundPage } from "./NotFoundPage.tsx";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
$name: "root-layout",
element: <RootLayout />,
children: [
// Placeholder to indicate where managed routes (routes that are not hoisted or nested)
// will be rendered.
ManagedRoutes
// Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
PublicRoutes,
ProtectedRoutes
]
}, {
hoist: true
});

runtime.registerRoute({
runtime.registerPublicRoute({
path: "*",
element: <NotFoundPage />
}, {
parentName: "root-layout"
});

runtime.registerRoute({
Expand Down
29 changes: 24 additions & 5 deletions docs/guides/add-a-public-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,40 @@ order: 900

# Add a public route

A route can have one of two visibility values: `"public"` or `"protected"`. However, these visibility values do not determine whether a route is accessible to everyone or restricted to authenticated users. That protection is typically enforced by an [authentication boundary](./add-authentication.md#add-an-authentication-boundary).
A route can be registered as either `public` or `protected`. This visibility indicator does not determine whether a route is accessible to everyone or restricted to authenticated users; that protection is typically enforced by an [authentication boundary](./add-authentication.md#add-an-authentication-boundary).

In a Squide application, if both the [usePublicDataQueries](../reference/tanstack-query/usePublicDataQueries.md) and [useProtectedDataQueries](../reference/tanstack-query/useProtectedDataQueries.md) hooks are defined, the following will occur: if the initially requested route is hinted as `"protected"`, both hooks will execute their respective requests. However, if the requested route is hinted as `"public"`, only the `usePublicDataQueries` hook will execute its requests.
In a Squide application, the visibility indicator determines whether routes will be rendered as children of either the [PublicRoutes](../reference/routing/publicRoutes.md) or [ProtectedRoutes](../reference/routing/protectedRoutes.md) placeholders and whether the [usePublicDataQueries](../reference/tanstack-query/usePublicDataQueries.md) or [useProtectedDataQueries](../reference/tanstack-query/useProtectedDataQueries.md) hooks should execute their requests. If the initially requested route is marked as `protected` and both hooks are defined, each query hook will execute its respective requests. However, if the route is marked as `public`, only the `usePublicDataQueries` hook will execute its requests.

By default, when a route is registered with the [registerRoute](../reference/runtime/runtime-class.md#register-routes) function, the route is considered as `"protected"`. Therefore, if a route and its layout do not rely on the initial protected data of the application, the route should be explicitly declared as `"public"` using the `$visibility` option:
When a route is registered with the [registerRoute](../reference/runtime/runtime-class.md#register-routes) function, it is considered `protected` by default. Therefore, if a route does not rely on the application's global protected data, it should be explicitly registered as `public` using the [registerPublicRoute](../reference/runtime/runtime-class.md#register-a-public-route) function:

```tsx !#6 remote/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { Page } from "./Page.tsx";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
$visibility: "public",
runtime.registerPublicRoute({
path: "/page",
element: <Page />
});
}
```

Don't forget to register the `PublicRoutes` placeholder in the host application:

```tsx !#9 host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
runtime.registerRoute({
element: <RootLayout />,
children: [
// Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
PublicRoutes,
ProtectedRoutes
]
}, {
hoist: true
});
};
```
23 changes: 10 additions & 13 deletions docs/guides/add-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,8 @@ export function RootLayout() {

Finally, assemble everything:

```tsx !#15,19,27-33 host/src/register.tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
```tsx !#16,20,30-33 host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./Rootlayout.tsx";
import { AuthenticationBoundary } from "./AuthenticationBoundary.tsx";
import { LoginPage } from "./LoginPage.tsx";
Expand All @@ -641,37 +641,34 @@ import { NotFoundPage } from "./NotFoundPage.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = async runtime => {
runtime.registerRoute({
$name: "root-layout",
element: <RootLayout />,
children: [
// All the public routes will render before the authenticated layout.
PublicRoutes,
{
// Every page beyond the authenticated boundary are protected.
element: <AuthenticationBoundary />,
children: [
{
// All the managed routes will render the authenticated layout.
// All the protected routes will render the authenticated layout.
element: <AuthenticatedLayout />,
children: ManagedRoutes
children: [
ProtectedRoutes
]
}
]
}
]
});

runtime.registerRoute({
$visibility: "public",
runtime.registerPublicRoute({
path: "/login",
element: <LoginPage />
}, {
parentName: "root-layout"
});

runtime.registerRoute({
$visibility: "public",
runtime.registerPublicRoute({
path: "*",
element: <NotFoundPage />
}, {
parentName: "root-layout"
});

runtime.registerRoute({
Expand Down
5 changes: 3 additions & 2 deletions docs/guides/develop-a-module-in-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function FireflyAppRouter() {
Finally, create a local module to register the **application shell**. This module will be used by both the host application and the isolated modules:

```tsx shell/src/register.tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";
import { RootErrorBoundary } from "./RootErrorBoundary.tsx";

Expand All @@ -90,7 +90,8 @@ export const registerShell: ModuleRegisterFunction<FireflyRuntime> = runtime =>
{
errorElement: <RootErrorBoundary />,
children: [
ManagedRoutes
PublicRoutes,
ProtectedRoutes
]
}
]
Expand Down
7 changes: 4 additions & 3 deletions docs/guides/isolate-module-failures.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Then, update the host application `registerHost` function to declare the `RootEr
A React Router's error boundary is declared with the [errorElement](https://reactrouter.com/en/main/route/error-element) of a route:

```tsx !#7,11 host/src/register.tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";
import { RootErrorBoundary } from "./RootErrorBoundary.tsx";

Expand All @@ -41,7 +41,8 @@ export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
// Default error boundary.
errorElement: <RootErrorBoundary />,
children: [
ManagedRoutes
PublicRoutes,
ProtectedRoutes
]
}
]
Expand All @@ -58,7 +59,7 @@ By implementing this mechanism, the level of failure isolation achieved is **com
If your application is [hoisting pages](../reference/runtime/runtime-class.md#register-an-hoisted-route), it's important to note that they will be rendered outside of the host application's `RootErrorBoundary` component. To prevent breaking the entire application when an hoisted page encounters unhandled errors, it is highly recommended to declare a React Router's error boundary for each hoisted page as well, again using [errorElement](https://reactrouter.com/en/main/route/error-element):

```tsx !#9,11 remote-module/src/register.tsx
import { ManagedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { Page } from "./Page.tsx";
import { RemoteErrorBoundary } from "./RemoteErrorBoundary.tsx";

Expand Down
9 changes: 6 additions & 3 deletions docs/guides/migrate-to-firefly-v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Finally, with `v9`, Squide's philosophy has evolved. We used to describe Squide
- The `useSession` hook has been removed, define your own React context instead.
- The `useIsAuthenticated` hook has been removed, define your own React context instead.
- The `sessionAccessor` option has been removed from the [FireflyRuntime](../reference/runtime/runtime-class.md) options, define your own React context instead.
- The `ManagedRoutes`placeholder has been removed, use [PublicRoutes](../reference/routing/publicRoutes.md) and [ProtectedRoutes](../reference/routing/protectedRoutes.md) instead.

### Renamed

Expand Down Expand Up @@ -244,6 +245,7 @@ export function App() {
- A new [usePublicDataQueries](../reference/tanstack-query/usePublicDataQueries.md) hook is now available.
- A new [useProtectedDataQueries](../reference/tanstack-query/useProtectedDataQueries.md) hook is now available.
- A new [isGlobalDataQueriesError](../reference/tanstack-query/isGlobalDataQueriesError.md) function is now available.
- A new [registerPublicRoute](../reference/runtime/runtime-class.md#register-a-public-route) function is now available.

## Improvements

Expand Down Expand Up @@ -308,11 +310,12 @@ The `v9` release introduces several breaking changes affecting the host applicat
- `errorElement` is removed and somewhat replaced by a [root error boundary](#root-error-boundary)
3. Create a `TanStackSessionManager` class and the `SessionManagerContext`. Replace the session's deprecated hooks by creating the customs `useSession` and `useIsAuthenticated` hooks. [View example](./add-authentication.md#create-a-session-manager)
4. Remove the `sessionAccessor` option from the `FireflyRuntime` instance. Update the `BootstrappingRoute` component to create a `TanStackSessionManager` instance and share it down the component tree using a `SessionManagedContext` provider. [View example](./add-authentication.md#fetch-the-session)
5. Update the `AuthenticationBoundary` component to use the new `useIsAuthenticated` hook. [View example](./add-authentication.md#add-an-authentication-boundary)
5. Add or update the `AuthenticationBoundary` component to use the new `useIsAuthenticated` hook. Global data fetch request shouldn't be throwing 401 error anymore when the user is not authenticated. [View example](./add-authentication.md#add-an-authentication-boundary)
6. Update the `AuthenticatedLayout` component to use the session manager instance to clear the session. Retrieve the session manager instance from the context defined in the `BootstrappingRoute` component using the `useSessionManager` hook. [View example](./add-authentication.md#define-an-authenticated-layout)
7. Update the `AuthenticatedLayout` component to use the new `$key` option of the navigation item. [View example](#new-key-option-for-navigation-items)
8. Convert all deferred routes into static routes. [View example](#removed-support-for-deferred-routes)
9. Add a `$key` option to the navigation item registrations. [View example](#new-key-option-for-navigation-items)
8. Replace the `ManagedRoutes` placeholder with the new [PublicRoutes](../reference/routing/publicRoutes.md) and [ProtectedRoutes](../reference/routing/protectedRoutes.md) placeholders. [View example](../getting-started/create-host.md#homepage)
9. Convert all deferred routes into static routes. [View example](#removed-support-for-deferred-routes)
10. Add a `$key` option to the navigation item registrations. [View example](#new-key-option-for-navigation-items)

### Root error boundary

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/override-the-host-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ order: 880

# Override the host layout

The `RootLayout` component defined in the [Create an host application](../getting-started/create-host.md#navigation-items) starting guide serves as the default layout for the homepage and all the [managed routes](../reference/routing/managedRoutes.md).
The `RootLayout` component defined in the [Create an host application](../getting-started/create-host.md#navigation-items) starting guide serves as the default layout for the homepage and all the managed routes.

For most routes, this behavior is what the author expects. However, as an application introduces [authentication](./add-authentication.md) and adds many session-related features to the default layout, this default layout may no longer be suitable for every route. For example, a login page doesn't require session-related features, as the user isn't authenticated yet. In such cases, the default layout isn't appropriate.

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ toc:
### Routing

- [AppRouter](./routing/AppRouter.md)
- [ManagedRoutes](./routing/managedRoutes.md)
- [PublicRoutes](./routing/publicRoutes.md)
- [ProtectedRoutes](./routing/protectedRoutes.md)
- [useRenderedNavigationItems](./routing/useRenderedNavigationItems.md)
- [useIsBoostrapping](./routing/useIsBootstrapping.md)
- [useRouteMatch](./routing/useRouteMatch.md)
Expand Down
Loading

0 comments on commit 98e4839

Please sign in to comment.