Promote useMutation to the @probo/relay package

Extract the awaitable useMutation into @probo/relay as a
createUseMutation factory that delegates feedback to an injected
MutationNotifier, keeping the package free of UI and i18n
dependencies. compliance-portal binds it to its Base UI toast +
i18next + formatError stack and imports it by explicit path
(#/lib/relay/useMutation), dropping the lone intra-app barrel; a
compliance-portal-scoped no-restricted-imports rule forbids
react-relay's useMutation.

Bring packages/relay and packages/routes into the shared ESLint
scope and fix the violations that surfaced, and deprecate the
legacy withQueryRef / loaderFromQueryLoader helpers. Document the
shared-hook pattern and the "index.ts for package entrypoints only"
rule in the relay, hooks, and app-arborescence guides.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-24 10:28:57 +02:00
parent e93faf4caa
commit ff966b462e
14 changed files with 684 additions and 122 deletions

View File

@@ -18,7 +18,7 @@ import { type 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;

View File

@@ -12,5 +12,5 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
export { routeFromAppRoute, type AppRoute } from "./appRoute";
export { withQueryRef, loaderFromQueryLoader } from "./relay";
export { type AppRoute, routeFromAppRoute } from "./appRoute";
export { loaderFromQueryLoader, withQueryRef } from "./relay";

View File

@@ -12,21 +12,31 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useCleanup } from "@probo/hooks";
import { type ComponentType } from "react";
import type { EnvironmentProviderOptions, PreloadedQuery } from "react-relay";
import { type LoaderFunction, type LoaderFunctionArgs, useLoaderData } from "react-router";
import { type OperationType } from "relay-runtime";
import { useCleanup } from "@probo/hooks";
// Infer the concrete `queryRef` type from a naked type position. Relay 21's
// first-party types model `PreloadedQuery#variables` as `VariablesOf<TQuery>`,
// which prevents inferring `TQuery` through it, so we infer the whole queryRef.
/**
* @deprecated Use a `*PageLoader` component with `useQueryLoader` +
* `usePreloadedQuery` instead. See contrib/claude/relay.md.
*
* Infer the concrete `queryRef` type from a naked type position. Relay 21's
* first-party types model `PreloadedQuery#variables` as `VariablesOf<TQuery>`,
* which prevents inferring `TQuery` through it, so we infer the whole queryRef.
*/
export function withQueryRef<
TQueryRef extends PreloadedQuery<OperationType>
TQueryRef extends PreloadedQuery<OperationType>,
>(
Component: ComponentType<{ queryRef: TQueryRef }>,
) {
return () => {
return function WithQueryRef() {
// `useLoaderData` is typed `any` (default generic), and its `SerializeFrom`
// generic would strip the `dispose` function type. Assert the loader's
// shape so the rest of the component stays type-safe; the assertion is not
// redundant despite the rule flagging it (the source is `any`).
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const { queryRef, dispose } = useLoaderData() as {
queryRef: TQueryRef;
dispose: () => void;
@@ -34,15 +44,19 @@ export function withQueryRef<
useCleanup(dispose, 1000);
return <Component queryRef={queryRef} />
}
return <Component queryRef={queryRef} />;
};
}
/**
* @deprecated Use a `*PageLoader` component with `useQueryLoader` +
* `usePreloadedQuery` instead. See contrib/claude/relay.md.
*/
export function loaderFromQueryLoader<
TQuery extends OperationType,
TEnvironmentProviderOptions = EnvironmentProviderOptions
TEnvironmentProviderOptions = EnvironmentProviderOptions,
>(
queryLoader: (params: Record<string, string>) => PreloadedQuery<TQuery, TEnvironmentProviderOptions>
queryLoader: (params: Record<string, string>) => PreloadedQuery<TQuery, TEnvironmentProviderOptions>,
): LoaderFunction {
return ({ params }: LoaderFunctionArgs) => {
const query = queryLoader(params as Record<string, string>);
@@ -50,5 +64,5 @@ export function loaderFromQueryLoader<
queryRef: query,
dispose: query.dispose,
};
}
};
}