npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gaddario98/react-pages

v3.0.4

Published

A performance-optimized React component library for building dynamic pages that work across web (React DOM) and React Native. It integrates form management, query handling, SEO metadata, and content rendering behind a single unified `PageProps` interface.

Readme

@gaddario98/react-pages

A performance-optimized React component library for building dynamic pages that work across web (React DOM) and React Native. It integrates form management, query handling, SEO metadata, and content rendering behind a single unified PageProps interface.

Version: v3.0.4


Table of Contents


Overview

Core capabilities:

  • Universal Cross-Platform: Single PageProps configuration works on web (React DOM) and React Native
  • Performance Optimized: Automatic dependency tracking for selective re-rendering, debounced form inputs, 90% fewer re-renders
  • SEO & AI Metadata: Dynamic metadata with Open Graph, Twitter Card, JSON-LD, and AI crawler hints
  • Fully Extensible: Custom layout containers and platform adapters

Developer experience:

  • TypeScript First: Full generic type support with strict mode
  • Form Integration: Built-in form management via @gaddario98/react-form
  • Query Management: Query and mutation handling via @gaddario98/react-queries
  • Internationalization: Namespace-based i18n via ns prop and pluggable translateText
  • Tree-Shakeable: 5 optimized entry points for minimal bundle footprint
  • React Compiler: Automatic memoization via babel-plugin-react-compiler

Installation

npm install @gaddario98/react-pages

Peer Dependencies

npm install react@^19.2.0
# Only if using for the web:
npm install react-dom@^19.2.0

Note: Form management (@gaddario98/react-form) and query/mutation handling (@gaddario98/react-queries) are internal dependencies — you do not need to install them separately. @tanstack/react-query is bundled transitively.


Quick Start

import { useMemo } from "react";
import {
  PageGenerator,
  type PageProps,
  type QueryDefinition,
} from "@gaddario98/react-pages";

interface MyForm {
  name: string;
}
type MyQueries = [QueryDefinition<"hello", "query", never, string>];

export function MyPage() {
  const props: PageProps<MyForm, MyQueries> = useMemo(
    () => ({
      id: "my-page",
      meta: {
        title: "My Page",
        description: "My awesome page",
      },
      contents: [
        {
          type: "custom",
          // Component functions receive { get, set } for type-safe state access
          component: ({ get }) => (
            <h1>Welcome, {get("form", "name", "Guest")}!</h1>
          ),
        },
      ],
    }),
    [],
  );

  return <PageGenerator<MyForm, MyQueries> {...props} />;
}

Global Configuration

Before rendering any PageGenerator, configure the global defaults once near the root of your app using usePageConfigState:

import { usePageConfigState } from "@gaddario98/react-pages";
import { useEffect } from "react";

export function GlobalProvider({ children }: { children: React.ReactNode }) {
  const [, setPageConfig] = usePageConfigState();

  useEffect(() => {
    setPageConfig((prev) => ({
      ...prev,
      // Default SEO metadata applied to all pages
      defaultMetadata: {
        title: "My App",
        description: "Default description",
      },
      // Custom layout containers (web or React Native)
      PageContainer: MyPageContainer,
      BodyContainer: MyBodyContainer,
      // Authentication
      isLogged: (user) => !!user?.token,
      authPageProps: { id: "login-page" },
    }));
  }, [setPageConfig]);

  return <>{children}</>;
}

Key configurable slots in PageConfigProps:

| Key | Type | Purpose | | ----------------- | ------------------------------------------ | ------------------------------------- | | PageContainer | ComponentType | Outermost page wrapper | | BodyContainer | ComponentType | Scrollable body area | | HeaderContainer | ComponentType | Header slot renderer | | FooterContainer | ComponentType | Footer slot renderer | | ItemsContainer | ComponentType | Wrapper for type: "container" items | | LoaderComponent | ComponentType | Custom loader component | | isLogged | (user) => boolean | Auth check function | | authValues | PageAuthState \| null | Current authentication state values | | authPageProps | PageProps | Fallback page when not authenticated | | authPageImage | string | Image URL for the fallback auth page | | defaultMetadata | MetadataConfig | Default SEO metadata | | meta | { title?: string; description?: string } | Basic fallback metadata | | translateText | (key, options) => string | Pluggable i18n function | | locale | string | Active locale |


