Skip to content

Commit

Permalink
React Router 7 experimental PoC (plone#6472)
Browse files Browse the repository at this point in the history
  • Loading branch information
sneridagh authored Nov 6, 2024
1 parent 5d08348 commit 5c0dd14
Show file tree
Hide file tree
Showing 18 changed files with 1,727 additions and 197 deletions.
22 changes: 14 additions & 8 deletions PACKAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,31 @@ Core packages must not depend on any other `@plone/*` package, with only one exc
They must be published and bundled in a traditional (transpiled) way.
The bundle of these packages must work on both CommonJS and ECMAScript Module (ESM) environments.

## Feature packages

- `@plone/contents`


## Utility packages

- `@plone/blocks`
- `@plone/providers`
- `@plone/helpers`
- `@plone/drivers`
- `@plone/rsc`


### Rules

Utility packages can depend on core packages and other utility packages.
They must be published in a traditional way, bundled.
This bundle must work on both CommonJS and ESM environments.

## Feature packages

- `@plone/blocks`
- `@plone/slots`
- `@plone/contents`

### Rules

Feature packages (or add-on packages) can depend on any other package.
They must not be transpiled, but as source.
They must provide a default configuration registry loader as default main entry point export.
They must be able to be loaded as any other add-on.

## Development utility packages

Expand All @@ -55,7 +61,7 @@ They contain utilities that are useful for the development of a Volto project.
Some of them are released:

- `@plone/scripts`
- `@plone/generator-volto`
- `@plone/generator-volto` (deprecated)

Some of them are used by the build, and separated in packages for convenience.

Expand Down
80 changes: 80 additions & 0 deletions apps/rr7/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/

/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},

// Base config
extends: ['eslint:recommended'],

overrides: [
// React
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: ['react', 'jsx-a11y'],
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
],
settings: {
react: {
version: 'detect',
},
formComponents: ['Form'],
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
],
},
},

// Typescript
{
files: ['**/*.{ts,tsx}'],
plugins: ['@typescript-eslint', 'import'],
parser: '@typescript-eslint/parser',
settings: {
'import/internal-regex': '^~/',
'import/resolver': {
node: {
extensions: ['.ts', '.tsx'],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
],
},

// Node
{
files: ['.eslintrc.js'],
env: {
node: true,
},
},
],
};
7 changes: 7 additions & 0 deletions apps/rr7/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules

/.cache
/build
.env
.react-router
.registry.loader.js
30 changes: 30 additions & 0 deletions apps/rr7/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Plone on React Router 7

This is a proof of concept of a [React Router](https://reactrouter.com/dev/docs) app, using the `@plone/*` libraries.
This is intended to serve as both a playground for the development of both packages and as a demo of Plone using Remix.

> [!WARNING]
> This package or app is experimental.
> The community offers no support whatsoever for it.
> Breaking changes may occur without notice.
## Development

To start, from the root of the monorepo:

```shell
pnpm install
pnpm --filter plone-remix run dev
```

Then start the Plone backend:

% TODO MAKEFILE
```shell
make backend-docker-start
```


## About this app

- [Remix Docs](https://remix.run/docs/en/main)
8 changes: 8 additions & 0 deletions apps/rr7/app/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ploneClient from '@plone/client';
import config from '@plone/registry';

const cli = ploneClient.initialize({
apiPath: config.settings.apiPath,
});

export { cli as ploneClient };
15 changes: 15 additions & 0 deletions apps/rr7/app/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import config from '@plone/registry';
import { blocksConfig, slate } from '@plone/blocks';

const settings = {
apiPath: 'http://localhost:3000',
slate,
};

// @ts-expect-error We need to fix typing
config.set('settings', settings);

// @ts-expect-error We need to fix typing
config.set('blocks', { blocksConfig });

export default config;
98 changes: 98 additions & 0 deletions apps/rr7/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useState } from 'react';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useHref,
useLocation,
useNavigate,
useParams,
} from 'react-router';
import type { LinksFunction } from 'react-router';

import { QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import PloneClient from '@plone/client';
import { PloneProvider } from '@plone/providers';
import { flattenToAppURL } from './utils';
import config from '@plone/registry';
import './config';

import '@plone/components/dist/basic.css';

function useHrefLocal(to: string) {
return useHref(flattenToAppURL(to));
}

export const links: LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
];

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App() {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
);

const [ploneClient] = useState(() =>
PloneClient.initialize({
apiPath: config.settings.apiPath,
}),
);

const RRNavigate = useNavigate();
const navigate = (to: string) => {
return RRNavigate(flattenToAppURL(to));
};

return (
<PloneProvider
ploneClient={ploneClient}
queryClient={queryClient}
useLocation={useLocation}
useParams={useParams}
useHref={useHrefLocal}
navigate={navigate}
>
<Outlet />
<ReactQueryDevtools initialIsOpen={false} />
</PloneProvider>
);
}
7 changes: 7 additions & 0 deletions apps/rr7/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { RouteConfig } from '@react-router/dev/routes';
import { index, route } from '@react-router/dev/routes';

export const routes: RouteConfig = [
index('routes/home.tsx'),
route('*', 'routes/$.tsx'),
];
2 changes: 2 additions & 0 deletions apps/rr7/app/routes/$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Content, { loader } from './home';
export { loader, Content as default };
79 changes: 79 additions & 0 deletions apps/rr7/app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { LoaderArgs } from '../routes/+types.home';
import {
dehydrate,
QueryClient,
HydrationBoundary,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { flattenToAppURL } from '../utils';
import { useLoaderData, useLocation } from 'react-router';
import { usePloneClient } from '@plone/providers';
import { Breadcrumbs, RenderBlocks } from '@plone/components';
import config from '@plone/registry';
import { ploneClient } from '../client';

import type { MetaFunction } from 'react-router';

export const meta: MetaFunction = () => {
return [
{ title: 'Plone on React Router 7' },
{ name: 'description', content: 'Welcome to Plone!' },
];
};

const expand = ['breadcrumbs', 'navigation'];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function loader({ params, request }: LoaderArgs) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
});

const { getContentQuery } = ploneClient;

await queryClient.prefetchQuery(
getContentQuery({ path: flattenToAppURL(request.url), expand }),
);

return { dehydratedState: dehydrate(queryClient) };
}

function Page() {
const { getContentQuery } = usePloneClient();
const pathname = useLocation().pathname;
const { data } = useQuery(getContentQuery({ path: pathname, expand }));

if (!data) return 'Loading...';
return (
<>
<Breadcrumbs
items={data['@components'].breadcrumbs.items || []}
root={data['@components'].breadcrumbs.root}
includeRoot
/>
<RenderBlocks
content={data}
blocksConfig={config.blocks.blocksConfig}
pathname="/"
/>
</>
);
}

export default function Content() {
const { dehydratedState } = useLoaderData<typeof loader>();
const queryClient = useQueryClient();

return (
<HydrationBoundary state={dehydratedState} queryClient={queryClient}>
<Page />
</HydrationBoundary>
);
}
Loading

0 comments on commit 5c0dd14

Please sign in to comment.