UNPKG

@gaddario98/react-pages

Version:

A powerful, performance-optimized React component library for creating dynamic pages that work seamlessly across web (React DOM) and React Native with integrated form management, query handling, SEO metadata, lazy loading, and content rendering.

1 lines 181 kB
{"version":3,"file":"index.mjs","sources":["../../../hooks/useDataExtractor.tsx","../../../hooks/useFormPage.ts","../../../utils/dependencyGraph.ts","../../../hooks/useDependencyGraph.ts","../../../hooks/useGenerateContentRender.tsx","../../../config/metadata.ts","../../../config/index.ts","../../../utils/optimization.ts","../../../components/RenderComponent.tsx","../../../components/Container.tsx","../../../hooks/useGenerateContent.tsx","../../../hooks/useMemoizedProps.ts","../../../hooks/usePageQueries.ts","../../../hooks/useViewSettings.ts","../../../utils/merge.ts","../../../hooks/useFormData.ts","../../../config/platformAdapters/web.ts","../../../config/platformAdapters/native.ts","../../../config/platformAdapters/index.ts","../../../config/PlatformAdapterProvider.tsx","../../../hooks/usePlatformAdapter.ts","../../../hooks/useLifecycleCallbacks.ts","../../../hooks/usePageConfig.tsx","../../../hooks/usePageUtiles.tsx"],"sourcesContent":["import {\n AllMutation,\n MultipleQueryResponse,\n QueriesArray,\n} from \"@gaddario98/react-queries\";\nimport { createExtractor, ExtractorCache } from \"@gaddario98/utiles\";\nimport { useRef, useCallback } from \"react\";\nimport { FieldValues } from \"react-hook-form\";\n\ninterface UseDataExtractorProps<\n F extends FieldValues,\n Q extends QueriesArray,\n> {\n formValues: F;\n allQuery: MultipleQueryResponse<Q>;\n allMutation: AllMutation<Q>;\n}\n\nfunction createQueryExtractor<\n D extends QueriesArray,\n V extends MultipleQueryResponse<D>,\n K extends keyof V = keyof V,\n>(allQuery: V, queryCacheRef: ExtractorCache<Partial<V>>, usedKeys?: K[]) {\n return createExtractor<V>(allQuery, queryCacheRef, usedKeys);\n}\n\nfunction createMutationExtractor<\n D extends QueriesArray,\n V extends AllMutation<D>,\n K extends keyof V = keyof V,\n>(\n allMutation: V,\n mutationCacheRef: ExtractorCache<Partial<V>>,\n usedKeys?: K[]\n) {\n return createExtractor<V>(allMutation, mutationCacheRef, usedKeys);\n}\n\nfunction createFormValuesExtractor<\n D extends FieldValues,\n V extends D,\n K extends keyof V = keyof V,\n>(\n formValues: V,\n formValuesCacheRef: ExtractorCache<Partial<V>>,\n usedKeys?: K[]\n) {\n return createExtractor<V>(formValues, formValuesCacheRef, usedKeys);\n}\n\nexport function useDataExtractor<\n F extends FieldValues,\n Q extends QueriesArray,\n>({ allQuery, allMutation, formValues }: UseDataExtractorProps<F, Q>) {\n // Initialize cache refs\n const queryCacheRef = useRef<\n ExtractorCache<Partial<MultipleQueryResponse<Q>>>\n >(new Map());\n const mutationCacheRef = useRef<ExtractorCache<Partial<AllMutation<Q>>>>(\n new Map()\n );\n const formValuesCacheRef = useRef<\n ExtractorCache<Partial<F>>\n >(new Map());\n\n // Create extractors with caching\n const extractQuery = useCallback(\n (usedKeys: Array<keyof MultipleQueryResponse<Q>>) =>\n createQueryExtractor<Q, MultipleQueryResponse<Q>>(\n allQuery,\n queryCacheRef.current,\n usedKeys\n ),\n [allQuery]\n );\n\n const extractMutations = useCallback(\n (usedKeys: Array<keyof AllMutation<Q>>) =>\n createMutationExtractor<Q, AllMutation<Q>>(\n allMutation,\n mutationCacheRef.current,\n usedKeys\n ),\n [allMutation]\n );\n\n const extractFormValues = useCallback(\n (usedKeys: Array<keyof F>) =>\n createFormValuesExtractor<F, F>(\n formValues,\n formValuesCacheRef.current,\n usedKeys\n ),\n [formValues]\n );\n\n // Clear cache utility\n const clearCache = useCallback(() => {\n queryCacheRef.current.clear();\n mutationCacheRef.current.clear();\n formValuesCacheRef.current.clear();\n }, []);\n\n return {\n extractQuery,\n extractMutations,\n extractFormValues,\n clearCache,\n cacheRefs: {\n queryCacheRef,\n mutationCacheRef,\n formValuesCacheRef,\n },\n };\n}\n","import { useState, useEffect, useMemo, useCallback } from \"react\";\nimport { DefaultValues, useForm, FieldValues, Path } from \"react-hook-form\";\nimport { useQueryClient, QueryObserver } from \"@tanstack/react-query\";\nimport { useDebounce } from \"use-debounce\";\nimport { FormPageProps } from \"../types\";\nimport { QueriesArray } from \"@gaddario98/react-queries\";\n\nexport const useFormPage = <F extends FieldValues, Q extends QueriesArray>({\n form,\n}: {\n form?: FormPageProps<F, Q>;\n}) => {\n const queryClient = useQueryClient();\n const [defaultValueQuery, setDefaultValueQuery] = useState<\n DefaultValues<F> | undefined\n >(form?.defaultValues);\n\n useEffect(() => {\n if (!form?.defaultValueQueryKey) {\n setDefaultValueQuery(form?.defaultValues);\n return;\n }\n const initialData = queryClient.getQueryData<DefaultValues<F>>(\n form.defaultValueQueryKey\n );\n if (initialData) {\n setDefaultValueQuery(initialData);\n }\n const observer = new QueryObserver<DefaultValues<F>>(queryClient, {\n queryKey: form.defaultValueQueryKey,\n enabled: true,\n notifyOnChangeProps: [\"data\"],\n refetchOnWindowFocus: false,\n });\n const unsubscribe = observer.subscribe((result) => {\n if (result.data !== undefined) {\n setDefaultValueQuery(result.data);\n }\n });\n return () => unsubscribe();\n }, [form?.defaultValueQueryKey, form?.defaultValues, queryClient]);\n\n const defaultValues = useMemo(\n () =>\n ({\n ...(defaultValueQuery ?? {}),\n ...(form?.defaultValueQueryMap?.(defaultValueQuery) ?? {}),\n }) as DefaultValues<F>,\n [defaultValueQuery, form]\n );\n\n const formControl = useForm<F>({\n mode: \"all\",\n ...(form?.formSettings ?? {}),\n defaultValues,\n resetOptions: {\n keepDirtyValues: true,\n keepDefaultValues: false,\n ...(form?.formSettings?.resetOptions ?? {}),\n },\n });\n\n // Memoize formControl to avoid unnecessary re-renders\n const stableFormControl = useMemo(() => formControl, []);\n\n useEffect(() => {\n stableFormControl.reset(defaultValues, {\n keepDirtyValues: true,\n keepDefaultValues: false,\n });\n }, [defaultValues, stableFormControl]);\n\n // Watch form values (raw, updates on every keystroke)\n const rawFormValues = stableFormControl.watch();\n\n // NEW IN 2.0: Debounce form values to reduce re-render cascades\n // Default 300ms delay - reduces re-renders by ~80% during rapid typing\n // Components using formValues will only re-render after user stops typing\n const [debouncedFormValues] = useDebounce(\n rawFormValues,\n form?.debounceDelay ?? 300\n );\n\n const setValueAndTrigger = useCallback(\n async (\n name: Path<F>,\n value: any,\n options?: Parameters<typeof stableFormControl.setValue>[2]\n ) => {\n stableFormControl.setValue(name, value, options);\n await stableFormControl.trigger(name);\n },\n [stableFormControl]\n );\n\n return {\n formValues: debouncedFormValues, // Return debounced values\n rawFormValues, // Also expose raw values for immediate updates (e.g., input controlled components)\n formControl: stableFormControl,\n setValue: setValueAndTrigger,\n };\n};\n","/**\n * Dependency Graph Module\n * Tracks component dependencies for selective re-rendering optimization\n *\n * @module utils/dependencyGraph\n */\n\n/**\n * Represents a node in the dependency graph\n * Tracks which queries, form values, and mutations a component depends on\n */\nexport interface DependencyNode {\n /** Unique identifier for this component */\n componentId: string;\n\n /** Query keys this component depends on */\n usedQueries: string[];\n\n /** Form field keys this component depends on */\n usedFormValues: string[];\n\n /** Mutation keys this component uses */\n usedMutations: string[];\n\n /** Parent component ID (null for root) */\n parentComponent: string | null;\n\n /** Child component IDs */\n childComponents: string[];\n}\n\n/**\n * Manages component dependencies for efficient selective re-rendering\n *\n * @example\n * ```typescript\n * const graph = new DependencyGraph();\n *\n * // Register a component and its dependencies\n * graph.addNode({\n * componentId: 'item-1',\n * usedQueries: ['getUser', 'getPosts'],\n * usedFormValues: ['username'],\n * usedMutations: ['updateProfile'],\n * parentComponent: null,\n * childComponents: [],\n * });\n *\n * // Find affected components when query updates\n * const affected = graph.getAffectedComponents(['getUser']);\n * // Returns: ['item-1']\n * ```\n */\nexport class DependencyGraph {\n private nodes: Map<string, DependencyNode> = new Map();\n\n /**\n * Register a component and its dependencies in the graph\n * @param node The dependency node to add\n */\n addNode(node: DependencyNode): void {\n this.nodes.set(node.componentId, node);\n\n // Update parent's children list if parent exists\n if (node.parentComponent) {\n const parent = this.nodes.get(node.parentComponent);\n if (\n parent &&\n !parent.childComponents.includes(node.componentId)\n ) {\n parent.childComponents.push(node.componentId);\n }\n }\n }\n\n /**\n * Retrieve a dependency node by component ID\n * @param componentId The component ID to look up\n * @returns The dependency node or undefined if not found\n */\n getNode(componentId: string): DependencyNode | undefined {\n return this.nodes.get(componentId);\n }\n\n /**\n * Find all components affected by changed data keys\n * Used to determine which components need re-rendering\n *\n * @param changedKeys Array of changed query keys or form field keys\n * @returns Array of component IDs that need re-rendering\n */\n getAffectedComponents(changedKeys: string[]): string[] {\n const affected: string[] = [];\n\n for (const [componentId, node] of this.nodes.entries()) {\n // Check if any changed key matches this component's dependencies\n const hasAffectedQuery = node.usedQueries.some(q =>\n changedKeys.includes(q)\n );\n const hasAffectedFormValue = node.usedFormValues.some(f =>\n changedKeys.includes(f)\n );\n const hasAffectedMutation = node.usedMutations.some(m =>\n changedKeys.includes(m)\n );\n\n if (hasAffectedQuery || hasAffectedFormValue || hasAffectedMutation) {\n affected.push(componentId);\n }\n }\n\n return affected;\n }\n\n /**\n * Detect circular dependencies in the graph\n * Helps identify configuration errors that could cause infinite loops\n *\n * @returns Array of circular dependency paths for debugging\n */\n detectCircularDependencies(): string[][] {\n const cycles: string[][] = [];\n const visited = new Set<string>();\n const stack = new Set<string>();\n\n const dfs = (nodeId: string, path: string[]): void => {\n if (stack.has(nodeId)) {\n // Found a cycle\n const cycleStart = path.indexOf(nodeId);\n if (cycleStart >= 0) {\n cycles.push(path.slice(cycleStart).concat(nodeId));\n }\n return;\n }\n\n if (visited.has(nodeId)) return;\n\n visited.add(nodeId);\n stack.add(nodeId);\n path.push(nodeId);\n\n const node = this.nodes.get(nodeId);\n if (node) {\n for (const childId of node.childComponents) {\n dfs(childId, [...path]);\n }\n }\n\n stack.delete(nodeId);\n };\n\n // Check for cycles starting from each unvisited node\n for (const nodeId of this.nodes.keys()) {\n if (!visited.has(nodeId)) {\n dfs(nodeId, []);\n }\n }\n\n return cycles;\n }\n\n /**\n * Get all nodes in the graph\n * @returns Map of all dependency nodes\n */\n getAllNodes(): Map<string, DependencyNode> {\n return new Map(this.nodes);\n }\n\n /**\n * Clear all nodes from the graph\n */\n clear(): void {\n this.nodes.clear();\n }\n\n /**\n * Get the size of the graph (number of nodes)\n */\n size(): number {\n return this.nodes.size;\n }\n\n /**\n * Check if a node exists in the graph\n */\n hasNode(componentId: string): boolean {\n return this.nodes.has(componentId);\n }\n\n /**\n * Remove a node from the graph\n * @param componentId The component ID to remove\n */\n removeNode(componentId: string): void {\n const node = this.nodes.get(componentId);\n if (!node) return;\n\n // Update parent's children list\n if (node.parentComponent) {\n const parent = this.nodes.get(node.parentComponent);\n if (parent) {\n parent.childComponents = parent.childComponents.filter(\n id => id !== componentId\n );\n }\n }\n\n // Update children's parent reference\n for (const childId of node.childComponents) {\n const child = this.nodes.get(childId);\n if (child) {\n child.parentComponent = null;\n }\n }\n\n this.nodes.delete(componentId);\n }\n\n /**\n * Get the depth of a component in the dependency tree\n * @param componentId The component ID\n * @returns The depth (0 for root)\n */\n getDepth(componentId: string): number {\n const node = this.nodes.get(componentId);\n if (!node || !node.parentComponent) return 0;\n\n let depth = 1;\n let parent = this.nodes.get(node.parentComponent);\n while (parent && parent.parentComponent) {\n depth++;\n parent = this.nodes.get(parent.parentComponent);\n }\n return depth;\n }\n\n /**\n * Get all leaf nodes (components with no children)\n */\n getLeafNodes(): DependencyNode[] {\n return Array.from(this.nodes.values()).filter(\n node => node.childComponents.length === 0\n );\n }\n\n /**\n * Get all root nodes (components with no parent)\n */\n getRootNodes(): DependencyNode[] {\n return Array.from(this.nodes.values()).filter(\n node => node.parentComponent === null\n );\n }\n}\n","/**\n * useDependencyGraph Hook\n * Manages component dependency tracking for selective re-rendering\n *\n * @module hooks/useDependencyGraph\n */\n\nimport { useRef, useCallback, useMemo } from 'react';\nimport { DependencyGraph, DependencyNode } from '../utils/dependencyGraph';\n\n/**\n * Hook for managing a dependency graph within a page\n * Provides methods to register components and find affected components\n *\n * @example\n * ```typescript\n * function PageRenderer() {\n * const {\n * graph,\n * registerComponent,\n * getAffectedComponents,\n * detectCircularDependencies\n * } = useDependencyGraph();\n *\n * // Register content items\n * useEffect(() => {\n * contentItems.forEach((item, index) => {\n * registerComponent({\n * componentId: `item-${index}`,\n * usedQueries: item.usedQueries || [],\n * usedFormValues: item.usedFormValues || [],\n * usedMutations: [],\n * parentComponent: null,\n * childComponents: [],\n * });\n * });\n *\n * // Check for circular dependencies\n * const cycles = detectCircularDependencies();\n * if (cycles.length > 0) {\n * console.warn('[DependencyGraph] Circular dependencies:', cycles);\n * }\n * }, [contentItems]);\n *\n * // When query updates\n * const handleQueryUpdate = useCallback((queryKey: string) => {\n * const affected = getAffectedComponents([queryKey]);\n * // Re-render only affected components\n * }, [getAffectedComponents]);\n * }\n * ```\n */\nexport function useDependencyGraph() {\n const graphRef = useRef<DependencyGraph>(new DependencyGraph());\n\n /**\n * Register a component and its dependencies\n */\n const registerComponent = useCallback((node: DependencyNode) => {\n graphRef.current.addNode(node);\n }, []);\n\n /**\n * Get a specific node from the graph\n */\n const getNode = useCallback((componentId: string) => {\n return graphRef.current.getNode(componentId);\n }, []);\n\n /**\n * Find all components affected by changed keys\n */\n const getAffectedComponents = useCallback((changedKeys: string[]) => {\n return graphRef.current.getAffectedComponents(changedKeys);\n }, []);\n\n /**\n * Detect circular dependencies\n */\n const detectCircularDependencies = useCallback(() => {\n return graphRef.current.detectCircularDependencies();\n }, []);\n\n /**\n * Clear all nodes from the graph\n */\n const clear = useCallback(() => {\n graphRef.current.clear();\n }, []);\n\n /**\n * Remove a specific component from the graph\n */\n const removeComponent = useCallback((componentId: string) => {\n graphRef.current.removeNode(componentId);\n }, []);\n\n /**\n * Check if a component is registered\n */\n const hasComponent = useCallback((componentId: string) => {\n return graphRef.current.hasNode(componentId);\n }, []);\n\n /**\n * Get graph statistics\n */\n const getStats = useCallback(() => {\n return {\n totalNodes: graphRef.current.size(),\n rootNodes: graphRef.current.getRootNodes().length,\n leafNodes: graphRef.current.getLeafNodes().length,\n };\n }, []);\n\n // eslint-disable-next-line react-hooks/refs\n return useMemo(\n () => ({\n // eslint-disable-next-line react-hooks/refs\n graph: graphRef.current,\n registerComponent,\n getNode,\n getAffectedComponents,\n detectCircularDependencies,\n clear,\n removeComponent,\n hasComponent,\n getStats,\n }),\n [\n registerComponent,\n getNode,\n getAffectedComponents,\n detectCircularDependencies,\n clear,\n removeComponent,\n hasComponent,\n getStats,\n ]\n );\n}\n\n/**\n * Hook variant that automatically registers components from a list\n * Simplifies common use case of registering content items\n */\nexport function useAutoRegisterDependencies<T extends { usedQueries?: string[]; usedFormValues?: string[]; key?: string }>(\n items: T[],\n idPrefix: string = 'item'\n) {\n const {\n graph,\n registerComponent,\n getAffectedComponents,\n detectCircularDependencies,\n clear\n } = useDependencyGraph();\n\n // Register all items\n useMemo(() => {\n // Clear previous registrations\n clear();\n\n items.forEach((item, index) => {\n const componentId = item.key || `${idPrefix}-${index}`;\n registerComponent({\n componentId,\n usedQueries: item.usedQueries || [],\n usedFormValues: item.usedFormValues || [],\n usedMutations: [],\n parentComponent: null,\n childComponents: [],\n });\n });\n\n // Detect and warn about circular dependencies\n const cycles = detectCircularDependencies();\n if (cycles.length > 0 && typeof console !== 'undefined') {\n console.warn('[useDependencyGraph] Circular dependencies detected:', cycles);\n }\n }, [items, idPrefix, registerComponent, detectCircularDependencies, clear]);\n\n return {\n graph,\n getAffectedComponents,\n };\n}\n","import { useMemo, useRef } from \"react\";\nimport { FieldValues, UseFormSetValue } from \"react-hook-form\";\nimport {\n AllMutation,\n MultipleQueryResponse,\n QueriesArray,\n} from \"@gaddario98/react-queries\";\nimport {\n FormElements,\n FormManagerConfig,\n Submit,\n} from \"@gaddario98/react-form\";\nimport { ContentItem, ContentItemsType } from \"../types\";\nimport { useDataExtractor } from \"./useDataExtractor\";\nimport { useAutoRegisterDependencies } from \"./useDependencyGraph\";\n\nexport interface GenerateContentRenderProps<\n F extends FieldValues,\n Q extends QueriesArray,\n> {\n contents?: ContentItemsType<F, Q>;\n ns?: string;\n pageId: string;\n formValues: F;\n allQuery: MultipleQueryResponse<Q>;\n allMutation: AllMutation<Q>;\n isAllQueryMapped?: boolean;\n formData:\n | false\n | {\n elements: FormElements[];\n formContents: (FormManagerConfig<F> | Submit<F>)[];\n };\n setValue: UseFormSetValue<F>;\n renderComponent: (props: {\n content: ContentItem<F, Q>;\n ns: string;\n formValues: F;\n pageId: string;\n allMutation: AllMutation<Q>;\n allQuery: MultipleQueryResponse<Q>;\n setValue: UseFormSetValue<F>;\n key: string;\n }) => JSX.Element;\n}\nexport interface Elements {\n index: number;\n element: JSX.Element;\n renderInFooter: boolean;\n renderInHeader: boolean;\n key: string;\n}\nexport const useGenerateContentRender = <\n F extends FieldValues,\n Q extends QueriesArray,\n>({\n pageId,\n ns = \"\",\n contents = [],\n allMutation,\n allQuery,\n formValues,\n isAllQueryMapped,\n formData,\n setValue,\n renderComponent,\n}: GenerateContentRenderProps<F, Q>) => {\n const memorizedContentsRef = useRef<Elements[]>([]);\n\n const contentsWithQueriesDeps = useMemo(() => {\n if (typeof contents === \"function\" && isAllQueryMapped) {\n return contents({\n formValues,\n allMutation,\n allQuery,\n setValue,\n });\n }\n return Array.isArray(contents) ? contents : [];\n }, [contents, isAllQueryMapped, formValues, allMutation, allQuery, setValue]);\n\n const filteredContents = useMemo(() => {\n if (typeof contents === \"function\") {\n return contentsWithQueriesDeps.filter((el) => !el?.hidden);\n } else {\n return contents.filter((el) => !el?.hidden);\n }\n }, [contents, contentsWithQueriesDeps]);\n\n // Register content items with dependency graph for selective re-rendering\n const { getAffectedComponents } = useAutoRegisterDependencies(\n filteredContents,\n `${pageId}-content`\n );\n\n const { extractFormValues, extractMutations, extractQuery } =\n useDataExtractor<F, Q>({\n allMutation,\n allQuery,\n formValues,\n });\n\n const memorizedContents = useMemo(() => {\n if (!isAllQueryMapped) return [];\n const getStableKey = (content: ContentItem<F, Q>, index: number) =>\n content.key ?? `content-${index}`;\n const dynamicElements = filteredContents.map((content, index: number) => {\n const stableKey = getStableKey(content, index);\n return {\n element: renderComponent({\n content,\n ns,\n formValues: extractFormValues(content.usedFormValues ?? []),\n pageId,\n allMutation: extractMutations(content.usedQueries ?? []),\n allQuery: extractQuery(\n (content.usedQueries ?? []) as Array<keyof MultipleQueryResponse<Q>>\n ),\n setValue,\n key: stableKey,\n }),\n index: content.index ?? index,\n renderInFooter: !!content.renderInFooter,\n renderInHeader: !!content.renderInHeader,\n key: stableKey,\n };\n });\n let formElementsWithKey: (FormElements & { key: string })[] = [];\n if (formData && Array.isArray(formData.elements)) {\n formElementsWithKey = (formData.elements as FormElements[]).map(\n (el: FormElements, idx: number) => ({\n ...el,\n key: (el as any).key ?? `form-element-${el.index ?? idx}`,\n })\n );\n }\n const next = [...dynamicElements, ...formElementsWithKey].sort(\n (a, b) => a.index - b.index || String(a.key).localeCompare(String(b.key))\n );\n const prev = memorizedContentsRef.current;\n // eslint-disable-next-line react-hooks/refs\n const merged = next.map((el) => {\n const found = prev.find((e) => e.key === el.key);\n if (found) {\n return { ...found, ...el, element: el.element };\n }\n return el;\n });\n // eslint-disable-next-line react-hooks/refs\n memorizedContentsRef.current = merged;\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n isAllQueryMapped,\n filteredContents,\n ns,\n pageId,\n setValue,\n extractFormValues,\n extractMutations,\n extractQuery,\n formData,\n ]);\n\n return {\n components: memorizedContents,\n allContents: [\n ...filteredContents,\n ...(!formData ? [] : (formData?.formContents ?? [])),\n ],\n // Expose dependency graph utilities for selective re-rendering\n getAffectedComponents,\n };\n};\n","/**\n * Custom Metadata Configuration System\n * Replaces react-helmet-async with ~1KB native implementation\n * Platform-agnostic: Web, React Native, and SSR support\n */\n\nimport { MetadataConfig, MetaTag } from \"./types\";\n\n// Store current metadata for SSR and getMetadata()\nlet currentMetadata: MetadataConfig = {};\n\n// Platform detection\nconst isWeb = typeof document !== \"undefined\";\nconst isReactNative =\n typeof navigator !== \"undefined\" && navigator.product === \"ReactNative\";\n\n/**\n * Apply metadata configuration to the page\n * @param config - Metadata configuration object\n */\nexport const setMetadata = (config: MetadataConfig): void => {\n // Store for getMetadata()\n currentMetadata = { ...currentMetadata, ...config };\n\n // SSR or React Native - just store, don't manipulate DOM\n if (!isWeb) {\n return;\n }\n\n // Set document title\n if (config.title && typeof config.title === 'string') {\n document.title = config.title;\n }\n\n // Helper to update or create meta tags\n const updateOrCreateMeta = (\n selector: string,\n content: string,\n attributes: Record<string, string> = {}\n ) => {\n let element = document.querySelector(selector) as HTMLMetaElement;\n if (!element) {\n element = document.createElement(\"meta\");\n Object.entries(attributes).forEach(([key, value]) => {\n element.setAttribute(key, value);\n });\n document.head.appendChild(element);\n }\n element.setAttribute(\"content\", content);\n };\n\n // Set standard meta tags\n if (config.description && typeof config.description === 'string') {\n updateOrCreateMeta('meta[name=\"description\"]', config.description, {\n name: \"description\",\n });\n }\n\n if (config.keywords && Array.isArray(config.keywords)) {\n updateOrCreateMeta('meta[name=\"keywords\"]', config.keywords.join(\", \"), {\n name: \"keywords\",\n });\n }\n\n if (config.author && typeof config.author === 'string') {\n updateOrCreateMeta('meta[name=\"author\"]', config.author, {\n name: \"author\",\n });\n }\n\n if (config.viewport) {\n updateOrCreateMeta('meta[name=\"viewport\"]', config.viewport, {\n name: \"viewport\",\n });\n }\n\n if (config.themeColor) {\n updateOrCreateMeta('meta[name=\"theme-color\"]', config.themeColor, {\n name: \"theme-color\",\n });\n }\n\n // Set Open Graph meta tags (T059)\n if (config.openGraph) {\n const og = config.openGraph;\n\n if (og.title && typeof og.title === 'string') {\n updateOrCreateMeta('meta[property=\"og:title\"]', og.title, {\n property: \"og:title\",\n });\n }\n\n if (og.description && typeof og.description === 'string') {\n updateOrCreateMeta('meta[property=\"og:description\"]', og.description, {\n property: \"og:description\",\n });\n }\n\n if (og.image && typeof og.image === 'string') {\n updateOrCreateMeta('meta[property=\"og:image\"]', og.image, {\n property: \"og:image\",\n });\n }\n\n if (og.url && typeof og.url === 'string') {\n updateOrCreateMeta('meta[property=\"og:url\"]', og.url, {\n property: \"og:url\",\n });\n }\n\n if (og.type && typeof og.type === 'string') {\n updateOrCreateMeta('meta[property=\"og:type\"]', og.type, {\n property: \"og:type\",\n });\n }\n\n if (og.siteName && typeof og.siteName === 'string') {\n updateOrCreateMeta('meta[property=\"og:site_name\"]', og.siteName, {\n property: \"og:site_name\",\n });\n }\n\n if (og.locale && typeof og.locale === 'string') {\n updateOrCreateMeta('meta[property=\"og:locale\"]', og.locale, {\n property: \"og:locale\",\n });\n }\n }\n\n // Backward compatibility: legacy ogImage, ogTitle (ogDescription handled via description)\n const configWithLegacy = config as any;\n\n if (configWithLegacy.ogImage) {\n updateOrCreateMeta('meta[property=\"og:image\"]', configWithLegacy.ogImage, {\n property: \"og:image\",\n });\n }\n\n if (configWithLegacy.ogTitle) {\n updateOrCreateMeta('meta[property=\"og:title\"]', configWithLegacy.ogTitle, {\n property: \"og:title\",\n });\n }\n\n if (configWithLegacy.ogDescription) {\n updateOrCreateMeta(\n 'meta[property=\"og:description\"]',\n configWithLegacy.ogDescription,\n { property: \"og:description\" }\n );\n }\n\n // Set robots directive (T062)\n if (config.robots) {\n const robotsValue = typeof config.robots === 'string'\n ? config.robots\n : [\n config.robots.noindex ? 'noindex' : 'index',\n config.robots.nofollow ? 'nofollow' : 'follow',\n config.robots.noarchive && 'noarchive',\n config.robots.nosnippet && 'nosnippet',\n config.robots.maxImagePreview && `max-image-preview:${config.robots.maxImagePreview}`,\n config.robots.maxSnippet && `max-snippet:${config.robots.maxSnippet}`,\n ].filter(Boolean).join(', ');\n\n updateOrCreateMeta('meta[name=\"robots\"]', robotsValue, {\n name: \"robots\",\n });\n }\n\n // Set structured data JSON-LD (T060)\n if (config.structuredData) {\n const schemaScriptId = 'react-pages-schema-org';\n let scriptElement = document.querySelector(`script[id=\"${schemaScriptId}\"]`) as HTMLScriptElement;\n\n if (!scriptElement) {\n scriptElement = document.createElement('script');\n scriptElement.type = 'application/ld+json';\n scriptElement.id = schemaScriptId;\n document.head.appendChild(scriptElement);\n }\n\n scriptElement.textContent = JSON.stringify(config.structuredData);\n }\n\n // Set AI crawler hints (T061)\n if (config.aiHints) {\n const hints = config.aiHints;\n\n if (hints.contentClassification && typeof hints.contentClassification === 'string') {\n updateOrCreateMeta(\n 'meta[name=\"ai-content-classification\"]',\n hints.contentClassification,\n { name: 'ai-content-classification' }\n );\n }\n\n if (hints.modelHints) {\n const modelHints = Array.isArray(hints.modelHints)\n ? hints.modelHints.join(', ')\n : typeof hints.modelHints === 'string'\n ? hints.modelHints\n : undefined;\n\n if (modelHints) {\n updateOrCreateMeta(\n 'meta[name=\"ai-model-hints\"]',\n modelHints,\n { name: 'ai-model-hints' }\n );\n }\n }\n\n if (hints.contextualInfo && typeof hints.contextualInfo === 'string') {\n updateOrCreateMeta(\n 'meta[name=\"ai-context\"]',\n hints.contextualInfo,\n { name: 'ai-context' }\n );\n }\n\n if (hints.excludeFromIndexing) {\n updateOrCreateMeta(\n 'meta[name=\"ai-exclude-from-indexing\"]',\n 'true',\n { name: 'ai-exclude-from-indexing' }\n );\n }\n }\n\n // Set canonical link\n if (config.canonical) {\n let link = document.querySelector('link[rel=\"canonical\"]') as HTMLLinkElement;\n if (!link) {\n link = document.createElement(\"link\");\n link.rel = \"canonical\";\n document.head.appendChild(link);\n }\n link.href = config.canonical;\n }\n\n // Set language\n if (config.lang) {\n document.documentElement.lang = config.lang;\n }\n\n // Set custom meta tags (T063)\n if (config.customMeta && Array.isArray(config.customMeta)) {\n config.customMeta.forEach((tag) => {\n const selector = tag.id\n ? `meta[id=\"${tag.id}\"]`\n : tag.name\n ? `meta[name=\"${tag.name}\"]`\n : tag.property\n ? `meta[property=\"${tag.property}\"]`\n : `meta[http-equiv=\"${tag.httpEquiv}\"]`;\n\n const attributes: Record<string, string> = tag.name\n ? { name: tag.name }\n : tag.property\n ? { property: tag.property }\n : tag.httpEquiv\n ? { \"http-equiv\": tag.httpEquiv }\n : {};\n\n if (tag.id) attributes.id = tag.id;\n\n updateOrCreateMeta(selector, tag.content, attributes);\n });\n }\n};\n\n/**\n * Get current metadata configuration (T073)\n * Useful for SSR framework integration, testing, and debugging\n *\n * @example SSR with Next.js App Router\n * ```typescript\n * import { getMetadata } from '@gaddario98/react-pages';\n *\n * export async function generateMetadata() {\n * const metadata = getMetadata();\n * return {\n * title: metadata.title,\n * description: metadata.description,\n * openGraph: {\n * title: metadata.openGraph?.title,\n * description: metadata.openGraph?.description,\n * images: metadata.openGraph?.image ? [metadata.openGraph.image] : undefined,\n * url: metadata.openGraph?.url,\n * type: metadata.openGraph?.type as any,\n * locale: metadata.openGraph?.locale,\n * siteName: metadata.openGraph?.siteName,\n * },\n * };\n * }\n * ```\n *\n * @example SSR with Remix\n * ```typescript\n * import { getMetadata } from '@gaddario98/react-pages';\n *\n * export const meta: MetaFunction = () => {\n * const metadata = getMetadata();\n * return [\n * { title: metadata.title },\n * { name: 'description', content: metadata.description },\n * { property: 'og:title', content: metadata.openGraph?.title },\n * { property: 'og:description', content: metadata.openGraph?.description },\n * ];\n * };\n * ```\n */\nexport const getMetadata = (): MetadataConfig => {\n return { ...currentMetadata };\n};\n\n/**\n * Reset all metadata to defaults\n * Removes dynamically-added meta tags on web\n */\nexport const resetMetadata = (): void => {\n currentMetadata = {};\n\n if (!isWeb) {\n return;\n }\n\n // Remove dynamically-added meta tags (with data-react-pages attribute)\n const metaTags = document.querySelectorAll(\n 'meta[name], meta[property], meta[http-equiv], link[rel=\"canonical\"]'\n );\n metaTags.forEach((tag) => {\n // Only remove tags we created (not hardcoded in HTML)\n // We can't reliably detect this, so we'll just reset the stored config\n // and let setMetadata() handle re-applying defaults\n });\n\n // Reset document title to empty (or leave as-is)\n // document.title = '';\n};\n","import { AuthState } from \"@gaddario98/react-auth\";\nimport { ContentItem, PageProps, ViewSettings } from \"../types\";\nimport { FormManagerConfig, Submit } from \"@gaddario98/react-form\";\nimport { FieldValues } from \"react-hook-form\";\nimport { QueriesArray } from \"@gaddario98/react-queries\";\nimport { setMetadata, getMetadata, resetMetadata } from \"./metadata\";\nimport { MetadataConfig, LazyLoadingConfig } from \"./types\";\n\nexport interface DefaultContainerProps<\n F extends FieldValues = FieldValues,\n Q extends QueriesArray = QueriesArray,\n> {\n children?: React.JSX.Element[];\n allContents: (ContentItem<F, Q> | FormManagerConfig<F> | Submit<F>)[];\n handleRefresh?: () => Promise<void>;\n hasQueries: boolean;\n viewSettings?: ViewSettings;\n pageId?: string;\n}\n\nexport interface PageConfigProps {\n HeaderContainer: <\n F extends FieldValues = FieldValues,\n Q extends QueriesArray = QueriesArray,\n >(\n props: Omit<DefaultContainerProps<F, Q>, \"viewSettings\"> &\n ViewSettings[\"header\"]\n ) => React.ReactNode;\n FooterContainer: <\n F extends FieldValues = FieldValues,\n Q extends QueriesArray = QueriesArray,\n >(\n props: Omit<DefaultContainerProps<F, Q>, \"viewSettings\"> &\n ViewSettings[\"footer\"]\n ) => React.ReactNode;\n BodyContainer: <\n F extends FieldValues = FieldValues,\n Q extends QueriesArray = QueriesArray,\n >(\n props: DefaultContainerProps<F, Q>\n ) => React.ReactNode;\n authPageImage: string;\n authPageProps: PageProps;\n isLogged: (val: AuthState | null) => boolean;\n ItemsContainer: (props: { children: React.ReactNode }) => React.ReactNode;\n LoaderComponent?: (props: {\n loading?: boolean;\n message?: string;\n ns?: string;\n }) => React.ReactNode;\n PageContainer: (props: {\n children: React.ReactNode;\n id: string;\n }) => React.ReactNode;\n meta?: {\n title?: string;\n description?: string;\n };\n // NEW: Metadata configuration\n defaultMetadata: MetadataConfig;\n setMetadata: (config: MetadataConfig) => void;\n getMetadata: () => MetadataConfig;\n resetMetadata: () => void;\n // NEW: Lazy loading configuration\n lazyLoading: LazyLoadingConfig;\n}\n\nconst DefaultContainer = <\n F extends FieldValues = FieldValues,\n Q extends QueriesArray = QueriesArray,\n>({\n children,\n}: DefaultContainerProps<F, Q>) => {\n return children;\n};\n\n// Lazy initialization to avoid side effects at module load time\n// This ensures tree-shaking works correctly by deferring singleton creation\nlet _pageConfig: PageConfigProps | undefined;\n\n/**\n * Get or initialize the page configuration singleton\n * Uses lazy initialization to avoid module-level side effects for better tree-shaking\n */\nfunction initializePageConfig(): PageConfigProps {\n if (!_pageConfig) {\n _pageConfig = {\n HeaderContainer: DefaultContainer,\n FooterContainer: DefaultContainer,\n BodyContainer: DefaultContainer,\n authPageImage: \"\",\n authPageProps: { id: \"auth-page\" },\n isLogged: (val: AuthState | null) => !!val?.id && !!val?.isLogged,\n ItemsContainer: ({ children }) => children,\n PageContainer: ({ children }) => children,\n meta: {\n title: \"\",\n description: \"\",\n },\n // Metadata configuration\n defaultMetadata: {},\n setMetadata,\n getMetadata,\n resetMetadata,\n // Lazy loading configuration\n lazyLoading: {\n enabled: true,\n preloadOnHover: false,\n preloadOnFocus: false,\n timeout: 30000,\n logMetrics: process.env.NODE_ENV === 'development',\n },\n };\n }\n return _pageConfig;\n}\n\n// Getter for pageConfig - initializes on first access\nexport function getPageConfig(): PageConfigProps {\n return initializePageConfig();\n}\n\n// Legacy export for backward compatibility\nexport const pageConfig = new Proxy({} as PageConfigProps, {\n get: (target, prop) => {\n return initializePageConfig()[prop as keyof PageConfigProps];\n },\n set: (target, prop, value) => {\n initializePageConfig()[prop as keyof PageConfigProps] = value;\n return true;\n },\n});\n\nexport const setPageConfig = (config: Partial<PageConfigProps>) => {\n const current = initializePageConfig();\n Object.assign(current, config);\n};\n\n// Re-export metadata functions and types for convenience\nexport { setMetadata, getMetadata, resetMetadata } from \"./metadata\";\nexport type { MetadataConfig, MetaTag, MetadataProvider, LazyLoadingConfig } from \"./types\";\n","import equal from 'fast-deep-equal';\n\n/**\n * Optimized shallow equality check for objects and functions\n * @param objA - First object to compare\n * @param objB - Second object to compare\n * @returns True if objects are shallow equal\n */\nexport function shallowEqual(objA: any, objB: any): boolean {\n if (objA === objB) return true;\n \n if (!objA || !objB) return false;\n \n if (typeof objA !== 'object' || typeof objB !== 'object') {\n return objA === objB;\n }\n \n if (typeof objA === 'function' && typeof objB === 'function') {\n return objA.name === objB.name && objA.toString() === objB.toString();\n }\n \n const keysA = Object.keys(objA);\n const keysB = Object.keys(objB);\n \n if (keysA.length !== keysB.length) return false;\n \n for (const key of keysA) {\n if (!keysB.includes(key)) return false;\n \n const valA = objA[key];\n const valB = objB[key];\n \n if (typeof valA === 'function' && typeof valB === 'function') {\n if (valA.name !== valB.name || valA.toString() !== valB.toString()) {\n return false;\n }\n continue;\n }\n \n if (valA !== valB) return false;\n }\n \n return true;\n}\n\n/**\n * Checks if a value is stable for React dependency arrays\n * @param value - Value to check for stability\n * @returns True if value is considered stable\n */\nexport function isStableValue(value: any): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value !== 'object' && typeof value !== 'function') return true;\n if (typeof value === 'function') return value.toString().length < 1000;\n return false;\n}\n\n/**\n * Creates an optimized dependency array by filtering unstable values\n * @param deps - Array of dependencies to optimize\n * @returns Filtered array of stable dependencies\n */\nexport function optimizeDeps(deps: any[]): any[] {\n return deps.filter(dep => isStableValue(dep) || typeof dep === 'object');\n}\n\n/**\n * Custom prop comparator for React.memo() to prevent unnecessary re-renders\n * Compares props shallowly and ignores function references if they have the same name\n * @param prevProps - Previous component props\n * @param nextProps - Next component props\n * @returns True if props are equal (component should NOT re-render)\n */\nexport function memoPropsComparator<P extends Record<string, any>>(\n prevProps: Readonly<P>,\n nextProps: Readonly<P>\n): boolean {\n return shallowEqual(prevProps, nextProps);\n}\n\n/**\n * Deep equality check for complex objects\n * Use sparingly - prefer shallow equality for performance\n * Uses fast-deep-equal library for optimized deep comparison with circular reference protection\n * @param objA - First object\n * @param objB - Second object\n * @returns True if objects are deeply equal\n */\nexport function deepEqual(objA: any, objB: any): boolean {\n return equal(objA, objB);\n}\n\n/**\n * Memoization cache for expensive computations\n * Simple LRU cache with configurable size\n */\nexport class MemoizationCache<K, V> {\n private cache: Map<K, V>;\n private maxSize: number;\n\n constructor(maxSize: number = 100) {\n this.cache = new Map();\n this.maxSize = maxSize;\n }\n\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value !== undefined) {\n // Move to end (most recently used)\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n set(key: K, value: V): void {\n // Delete oldest entry if cache is full\n if (this.cache.size >= this.maxSize) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n }\n }\n this.cache.set(key, value);\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n clear(): void {\n this.cache.clear();\n }\n}\n\n/**\n * Creates a memoized function with custom cache key generator\n * @param fn - Function to memoize\n * @param cacheKeyFn - Optional function to generate cache key from arguments\n * @returns Memoized function\n */\nexport function memoize<Args extends any[], Result>(\n fn: (...args: Args) => Result,\n cacheKeyFn?: (...args: Args) => string\n): (...args: Args) => Result {\n const cache = new MemoizationCache<string, Result>();\n\n return (...args: Args): Result => {\n const cacheKey = cacheKeyFn\n ? cacheKeyFn(...args)\n : JSON.stringify(args);\n\n if (cache.has(cacheKey)) {\n return cache.get(cacheKey)!;\n }\n\n const result = fn(...args);\n cache.set(cacheKey, result);\n return result;\n };\n}","import {\n QueriesArray,\n } from \"@gaddario98/react-queries\";\n import { useMemo, memo } from \"react\";\n import { FieldValues } from \"react-hook-form\";\n import { ContentProps } from \"./types\";\n import { deepEqual } from \"../utils/optimization\";\n\n // Internal component implementation\n const RenderComponentImpl = <F extends FieldValues, Q extends QueriesArray>({\n content,\n formValues,\n allMutation,\n allQuery,\n setValue,\n }: ContentProps<F, Q>) => {\n const { component } = content;\n // Memo only on objects that don't change often, but dynamic data must propagate\n return useMemo(() => {\n if (typeof component === \"function\") {\n return component({\n allQuery,\n allMutation,\n formValues,\n setValue,\n });\n }\n return component;\n }, [allMutation, allQuery, component, formValues, setValue]);\n };\n\n // Export with React.memo and fast-deep-equal comparator for optimal performance\n export const RenderComponent = memo(\n RenderComponentImpl,\n (prevProps, nextProps) => {\n // Return true if props are equal (component should NOT re-render)\n return deepEqual(prevProps, nextProps);\n }\n ) as typeof RenderComponentImpl;","import { QueriesArray } from \"@gaddario98/react-queries\";\nimport { useMemo, memo } from \"react\";\nimport { FieldValues } from \"react-hook-form\";\nimport { useGenerateContentRender } from \"../hooks/useGenerateContentRender\";\nimport { pageConfig } from \"../config\";\nimport { ItemContainerProps } from \"./types\";\nimport { RenderComponent } from \"./RenderComponent\";\nimport { deepEqual } from \"../utils/optimization\";\n\nconst ContainerImpl = <F extends FieldValues, Q extends QueriesArray>({\n content,\n ns,\n pageId,\n allMutation,\n allQuery,\n formValues,\n setValue,\n }: ItemContainerProps<F, Q>) => {\n const { components } = useGenerateContentRender<F, Q>({\n allMutation,\n allQuery,\n formValues,\n pageId,\n isAllQueryMapped: true,\n formData: false,\n contents: content.items,\n ns,\n setValue,\n renderComponent: (props) => {\n if (props.content.type === \"container\") {\n return (\n <Container<F, Q>\n key={props.key}\n content={props.content}\n ns={props.ns}\n pageId={props.pageId}\n allMutation={props.allMutation}\n allQuery={props.allQuery}\n formValues={props.formValues}\n setValue={props.setValue}\n />\n );\n }\n return (\n <RenderComponent<F, Q>\n key={props.key}\n content={props.content}\n ns={props.ns}\n formValues={props.formValues}\n pageId={props.pageId}\n allMutation={props.allMutation}\n allQuery={props.allQuery}\n setValue={props.setValue}\n />\n );\n },\n });\n\n const Layout = useMemo(\n () => content?.component ?? pageConfig.ItemsContainer,\n [content?.component]\n );\n return <Layout>{components?.map((el) => el.element)}</Layout>;\n};\n\n// Export with React.memo and fast-deep-equal comparator for optimal performance\nexport const Container = memo(\n ContainerImpl,\n (prevProps, nextProps) => {\n // Return true if props are equal (component should NOT re-render)\n return deepEqual(prevProps, nextProps);\n }\n) as typeof ContainerImpl;\n","import { useMemo } from \"react\";\nimport { QueriesArray } from \"@gaddario98/react-queries\";\nimport { FieldValues } from \"react-hook-form\";\nimport { useGenerateContentRender } from \"./useGenerateContentRender\";\nimport { ContentItemsType } from \"../types\";\nimport { usePageConfig } from \"./usePageConfig\";\nimport { Container } from \"../components/Container\";\nimport { RenderComponent } from \"../components/RenderComponent\";\n\nexport interface GenerateContentProps<\n F extends FieldValues,\n Q extends QueriesArray,\n> {\n pageId: string;\n ns?: string;\n contents: ContentItemsType<F, Q>;\n pageConfig: ReturnType<typeof usePageConfig<F, Q>>;\n}\n\nexport const useGenerateContent = <\n F extends FieldValues,\n Q extends QueriesArray,\n>({\n pageId,\n ns = \"\",\n contents = [],\n pageConfig,\n}: GenerateContentProps<F, Q>) => {\n const {\n allMutation,\n allQuery,\n formData,\n formValues,\n isAllQueryMapped,\n setValue,\n } = pageConfig;\n const { allContents, components } = useGenerateContentRender<F, Q>({\n allMutation,\n allQuery,\n formData,\n formValues,\n pageId,\n contents,\n isAllQueryMapped,\n setValue,\n ns,\n renderComponent: (props) => {\n if (props.content.type === \"container\") {\n return (\n <Container<F, Q>\n key={props.key}\n content={props.content}\n ns={props.ns}\n pageId={props.pageId}\n allMutation={props.allMutation}\n allQuery={props.allQuery}\n formValues={props.formValues}\n setValue={props.setValue}\n />\n );\n }\n return (\n <RenderComponent<F, Q>\n key={props.key}\n content={props.content}\n ns={props.ns}\n formValues={props.formValues}\n pageId={props.pageId}\n allMutation={props.allMutation}\n allQuery={props.allQuery}\n setValue={props.setValue}\n />\n );\n },\n });\n const body = useMemo(\n () =>\n components\n .filter((el) => !el.renderInFooter && !el.renderInHeader)\n .map((item) => item.element),\n [components]\n );\n const header = useMemo(\n () =>\n components.filter((el) => el.renderInHeader).map((item) => item.element),\n [components]\n );\n const footer = useMemo(\n () =>\n components.filter((el) => el.renderInFooter).map((item) => item.element),\n [components]\n );\n return { header, body, footer, allContents };\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable react-hooks/exhaustive-deps */\n/**\n *