Architecture

PageGenerator

The main orchestrator component. It wires together forms, queries, metadata, layout, and authentication into a single declarative configuration.

  • Orchestrates the full page lifecycle and state management
  • Integrates form management via @gaddario98/react-form
  • Manages queries and mutations via @gaddario98/react-queries
  • Handles metadata injection (SEO, Open Graph, JSON-LD, AI hints)
  • Delegates internationalization to pageConfig.translateText
  • Handles authentication and access control via pageConfig.isLogged
  • Tracks data dependencies for selective re-rendering

When to use: any page with forms, queries, dynamic content, SEO requirements, or complex data dependencies.

Content Items & Layout

Content items are distributed across three layout slots: header, body, and footer. Use renderInHeader and renderInFooter to route items to the corresponding slot. Nest items in a type: "container" to group them inside the ItemsContainer.

<PageGenerator
  id="layout-page"
  viewSettings={{
    withoutPadding: false,
    disableRefreshing: false,
  }}
  contents={[
    { type: "custom", component: <PageHeader />, renderInHeader: true },
    { type: "custom", component: <PageFooter />, renderInFooter: true },
    {
      type: "container",
      items: [
        { type: "custom", component: <Section1 />, index: 0 },
        { type: "custom", component: <Section2 />, index: 1 },
      ],
    },
  ]}
/>

MetadataManager

An internal component (used automatically by PageGenerator) that applies resolved metadata to the page.

  • Writes to document.head on web (title, meta, link, JSON-LD script tags)
  • Is a silent no-op on React Native (typeof document === 'undefined')
  • Supports dynamic metadata evaluation via functions that receive { get, set }

Cross-Platform Containers

@gaddario98/react-pages is platform-agnostic by design — it imports no React DOM or React Native–specific APIs. All rendering logic (forms, queries, metadata, dependency tracking) is decoupled from layout, which is injected externally via five configurable container slots in pageConfig:

| Slot | Role | Default | | ----------------- | ----------------------------------------------- | ---------------------------- | | PageContainer | Outermost page wrapper (navigation integration) | ({ children }) => children | | BodyContainer | Scrollable body container | ({ children }) => children | | HeaderContainer | Header slot (receives renderInHeader items) | ({ children }) => children | | FooterContainer | Footer slot (receives renderInFooter items) | ({ children }) => children | | ItemsContainer | Children wrapper for type: "container" items | ({ children }) => children |

On web, replace these with div elements and CSS layouts. On React Native, replace them with View, ScrollView, etc. The PageGenerator component itself does not change — only the container implementations differ. See React Native & Ionic Integration for complete setup examples.

RenderComponents

An exported component that routes a ContentItem to the correct renderer based on its type ("custom" → RenderComponent, "container" → Container).

When to use: rendering a ContentItem outside of PageGenerator (e.g. in a custom page shell) or building custom orchestrators that reuse the library's rendering pipeline.


Features

SEO & Metadata

The meta prop accepts a rich MetadataConfig object. All values can be static strings or evaluator functions that receive { get, set } for dynamic metadata based on page state.

Supported metadata:

  • Page title and description
  • Open Graph (title, description, image, URL, type, article fields)
  • Twitter Card
  • Canonical URL and hreflang alternates
  • JSON-LD structured data (schema.org)
  • AI crawler hints (contentClassification, modelHints, contextualInfo)
  • Robots meta tags (noindex, nofollow, noarchive, nosnippet, maxSnippet, maxImagePreview)
  • Icons and PWA manifest
  • Custom meta tags
<PageGenerator
  id="seo-page"
  meta={{
    title: "Product Details",
    description: "High-quality product with amazing features",
    keywords: ["product", "quality"],
    openGraph: {
      title: "Check out this product!",
      description: "Amazing features included",
      image: "https://example.com/og-image.png",
      url: "https://example.com/product",
      type: "product",
    },
    structuredData: {
      type: "Product",
      schema: {
        name: "Product Name",
        price: "99.99",
      },
    },
    aiHints: {
      contentClassification: "product-page",
      contextualInfo: "Product information page",
      excludeFromIndexing: false,
    },
    robots: {
      noindex: false,
      nofollow: false,
      noarchive: false,
    },
  }}
  contents={[...]}
