Move route utils to packages/routes

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-04 16:09:36 +04:00
parent 2fc6143409
commit 57caa9d28f
28 changed files with 299 additions and 189 deletions

View File

@@ -0,0 +1,40 @@
import { ComponentType, LazyExoticComponent } from "react";
import { EnvironmentProviderOptions, PreloadedQuery } from "react-relay";
import { LoaderFunction, LoaderFunctionArgs, RouteObject, useLoaderData } from "react-router";
import { OperationType } from "relay-runtime";
import { useCleanup } from "./useDelayedEffect";
export function withQueryRef<
TQuery extends OperationType,
TEnvironmentProviderOptions = EnvironmentProviderOptions
>(
Component: LazyExoticComponent<ComponentType<{ queryRef: PreloadedQuery<TQuery, TEnvironmentProviderOptions> }>>,
) {
return () => {
const { queryRef, dispose } = useLoaderData();
useCleanup(dispose, 1000);
return <Component queryRef={queryRef} />
}
}
export function loaderFromQueryLoader<
TQuery extends OperationType,
TEnvironmentProviderOptions = EnvironmentProviderOptions
>(
queryLoader: (params: Record<string, string>) => PreloadedQuery<TQuery, TEnvironmentProviderOptions>
): LoaderFunction {
return ({ params }: LoaderFunctionArgs) => {
const query = queryLoader(params as Record<string, string>);
return {
queryRef: query,
dispose: query.dispose,
};
}
}
export type AppRoute = Omit<RouteObject, "children"> & {
children?: AppRoute[];
fallback?: ComponentType;
}

View File

@@ -0,0 +1,19 @@
import { useEffect, useRef } from "react";
/**
* Hook to handle cleanup after a delay
*
* Used for disposing the graphQL query when the component unmounts
*/
export function useCleanup(callback: () => void, delay: number) {
const timer = useRef<ReturnType<typeof setTimeout>>(null);
useEffect(() => {
if (timer.current) {
clearTimeout(timer.current);
}
return () => {
timer.current = setTimeout(callback, delay);
};
}, [callback, delay]);
}

View File

@@ -0,0 +1,19 @@
import { useEffect, useRef } from "react";
/**
* Hook to handle cleanup after a delay
*
* Used for disposing the graphQL query when the component unmounts
*/
export function useCleanup(callback: () => void, delay: number) {
const timer = useRef<ReturnType<typeof setTimeout>>(null);
useEffect(() => {
if (timer.current) {
clearTimeout(timer.current);
}
return () => {
timer.current = setTimeout(callback, delay);
};
}, [callback, delay]);
}

View File

@@ -0,0 +1,27 @@
{
"name": "@probo/routes",
"version": "1.0.0",
"description": "",
"main": "./src/index.ts",
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"format": "prettier --write ./src"
},
"prettier": "@probo/prettier",
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@probo/prettier": "1.0.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"vitest": "^3.1.3"
},
"peerDependencies": {
"react": "^19.2.1",
"react-relay": "^19.0.0",
"react-router": "^7.10.0",
"relay-runtime": "^19.0.0"
}
}

View File

@@ -0,0 +1,30 @@
import { ComponentType, Suspense } from "react";
import { RouteObject } from "react-router";
export type AppRoute = Omit<RouteObject, "children"> & {
children?: AppRoute[];
Fallback?: ComponentType;
}
export function routeFromAppRoute(appRoute: AppRoute): RouteObject {
const { Component, Fallback, children, ...rest } = appRoute;
let route = { ...rest } as RouteObject;
if (Component && Fallback) {
const OriginalComponent = Component as ComponentType;
route = {
...route,
Component: () => (
<Suspense fallback={<Fallback />}>
<OriginalComponent />
</Suspense>
),
};
}
return {
...route,
children: children?.map(routeFromAppRoute),
} as RouteObject;
}

View File

@@ -0,0 +1,2 @@
export { routeFromAppRoute, type AppRoute } from "./appRoute";
export { withQueryRef, loaderFromQueryLoader } from "./relay";

View File

@@ -0,0 +1,35 @@
import { ComponentType } from "react";
import { EnvironmentProviderOptions, PreloadedQuery } from "react-relay";
import { LoaderFunction, LoaderFunctionArgs, useLoaderData } from "react-router";
import { OperationType } from "relay-runtime";
import { useCleanup } from "@probo/hooks";
export function withQueryRef<
TQuery extends OperationType,
TEnvironmentProviderOptions = EnvironmentProviderOptions
>(
Component: ComponentType<{ queryRef: PreloadedQuery<TQuery, TEnvironmentProviderOptions> }>,
) {
return () => {
const { queryRef, dispose } = useLoaderData();
useCleanup(dispose, 1000);
return <Component queryRef={queryRef} />
}
}
export function loaderFromQueryLoader<
TQuery extends OperationType,
TEnvironmentProviderOptions = EnvironmentProviderOptions
>(
queryLoader: (params: Record<string, string>) => PreloadedQuery<TQuery, TEnvironmentProviderOptions>
): LoaderFunction {
return ({ params }: LoaderFunctionArgs) => {
const query = queryLoader(params as Record<string, string>);
return {
queryRef: query,
dispose: query.dispose,
};
}
}

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"lib": ["ES2021", "dom"],
"jsx": "react-jsx",
}
}