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

Use minimal router in @keystatic/core over different ones for each framework #728

Merged
merged 3 commits into from
Nov 3, 2023
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
8 changes: 8 additions & 0 deletions .changeset/curly-cows-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@keystatic/astro': major
'@keystatic/remix': major
'@keystatic/next': major
'@keystatic/core': minor
---

Update router integration between `@keystatic/core` and framework integration packages to improve performance
1 change: 0 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"@babel/runtime": "^7.18.3",
"@types/react": "^18.2.8",
"cookie": "^0.5.0",
"react-router-dom": "^6.8.1",
"set-cookie-parser": "^2.5.1"
},
"devDependencies": {
Expand Down
74 changes: 3 additions & 71 deletions packages/astro/src/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,14 @@
import React, {
AnchorHTMLAttributes,
forwardRef,
Ref,
useContext,
useMemo,
} from 'react';
import React from 'react';
import { Config } from '@keystatic/core';
import { Keystatic as GenericKeystatic, Router } from '@keystatic/core/ui';
import {
createBrowserRouter,
Link,
RouterProvider,
useHref,
useLocation,
useNavigate,
} from 'react-router-dom';

const KeystaticLink = forwardRef(function KeystaticLink(
{
href,
...props
}: { href: string } & AnchorHTMLAttributes<HTMLAnchorElement>,
ref: Ref<HTMLAnchorElement>
) {
return <Link to={href} {...props} ref={ref} />;
});

function ReactRouterKeystatic() {
const config = useContext(ConfigContext)!;
const navigate = useNavigate();
const location = useLocation();
const href = useHref(location);
const keystaticRouter = useMemo((): Router => {
const replaced = location.pathname.replace(/^\/keystatic\/?/, '');
const params =
replaced === '' ? [] : replaced.split('/').map(decodeURIComponent);

return {
push(path) {
navigate(path);
},
replace(path) {
navigate(path, { replace: true });
},
href,
params,
};
}, [navigate, href, location.pathname]);
return (
<GenericKeystatic
router={keystaticRouter}
config={config}
link={KeystaticLink}
appSlug={appSlug}
/>
);
}
import { Keystatic as GenericKeystatic } from '@keystatic/core/ui';

const appSlug = {
envName: 'PUBLIC_KEYSTATIC_GITHUB_APP_SLUG',
value: import.meta.env.PUBLIC_KEYSTATIC_GITHUB_APP_SLUG,
};

const ConfigContext = React.createContext<Config<any, any> | null>(null);

const router = createBrowserRouter([
{
path: '/keystatic/*',
element: <ReactRouterKeystatic />,
},
]);

export function makePage(config: Config<any, any>) {
return function Keystatic() {
return (
<ConfigContext.Provider value={config}>
<RouterProvider router={router} />
</ConfigContext.Provider>
);
return <GenericKeystatic config={config} appSlug={appSlug} />;
};
}
57 changes: 54 additions & 3 deletions packages/keystatic/src/app/router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { createContext, ReactNode, useContext } from 'react';
import React, {
createContext,
ReactNode,
startTransition,
useContext,
useEffect,
useMemo,
useState,
} from 'react';

export type Router = {
push: (path: string) => void;
Expand All @@ -9,9 +17,52 @@ export type Router = {

const RouterContext = createContext<Router | null>(null);

export function RouterProvider(props: { router: Router; children: ReactNode }) {
export function RouterProvider(props: { children: ReactNode }) {
const [url, setUrl] = useState(() => window.location.href);

const router = useMemo((): Router => {
function navigate(url: string, replace: boolean) {
const newUrl = new URL(url, window.location.href);
if (
newUrl.origin !== window.location.origin ||
!newUrl.pathname.startsWith('/keystatic')
) {
window.location.assign(newUrl);
return;
}
window.history[replace ? 'replaceState' : 'pushState'](null, '', newUrl);
startTransition(() => {
setUrl(newUrl.toString());
});
}
const replaced = location.pathname.replace(/^\/keystatic\/?/, '');
const params =
replaced === '' ? [] : replaced.split('/').map(decodeURIComponent);
const parsedUrl = new URL(url);
return {
href: parsedUrl.pathname + parsedUrl.search,
replace(path) {
navigate(path, true);
},
push(path) {
navigate(path, false);
},
params,
};
}, [url]);
useEffect(() => {
const handleNavigate = () => {
startTransition(() => {
setUrl(window.location.href);
});
};
window.addEventListener('popstate', handleNavigate);
return () => {
window.removeEventListener('popstate', handleNavigate);
};
}, []);
return (
<RouterContext.Provider value={props.router}>
<RouterContext.Provider value={router}>
{props.children}
</RouterContext.Provider>
);
Expand Down
56 changes: 33 additions & 23 deletions packages/keystatic/src/app/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
AnchorHTMLAttributes,
ReactElement,
ReactNode,
RefAttributes,
useContext,
useEffect,
useState,
Expand All @@ -29,7 +27,7 @@ import { CreatedGitHubApp } from './onboarding/created-github-app';
import { KeystaticSetup } from './onboarding/setup';
import { RepoNotFound } from './onboarding/repo-not-found';
import { AppSlugProvider } from './onboarding/install-app';
import { useRouter, Router, RouterProvider } from './router';
import { useRouter, RouterProvider } from './router';
import {
isCloudConfig,
isGitHubConfig,
Expand Down Expand Up @@ -266,19 +264,7 @@ function AuthWrapper(props: {
return null;
}

export function Keystatic(props: {
config: Config;
router: Router;
/** @deprecated This functionality is now abstracted from the `router` prop. */
link: (
props: { href: string } & AnchorHTMLAttributes<HTMLAnchorElement> &
RefAttributes<HTMLAnchorElement>
) => ReactNode;
appSlug?: { envName: string; value: string | undefined };
}) {
if (props.config.storage.kind === 'github') {
assertValidRepoConfig(props.config.storage.repo);
}
function RedirectToLoopback(props: { children: ReactNode }) {
useEffect(() => {
if (window.location.hostname === 'localhost') {
window.location.href = window.location.href.replace(
Expand All @@ -290,13 +276,37 @@ export function Keystatic(props: {
if (window.location.hostname === 'localhost') {
return null;
}
return props.children;
}

export function Keystatic(props: {
config: Config;
appSlug?: { envName: string; value: string | undefined };
}) {
if (props.config.storage.kind === 'github') {
assertValidRepoConfig(props.config.storage.repo);
}

return (
<AppSlugProvider value={props.appSlug}>
<RouterProvider router={props.router}>
<Provider config={props.config}>
<PageInner config={props.config} />
</Provider>
</RouterProvider>
</AppSlugProvider>
<ClientOnly>
<RedirectToLoopback>
<AppSlugProvider value={props.appSlug}>
<RouterProvider>
<Provider config={props.config}>
<PageInner config={props.config} />
</Provider>
</RouterProvider>
</AppSlugProvider>
</RedirectToLoopback>
</ClientOnly>
);
}

function ClientOnly(props: { children: ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return props.children;
}
48 changes: 2 additions & 46 deletions packages/next/src/ui/app.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,9 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useEffect, useMemo, useState } from 'react';
import { Config } from '@keystatic/core';
import { Keystatic, Router } from '@keystatic/core/ui';

let _isClient = false;
function useIsClient() {
const [isClient, setIsClient] = useState(_isClient);
useEffect(() => {
_isClient = true;
setIsClient(true);
}, []);
return isClient;
}
import { Keystatic } from '@keystatic/core/ui';

export function makePage(config: Config<any, any>) {
return function Page() {
const isClient = useIsClient();
const router = useRouter();
const pathname = usePathname()!;
let href = pathname;
const searchParams = useSearchParams()!.toString();
if (searchParams) {
href += `?${searchParams}`;
}
const keystaticRouter = useMemo((): Router => {
const replaced = pathname.replace(/^\/keystatic\/?/, '');
const params =
replaced === '' ? [] : replaced.split('/').map(decodeURIComponent);
return {
href,
params,
push: async path => {
router.push(path);
},
replace: async path => {
router.replace(path);
},
};
}, [href, router, pathname]);
if (!isClient) return null;
return (
<Keystatic
router={keystaticRouter}
config={config}
link={Link}
appSlug={appSlug}
/>
);
return <Keystatic config={config} appSlug={appSlug} />;
};
}

Expand Down
33 changes: 2 additions & 31 deletions packages/next/src/ui/pages.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
import { useRouter } from 'next/router';
import Link from 'next/link';
import { useMemo } from 'react';
import { Config } from '@keystatic/core';
import { Keystatic, Router } from '@keystatic/core/ui';
import { Keystatic } from '@keystatic/core/ui';

export function makePage(config: Config<any, any>) {
return function Page() {
const router = useRouter();
const keystaticRouter = useMemo((): null | Router => {
if (!router.isReady) return null;
const params = (router.query.params ??
router.query.rest ??
[]) as string[];

return {
href: router.asPath,
params,
push: path => {
router.push(path);
},
replace: path => {
router.replace(path);
},
};
}, [router]);
if (!keystaticRouter) return null;
return (
<Keystatic
router={keystaticRouter}
config={config}
link={Link}
appSlug={appSlug}
/>
);
return <Keystatic config={config} appSlug={appSlug} />;
};
}

Expand Down
Loading
Loading