/>

Multi-Tab, Cached Pages & Keep-Alive Support (isActive)

When using tabbed navigation, stacked routers (e.g., Ionic IonRouterOutlet), or keep-alive layouts, multiple PageGenerator instances stay mounted in memory at the same time to preserve component state, form inputs, and scroll position.

By default, isActive is true. When multiple pages remain mounted simultaneously, pass isActive={currentTab === pageId} so that PageGenerator can manage the document <head> dynamically:

  • When active (isActive: true): The page automatically syncs its metadata to document.head, and cleans up any tags left behind by the previously active page.
  • When deactivated (isActive: false): The page cleans up its metadata tags from document.head.
  • Switching back and forth: Returning to an already-mounted page immediately restores its metadata without needing to remount the component or re-run queries.
import { useState } from "react";
import { PageGenerator } from "@gaddario98/react-pages";

export function TabbedDashboard() {
  const [activeTab, setActiveTab] = useState<"overview" | "analytics">(
    "overview",
  );

  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab("overview")}>Overview</button>
        <button onClick={() => setActiveTab("analytics")}>Analytics</button>
      </nav>

      {/* Both pages stay mounted; only the active one controls document.head */}
      <div style={{ display: activeTab === "overview" ? "block" : "none" }}>
        <PageGenerator
          id="overview-page"
          isActive={activeTab === "overview"}
          meta={{
            title: "Overview Dashboard",
            description: "Summary of your operations",
          }}
          contents={overviewContents}
        />
      </div>

      <div style={{ display: activeTab === "analytics" ? "block" : "none" }}>
        <PageGenerator
          id="analytics-page"
          isActive={activeTab === "analytics"}
          meta={{
            title: "Detailed Analytics",
            description: "Deep dive into performance metrics",
          }}
          contents={analyticsContents}
        />
      </div>
    </div>
  );
}

Differential DOM Updates & Automatic Tag Cleanup

PageGenerator tags all managed DOM elements with data-react-pages="meta" and data-page-id.

When transitioning between pages (or toggling isActive), the library performs a differential cleanup:

  • Tags defined by the new page are updated or added.
  • Any managed tags present in the previous page that are not specified by the new page (such as page-specific og:image, twitter:*, keywords, canonical, robots, or structuredData) are automatically removed from <head>, preventing obsolete tags from leaking into subsequent pages.
  • When the page is unmounted, all of its managed tags are cleaned up.

Global Defaults Fallback

If a page configuration omits specific fields (e.g., themeColor or robots), they will automatically fallback to the defaultMetadata configured globally in usePageConfigState:

setPageConfig((prev) => ({
  ...prev,
  defaultMetadata: {
    title: "Default App Name",
    description: "Global fallback description",
    themeColor: "#0284c7",
    robots: { noindex: false },
  },
}));

Imperative Metadata Cleanup (cleanupMetadata)

You can also programmatically remove managed metadata tags from the DOM at any time using cleanupMetadata:

import { cleanupMetadata } from "@gaddario98/react-pages";

// Remove all tags managed by react-pages
cleanupMetadata();

// Or target tags belonging to a specific pageId
cleanupMetadata("overview-page");

Form Management

Form configuration is passed via the form prop. Under the hood it uses @gaddario98/react-form.

Capabilities:

  • Field types: text, email, password, textarea, select, checkbox, radio, date, and more
  • Built-in validation: required, pattern, min/max length, custom validators
  • Default values sourced from a query result via defaultValueQueryKey + defaultValueQueryMap
  • Form state tracking: values, errors, touched fields, submission state
  • Submit buttons configured declaratively, with access to mutations via get()
  • Per-field debounceDelay to reduce re-renders on fast typing
<PageGenerator
  id="form-page"
  form={{
    defaultValues: { email: "", message: "" },
    data: [
      {
        name: "email",
        type: "email",
        placeholder: "[email protected]",
        rules: {
          onChange: ({ value }) => !value ? "Email is required" : undefined,
        },
      },
      {
        name: "message",
        type: "textarea",
        placeholder: "Your message",
      },
    ],
    submit: ({ get }) => {
      const sendEmail = get("mutation", "sendEmail.mutateAsync");
      const isPending = get("mutation", "sendEmail.isPending", false);

      return [
        {
          values: ["email", "message"],
          component: ({ onClick }) => (
            <button onClick={onClick} disabled={isPending}>
              {isPending ? "Sending..." : "Send"}
            </button>
          ),
          onSuccess: sendEmail,
        },
      ];
    },
  }}
  contents={[...]}
