Skip to content
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

fix: AppRouter component #131

Merged
merged 12 commits into from
Jan 16, 2024
Merged
5 changes: 5 additions & 0 deletions .changeset/afraid-mirrors-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@squide/core": minor
---

- Added a `usePlugin` hook.
72 changes: 72 additions & 0 deletions .changeset/polite-geckos-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
"@squide/firefly": major
---

- The `AppRouter` component now requires to define a `RouterProvider` as a child. This change has been made to provide more flexibility on the consumer side about the definition of the React Router router.

Before:

```tsx
<AppRouter
fallbackElement={...}
errorElement={...}
waitForMsw={...}
/>
```

Now:

```tsx
<AppRouter
fallbackElement={...}
errorElement={...}
waitForMsw={...}
>
{(routes, providerProps) => (
<RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
)}
</AppRouter>
```

- When in development and using React strict mode, the public and protected handler can be called twice. This issue highlighted that the `AppRouter` component doesn't equipe correctly the handlers to dispose of previous HTTP requests if they are called multiple times because of re-renders. Therefore, the handlers now receives an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that should be forwared to the HTTP client initiating the fetch request.
- The fix also requires the consumer to provide new properties (`isPublicDataLoaded` and `isProtectedDataLoaded`) indicating whether or not the public and/or protected data has been loaded.

```tsx
async function fetchPublicData(setFeatureFlags: (featureFlags: FeatureFlags) => void, signal: AbortSignal) {
try {
const response = await fetch("/api/feature-flags", {
signal
});

if (response.ok) {
const data = await response.json();

setFeatureFlags(data);
}
} catch (error: unknown) {
if (!signal.aborted) {
throw error;
}
}
}

const [featureFlags, setFeatureFlags] = useState<FeatureFlags>();

const handleLoadPublicData = useCallback((signal: AbortSignal) => {
return fetchPublicData(setFeatureFlags, signal);
}, []);

<AppRouter
onLoadPublicData={handleLoadPublicData}
isPublicDataLoaded={!!featureFlags}
fallbackElement={...}
errorElement={...}
waitForMsw={...}
>
{(routes, providerProps) => (
<RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
)}
</AppRouter>
```

- Fixed an issue where the deferred registrations could be completed before the protected data has been loaded.
17 changes: 17 additions & 0 deletions .changeset/six-teachers-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@squide/react-router": major
---

- To be consistent with the other API of Squide, the `useNavigationItems` hook now accept an object literal of options rather than an optional `menuId` argument.

Before:

```tsx
const items = useNavigationItems("my-custom-menu");
```

Now:

```tsx
const items = useNavigationItems({ menuId: "my-custom-menu" });
```
13 changes: 10 additions & 3 deletions docs/getting-started/create-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,23 @@ root.render(
);
```

Then, render the [AppRouter](../reference/routing/appRouter.md) component. The `AppRouter` component will render a React Router [browser instance](https://reactrouter.com/en/main/routers/create-browser-router) configured with the registered routes:
Then, render the [AppRouter](../reference/routing/appRouter.md) component to define a React Router [browser instance](https://reactrouter.com/en/main/routers/create-browser-router) configured with the registered routes:

```tsx host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { RouterProvider, createBrowserRouter } from "react-router-dom";

export function App() {
return (
<AppRouter
fallbackElement={<div>Loading...</div>}
errorElement={<div>An error occured!</div>}
waitForMsw={false}
/>
>
{(routes, providerProps) => (
<RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
)}
</AppRouter>
);
}
```
Expand All @@ -143,7 +148,7 @@ import {
isNavigationLink,
type RenderItemFunction,
type RenderSectionFunction
} from "@squide/react-router";
} from "@squide/firefly";

const renderItem: RenderItemFunction = (item, index, level) => {
// To keep thing simple, this sample doesn't support nested navigation items.
Expand All @@ -153,6 +158,8 @@ const renderItem: RenderItemFunction = (item, index, level) => {
return null;
}

const { label, linkProps, additionalProps } = item;

return (
<li key={`${level}-${index}`}>
<Link {...linkProps} {...additionalProps}>
Expand Down
53 changes: 41 additions & 12 deletions docs/guides/add-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,36 +309,63 @@ export const requestHandlers: HttpHandler[] = [

Then, update the host application `App` component to load the session when a user navigate to a protected page for the first time:

```tsx !#15,21 host/src/App.tsx
```tsx !#20,22,31,33-35,39-40 host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import type { Session } from "@sample/shared";
import { sessionManager } from "./session.ts";
import { RouterProvider, createBrowserRouter } from "react-router-dom";

async function handleLoadProtectedData() {
const response = await fetch("/api/session");
const data = await response.json();
async function fetchProtectedData(setIsSessionLoaded: (isLoaded: boolean) => void,signal: AbortSignal) {
try {
const response = await fetch("/api/session", {
signal
});

const session: Session = {
user: {
name: data.username
}
};
const data = await response.json();

sessionManager.setSession(session);
const session: Session = {
user: {
name: data.username
}
};

sessionManager.setSession(session);

setIsSessionLoaded(true);
} catch (error: unknown) {
if (!signal.aborted) {
throw error;
}
}
}

export function App() {
const [isSessionLoaded, setIsSessionLoaded] = useState(false);

const handleLoadProtectedData = useCallback((signal: AbortSignal) => {
return fetchProtectedData(setIsSessionLoaded, signal);
}, []);

return (
<AppRouter
onLoadProtectedData={handleLoadProtectedData}
isProtectedDataLoaded={isSessionLoaded}
fallbackElement={<div>Loading...</div>}
errorElement={<div>An error occured!</div>}
waitForMsw={true}
/>
>
{(routes, providerProps) => (
<RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
)}
</AppRouter>
);
}
```

!!!info
Since the `sessionManager` doesn't trigger a re-render, a `isSessionLoaded` state value is added to trigger a re-render when the session has been loadded.
!!!

## Add an authentication boundary

Next, create a new React Router authentication boundary component using the [useIsAuthenticated](../reference/session/useIsAuthenticated.md) hook:
Expand Down Expand Up @@ -435,7 +462,7 @@ export const requestHandlers: HttpHandler[] = [

Then, introduce a new `AuthenticatedLayout` displaying the name of the logged-in user along with a logout button:

```tsx !#39,41-58,70,73 host/src/AuthenticatedLayout.tsx
```tsx !#41,43-60,72,75 host/src/AuthenticatedLayout.tsx
import { useCallback, type ReactNode, type MouseEvent, type HTMLButtonElement } from "react";
import { Link, Outlet, navigate } from "react-router-dom";
import {
Expand All @@ -455,6 +482,8 @@ const renderItem: RenderItemFunction = (item, index, level) => {
return null;
}

const { label, linkProps, additionalProps } = item;

return (
<li key={`${level}-${index}`}>
<Link {...linkProps} {...additionalProps}>
Expand Down
Loading