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

React Router 7 experimental PoC #6472

Merged
merged 8 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }));
pnicolli marked this conversation as resolved.
Show resolved Hide resolved

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
Loading