/>

Query & Mutation Management

Queries and mutations are configured via the queries prop and managed by @gaddario98/react-queries (built on TanStack Query). Query results are scoped to the page and accessible via get("query", ...) and get("mutation", ...).

Capabilities:

  • Type-safe query definitions with QueryDefinition
  • Queries can be static configs or functions receiving { get, set } for dependent queries
  • Full TanStack Query options (staleTime, enabled, refetchInterval, etc.)
  • Query results automatically invalidated on mutation success
<PageGenerator
  id="data-page"
  queries={[
    {
      type: "query",
      key: "products",
      queryConfig: {
        queryKey: ["products"],
        queryFn: () => fetch("/api/products").then((r) => r.json()),
        staleTime: 1000 * 60 * 5, // 5 minutes
      },
    },
    {
      type: "mutation",
      key: "addProduct",
      mutationConfig: {
        mutationFn: (newProduct) =>
          fetch("/api/products", {
            method: "POST",
            body: JSON.stringify(newProduct),
          }),
      },
    },
  ]}
  contents={[
    {
      type: "custom",
      component: ({ get }) => {
        const isLoading = get("query", "products.isLoading", false);
        const products = get("query", "products.data", []);

        return isLoading ? (
          <p>Loading...</p>
        ) : (
          <ul>
            {products.map((p) => (
              <li key={p.id}>{p.name}</li>
            ))}
          </ul>
        );
      },
    },
  ]}
/>

Authentication & Access Control

When enableAuthControl is true (default), the page checks pageConfig.isLogged(authValues) before rendering. If the check fails, it renders pageConfig.authPageProps instead.

// Configure globally
setPageConfig((prev) => ({
  ...prev,
  isLogged: (user) => !!user?.token,
  authValues: currentUser,
  authPageProps: {
    id: "login",
    contents: [{ type: "custom", component: <LoginForm /> }],
  },
}));

// Protect a page
<PageGenerator
  id="protected-page"
  enableAuthControl={true}
  contents={[...]}
/>

Internationalization

The library does not depend on any i18n library. You wire your own translation function via pageConfig.translateText and scope it per page with the ns prop.

// Global setup — wire your i18n solution once
setPageConfig((prev) => ({
  ...prev,
  locale: "it",
  translateText: (key, options) => i18next.t(key, options),
}));

// Per-page usage
<PageGenerator
  id="i18n-page"
  ns="myPage"
  meta={{
    title: i18next.t("myPage:meta.title"),
    description: i18next.t("myPage:meta.description"),
  }}
  contents={[...]}
/>

Performance Optimization

The library tracks which data keys each component reads via the get() function and re-renders only the affected items when those values change. No manual dependency declarations needed.

| Technique | Impact | | ------------------------------------- | ------------------------------------------- | | Automatic get() dependency tracking | 90% reduction in query-driven re-renders | | Form debounceDelay | Up to 80% reduction in keystroke re-renders | | React.memo + fast-deep-equal | Stable component identity across renders | | React Compiler | Automatic memoization at compile time |

<PageGenerator
  id="optimized-page"
  form={{
    data: [{ name: "search", debounceDelay: 300 }],
  }}
  contents={[
    {
      type: "custom",
      component: ({ get }) => {
        // Reading these keys registers dependencies automatically
        const search = get("form", "search", "");
        const results = get("query", "results.data", []);
        return <SearchResults search={search} results={results} />;
      },
    },
  ]}
/>

Tips:

  1. Use get() inside component functions — the library registers dependencies automatically
  2. Apply debounceDelay on search and filter fields
  3. Import selectively — use /hooks, /config, /utils sub-paths instead of the main entry
  4. Wrap stable components with React.memo if they receive no dynamic props

Bundle Size (All Tree-Shakeable)

| Entry Point | Size | Gzipped | | -------------------------------- | ------ | ------- | | Main (@gaddario98/react-pages) | ~90 KB | ~11 KB | | /components | ~90 KB | ~11 KB | | /hooks | ~76 KB | ~10 KB | | /config | ~12 KB | ~2 KB | | /utils | ~8 KB | ~2 KB |


API Reference

PageGenerator Props

| Prop | Type | Default | Description | | ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | | id | string | — | Unique page identifier (required) | | contents | ContentItemsType<F, Q, V> | [] | Page content items or a function returning them | | queries | QueryPageConfigArray<F, Q, V> | [] | Query and mutation definitions | | form | FormPageProps<F, Q, V> | — | Form configuration and fields | | variables | V | — | Page-scoped state variables, accessible via get("state", ...) | | viewSettings | ViewSettings \| MappedItemsFunction | {} | Layout and behavior settings | | meta | MetadataConfig<F, Q, V> | — | SEO and metadata configuration | | isActive | boolean | true | Controls whether the page is active and syncs metadata to <head>. Set to false for background/cached tabs | | ns | string | — | i18n namespace | | enableAuthControl | boolean | true | Whether to check authentication before rendering |

Content Item Types

type: "custom"

{
  type: "custom",
  component: React.JSX.Element | ((props: FunctionProps) => React.JSX.Element),
  index?: number,
  renderInHeader?: boolean,
  renderInFooter?: boolean,
  hidden?: boolean | ((props: FunctionProps) => boolean),
  key?: string,
  isDraggable?: boolean,
  isInDraggableView?: boolean,
  /** @deprecated — use get() inside component to track dependencies automatically */
  usedQueries?: string[],
}

type: "container"

{
  type: "container",
  items: ContentItem[],
  component?: React.ComponentType,  // custom ItemsContainer for this group
  // all optional props from "custom" apply here too
}

get / set Interface

FunctionProps<F, Q, V> provides the get and set accessors passed to component functions, query/mutation configs, submit handlers, and metadata evaluators:

| Signature | Returns | | ------------------------------------------ | ------------------------------------------------ | | get("query", "myQuery.data") | Typed query data | | get("query", "myQuery.isLoading", false) | boolean (with default) | | get("mutation", "myMutation.mutate") | Typed mutate function | | get("form", "fieldName", "") | Typed field value | | get("state", "myVar") | Value from variables prop | | set("form") | Returns setValue from @gaddario98/react-form | | set("state") | Returns a setter for variables |

Metadata Configuration

{
  // Basic
  title?: string | ((context: FunctionProps) => string),
  description?: string | ((context: FunctionProps) => string),
  keywords?: string[] | ((context: FunctionProps) => string[]),
  canonical?: string | ((context: FunctionProps) => string),
  lang?: string,
  author?: string,
  viewport?: string,
  themeColor?: string,

  // Indexing
  robots?: {
    noindex?: boolean,
    nofollow?: boolean,
    noarchive?: boolean,
    nosnippet?: boolean,
    maxImagePreview?: "none" | "standard" | "large",
    maxSnippet?: number,
  },
  disableIndexing?: boolean,  // shorthand: sets noindex + nofollow

  // Open Graph
  openGraph?: {
    type?: "website" | "article" | "product" | "profile",
    title?: string | ((context: FunctionProps) => string),
    description?: string | ((context: FunctionProps) => string),
    image?: string | OpenGraphImage | ((context: FunctionProps) => string | OpenGraphImage),
    images?: OpenGraphImage[] | ((context: FunctionProps) => OpenGraphImage[]),
    url?: string | ((context: FunctionProps) => string),
    siteName?: string,
    locale?: string,
    article?: OpenGraphArticle,  // used when type="article"
  },

  // Twitter Card
  twitter?: {
    card?: "summary" | "summary_large_image" | "app" | "player",
    site?: string,
    creator?: string,
    title?: string | ((context: FunctionProps) => string),
    description?: string | ((context: FunctionProps) => string),
    image?: string | ((context: FunctionProps) => string),
    imageAlt?: string | ((context: FunctionProps) => string),
  },

  // Alternates / hreflang
  alternates?: {
    canonical?: string,
    languages?: Record<string, string>,  // locale → URL
    media?: Record<string, string>,
    types?: Record<string, Array<{ url: string; title?: string }>>,
  },

  // Icons & PWA
  icons?: {
    icon?: string | IconConfig | IconConfig[],
    apple?: string | IconConfig | IconConfig[],
    shortcut?: string,
  },
  manifest?: string,

  // Structured Data (schema.org JSON-LD)
  structuredData?: {
    type: "Article" | "Product" | "WebPage" | "FAQPage" | "Organization"
        | "Person" | "WebSite" | "BreadcrumbList",
    schema: Record<string, unknown> | ((context: FunctionProps) => Record<string, unknown>),
  },

  // AI Crawler Hints
  aiHints?: {
    contentClassification?: string | ((context: FunctionProps) => string),
    modelHints?: string[] | ((context: FunctionProps) => string[]),
    contextualInfo?: string | ((context: FunctionProps) => string),
    excludeFromIndexing?: boolean,
  },

  // Custom tags
  customMeta?: Array<MetaTag> | ((context: FunctionProps) => Array<MetaTag>),
}

useMetadata Hook & Cleanup Utilities

For custom page wrappers or fine-grained programmatic control:

import { useMetadata, cleanupMetadata } from "@gaddario98/react-pages";

// Hook invoked internally by PageGenerator
useMetadata({
  meta: myMetadataConfig,
  isActive: true, // controls whether to apply metadata (default: true)
  ns: "my-namespace", // optional i18n namespace
  pageId: "my-page-id", // optional page identifier (used in data-page-id)
  autoApply: true, // whether to write directly to DOM (default: true)
});

// Imperatively remove tags managed by react-pages
cleanupMetadata(); // removes all managed tags
cleanupMetadata("my-page-id"); // removes tags for a specific pageId

Form Configuration

{
  data?: Array<FormManagerConfig | ((props: FunctionProps) => FormManagerConfig)>,
  submit?: Array<Submit> | ((props: FunctionProps) => Array<Submit>),
  defaultValues?: Partial<F>,
  defaultValueQueryKey?: string[],
  defaultValueQueryMap?: (data: QueryResult) => Partial<F>,
  debounceDelay?: number,  // global debounce for all fields (ms)
}

Query Configuration

// Query
{
  type: "query",
  key: string,
  queryConfig?: QueryProps | ((props: FunctionProps<F, Q>) => QueryProps),
}

// Mutation
{
  type: "mutation",
  key: string,
  mutationConfig?: MutationConfig | ((props: FunctionProps<F, Q>) => MutationConfig),
}

Advanced Patterns

Dependent Queries

Pass queryConfig as a function to access resolved query data when defining a subsequent query:

<PageGenerator
  id="dependent-queries"
  queries={[
    {
      type: "query",
      key: "user",
      queryConfig: { queryKey: ["user"], queryFn: fetchUser },
    },
    {
      type: "query",
      key: "posts",
      queryConfig: ({ get }) => ({
        queryKey: ["posts", get("query", "user.data.id")],
        queryFn: () =>
          fetch(`/api/users/${get("query", "user.data.id")}/posts`).then((r) => r.json()),
        enabled: !!get("query", "user.data.id"),
      }),
    },
  ]}
  contents={({ get }) => [...]}
/>

Dynamic Content Driven by Query State

Use contents as a function to conditionally build the content array based on query state:

<PageGenerator
  id="dynamic-page"
  queries={[...]}
  contents={({ get }) => [
    {
      type: "custom",
      component: (
        <div>
          {get("query", "data.isLoading", false) && <Spinner />}
          {get("query", "data.error") && (
            <Error message={get("query", "data.error.message", "")} />
          )}
          {get("query", "data.data") && (
            <DataDisplay data={get("query", "data.data")} />
          )}
        </div>
      ),
    },
  ]}
/>

Form-Driven Dynamic Content

Show different content sections based on current form values:

<PageGenerator
  id="form-with-dynamic-content"
  form={{
    data: [
      { name: "type", type: "select", options: ["A", "B", "C"] },
      { name: "details", type: "textarea" },
    ],
  }}
  contents={({ get }) => [
    {
      type: "custom",
      component: <TypeSpecificContent type={get("form", "type")} />,
    },
  ]}
/>

TypeScript Support

PageGenerator is fully generic over form data (F), query definitions (Q), and page-scoped variables (V), providing end-to-end type safety including autocomplete for field names and type-safe query key paths.

interface UserFormData {
  username: string;
  email: string;
}

type PageQueries = [
  QueryDefinition<"users", "query", never, User[]>,
  QueryDefinition<"createUser", "mutation", UserFormData, User>,
];

<PageGenerator<UserFormData, PageQueries>
  id="typed-page"
  form={{
    data: [
      { name: "username", type: "text" }, // autocompleted: only valid field names
      { name: "email", type: "email" },
    ],
  }}
  contents={({ get }) => [
    {
      type: "custom",
      component: (() => {
        const mutate = get("mutation", "createUser.mutate");
        const formValues = {
          username: get("form", "username", ""),
          email: get("form", "email", ""),
        };
        return <button onClick={() => mutate(formValues)}>Create User</button>;
      })(),
    },
  ]}
/>;

React Native & Ionic Integration

React Native Setup

Mount a provider once near the root to replace the container slots with native components:

// rnPages/setup.tsx
import { View, ScrollView } from "react-native";
import { usePageConfigState } from "@gaddario98/react-pages";
import { useEffect } from "react";

export function ReactNativePageProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [, setPageConfig] = usePageConfigState();

  useEffect(() => {
    setPageConfig((prev) => ({
      ...prev,
      PageContainer: ({ children, id }) => (
        <View style={{ flex: 1 }} accessibilityLabel={id}>
          {children}
        </View>
      ),
      BodyContainer: ({ children }) => (
        <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
          {children}
        </ScrollView>
      ),
      HeaderContainer: ({ children }) => <View>{children}</View>,
      FooterContainer: ({ children }) => <View>{children}</View>,
      ItemsContainer: ({ children }) => <View>{children}</View>,
    }));
  }, [setPageConfig]);

  return <>{children}</>;
}
// App.tsx
export default function App() {
  return (
    <ReactNativePageProvider>
      <NavigationContainer>{/* your screens */}</NavigationContainer>
    </ReactNativePageProvider>
  );
}

Once configured, PageGenerator works identically on React Native:

import { PageGenerator } from "@gaddario98/react-pages";
import { Text } from "react-native";

export function ProfileScreen() {
  return (
    <PageGenerator
      id="profile-screen"
      queries={[
        {
          type: "query",
          key: "profile",
          queryConfig: {
            queryKey: ["profile"],
            queryFn: fetchProfile,
          },
        },
      ]}
      contents={({ get }) => [
        {
          type: "custom",
          component: (
            <Text>{get("query", "profile.data.name", "Loading...")}</Text>
          ),
        },
      ]}
    />
  );
}

Ionic Integration

Within the monorepo, @gaddario98/react-pages is extended by @gaddario98/react-ionic-pages which overrides PageContainer with Ionic's <IonPage> component:

// react-ionic-pages/components/page/index.tsx
import React from "react";
import { IonPage } from "@ionic/react";
import { withMemo } from "@gaddario98/utiles";
import { pageConfig } from "@gaddario98/react-pages";

export const IonicPageContainer = withMemo(
  ({ children, id }: React.ComponentProps<typeof pageConfig.PageContainer>) => {
    return (
      <IonPage id={id} key={id}>
        {children}
      </IonPage>
    );
  },
);

Compatibility Summary

| Feature | Behavior on React Native | | ----------------- | ----------------------------------------------------- | | Form management | Works — field components are provided by the consumer | | Query / Mutation | Works — no DOM dependency | | MetadataManager | Silent no-op |


Troubleshooting

Form Not Submitting

  • Ensure submit is configured in the form prop
  • Check that form validation passes before submission
  • Verify the onSuccess callback (or mutation) does not throw

Queries Not Loading

  • Check that queryFn is correctly defined
  • Verify enabled is not set to false
  • Inspect network requests in DevTools

Metadata Not Appearing

  • Ensure meta is passed to PageGenerator
  • Confirm you are on a web platform (document.head must be available)
  • For SSR: use collectMetadataToHtml() from /config to inject tags server-side

Re-renders Too Frequent

  • Use component: ({ get }) => ... instead of passing pre-computed values as JSX props
  • Add debounceDelay to text and search fields
  • Wrap stable external components with React.memo

TypeScript Errors

  • Ensure the generic types <F, Q, V> match your form interface, query tuple, and variables type
  • Use as const assertions on query key arrays
  • Verify that all required peer dependencies are installed

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support