Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Created October 13, 2025 23:00
Show Gist options
  • Select an option

  • Save WomB0ComB0/54694ad9f78cafdfd7afa4ea69fe8c0e to your computer and use it in GitHub Desktop.

Select an option

Save WomB0ComB0/54694ad9f78cafdfd7afa4ea69fe8c0e to your computer and use it in GitHub Desktop.
effect-schema-data-loader - Enhanced with AI-generated documentation
("use client");
import { FetchHttpClient } from "@effect/platform";
import type { QueryKey } from "@tanstack/react-query";
import {
useQueryClient,
useSuspenseQuery,
type UseSuspenseQueryOptions,
} from "@tanstack/react-query";
import { Effect, pipe, Schema } from "effect";
import React, { Suspense, useCallback, useMemo } from "react";
import { ClientError } from "./client-error.tsx";
import {
fetcher,
FetcherError,
type FetcherOptions,
get,
type QueryParams,
ValidationError,
} from "./effect-schema-fetcher.ts";
import { Loader } from "./loader.tsx";
import { parseCodePath } from "./util/utils.ts";
/**
* @module effect-data-loader
*
* Enhanced DataLoader for React using React Query, Effect, and Effect Schema validation.
*
* This module provides a generic, type-safe React component and hook for loading data asynchronously
* with advanced error handling, runtime validation, caching, and developer experience. It is designed
* to work with Effect, Effect Schema, and React Query, supporting features like retries, timeouts,
* schema validation, optimistic updates, and more.
*
* ## Features
* - Type-safe data loading for React components with Effect Schema validation
* - Suspense support
* - Runtime type validation with detailed error messages
* - Customizable error and loading components
* - Query caching and invalidation
* - Optimistic updates
* - Retry and timeout logic
* - Render props and hook API
*
* @see DataLoader
* @see useDataLoader
*
* @example
* ```tsx
* import { DataLoader } from './effect-data-loader';
* import { Schema } from 'effect';
*
* const UserSchema = Schema.Struct({
* id: Schema.Number,
* name: Schema.String,
* email: Schema.String
* });
*
* function UserList() {
* return (
* <DataLoader url="/api/users" schema={UserSchema}>
* {(users) => (
* <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
* )}
* </DataLoader>
* );
* }
* ```
*/
/**
* Enhanced render props with additional query state and actions.
*
* @template T The type of the loaded data.
*/
export interface DataLoaderRenderProps<T> {
/** Manually trigger a refetch */
refetch: () => Promise<void>;
/** Whether the query is currently refetching */
isRefetching: boolean;
/** Query client for advanced operations */
queryClient: ReturnType<typeof useQueryClient>;
/** Invalidate this query's cache */
invalidate: () => Promise<void>;
/** Set query data optimistically */
setQueryData: (data: T | ((prev: T) => T)) => void;
}
/**
* Base props for DataLoader without schema.
*/
interface BaseDataLoaderProps<T> {
/** URL to fetch data from */
url: string;
/** Additional React Query options */
queryOptions?: Partial<UseSuspenseQueryOptions<T, Error, T, QueryKey>>;
/** Custom loading component */
LoadingComponent?: React.ReactNode;
/** Custom error component */
ErrorComponent?:
| React.ComponentType<{ error: Error; retry: () => void }>
| React.ReactElement;
/** Fetcher options (retries, timeout, etc.) */
options?: FetcherOptions<T>;
/** Query parameters */
params?: QueryParams;
/** Custom query key override */
queryKey?: QueryKey;
/** Callback fired when data is successfully loaded */
onSuccess?: (data: T) => void;
/** Callback fired when an error occurs */
onError?: (error: Error) => void;
/** Transform the data before passing to children */
transform?: (data: any) => T;
/** Stale time in milliseconds (default: 5 minutes) */
staleTime?: number;
/** Refetch interval in milliseconds (default: 5 minutes) */
refetchInterval?: number;
/** Whether to refetch on window focus (default: false) */
refetchOnWindowFocus?: boolean;
/** Whether to refetch on reconnect (default: true) */
refetchOnReconnect?: boolean;
/** Additional props */
[key: string]: unknown;
}
/**
* Props for DataLoader without schema (manual typing).
*/
export interface DataLoaderProps<T> extends BaseDataLoaderProps<T> {
/**
* Render prop that receives data and optional utilities.
*/
children:
| ((data: T) => React.ReactNode)
| ((data: T, utils: DataLoaderRenderProps<T>) => React.ReactNode);
/** Effect Schema for runtime validation (optional) */
schema?: never;
}
/**
* Props for DataLoader with Effect Schema (automatic type inference).
*/
export interface DataLoaderPropsWithSchema<
S extends Schema.Schema<any, any, never>,
> extends BaseDataLoaderProps<Schema.Schema.Type<S>> {
/**
* Render prop that receives validated data and optional utilities.
*/
children:
| ((data: Schema.Schema.Type<S>) => React.ReactNode)
| ((
data: Schema.Schema.Type<S>,
utils: DataLoaderRenderProps<Schema.Schema.Type<S>>,
) => React.ReactNode);
/** Effect Schema for runtime validation */
schema: S;
/** Fetcher options with schema */
options?: FetcherOptions<Schema.Schema.Type<S>> & { schema: S };
}
/**
* Enhanced DataLoader component with Effect Schema validation, better error handling, caching, and developer experience.
*
* @example Without schema
* ```tsx
* <DataLoader<User[]> url="/api/users">
* {(users) => <UserList users={users} />}
* </DataLoader>
* ```
*
* @example With schema (automatic type inference)
* ```tsx
* const UsersSchema = Schema.Array(Schema.Struct({
* id: Schema.Number,
* name: Schema.String,
* email: Schema.String
* }));
*
* <DataLoader url="/api/users" schema={UsersSchema}>
* {(users) => <UserList users={users} />} // users is fully typed!
* </DataLoader>
* ```
*/
export function DataLoader<T = unknown>(
props: DataLoaderProps<T>,
): React.ReactElement;
export function DataLoader<S extends Schema.Schema<any, any, never>>(
props: DataLoaderPropsWithSchema<S>,
): React.ReactElement;
export function DataLoader<
T = unknown,
S extends Schema.Schema<any, any, never> = any,
>({
children,
url,
queryOptions = {},
LoadingComponent = <Loader />,
ErrorComponent = ClientError,
options = {},
params = {},
queryKey,
onSuccess,
onError,
transform,
staleTime = 1_000 * 60 * 5, // 5 minutes
refetchInterval = 1_000 * 60 * 5, // 5 minutes
refetchOnWindowFocus = false,
refetchOnReconnect = true,
schema,
}: (DataLoaderProps<T> | DataLoaderPropsWithSchema<S>) & {
schema?: S;
}): React.ReactElement {
const queryClient = useQueryClient();
// Generate stable query key including schema
const finalQueryKey = useMemo(() => {
if (queryKey) return queryKey;
const headers = options?.headers;
const timeout = options?.timeout;
const schemaKey = schema ? `schema:${Schema.format(schema)}` : null;
const keyArray = [
"dataloader",
url,
params,
headers,
timeout,
schemaKey,
].filter(Boolean);
return keyArray;
}, [queryKey, url, params, options, schema]);
// Enhanced fetcher options with better defaults and schema support
const fetcherOptions = useMemo((): FetcherOptions<any> => {
const baseOptions: FetcherOptions<any> = {
retries: 3,
retryDelay: 1_000,
timeout: 30_000,
onError: (err) => {
const path = parseCodePath(url, fetcher);
console.error(`[DataLoader]: ${path}`);
if (err instanceof FetcherError) {
console.error(`[DataLoader]: Status ${err.status}`, err.responseData);
} else if (err instanceof ValidationError) {
console.error(
`[DataLoader]: Validation failed - ${err.getProblemsString()}`,
);
console.error(`[DataLoader]: Invalid data:`, err.responseData);
} else {
console.error("[DataLoader]: Unexpected error", err);
}
// Call user-provided error handler
if (onError && err instanceof Error) onError(err);
},
...(options || {}),
};
if (schema) return { ...baseOptions, schema };
return baseOptions;
}, [url, options, onError, schema]);
// Memoized query function with schema support
const queryFn = useCallback(async () => {
try {
const effect = pipe(
get(url, fetcherOptions, params),
Effect.provide(FetchHttpClient.layer),
);
const result = await Effect.runPromise(effect);
// Apply transformation if provided (note: schema validation happens first)
const finalResult =
transform && typeof transform === "function"
? transform(result)
: result;
// Call success callback
if (onSuccess) onSuccess(finalResult as any);
return finalResult;
} catch (error) {
// Enhanced error handling for validation errors
if (error instanceof ValidationError) throw error;
if (error instanceof FetcherError) throw error;
// Wrap unexpected errors
throw new FetcherError(
error instanceof Error ? error.message : "Unknown error occurred",
url,
undefined,
error,
);
}
}, [url, fetcherOptions, params, transform, onSuccess]);
// Enhanced query options
const queryOptionsWithDefaults = useMemo(() => {
const baseOptions: UseSuspenseQueryOptions<any, Error, any, QueryKey> = {
queryKey: finalQueryKey as QueryKey,
queryFn,
staleTime,
refetchInterval,
refetchOnWindowFocus,
refetchOnReconnect,
retry: (failureCount: number, error: unknown) => {
// Don't retry client errors (4xx)
if (
error instanceof ValidationError ||
(error instanceof FetcherError &&
error.status &&
error.status >= 400 &&
error.status < 500)
)
return false;
return failureCount < 3;
},
retryDelay: (attemptIndex: number) =>
Math.min(1_000 * 2 ** attemptIndex, 30_000),
...queryOptions,
};
return baseOptions;
}, [
finalQueryKey,
queryFn,
staleTime,
refetchInterval,
refetchOnWindowFocus,
refetchOnReconnect,
queryOptions,
]);
const { data, error, refetch, isRefetching } = useSuspenseQuery(
queryOptionsWithDefaults,
);
// Enhanced render props
const renderProps = useMemo(() => {
const props: DataLoaderRenderProps<any> = {
refetch: async () => {
await refetch();
},
isRefetching,
queryClient,
invalidate: async () => {
await queryClient.invalidateQueries({ queryKey: finalQueryKey });
},
setQueryData: (newData: any) => {
queryClient.setQueryData(finalQueryKey, newData);
},
};
return props;
}, [refetch, isRefetching, queryClient, finalQueryKey]);
// Enhanced error component with retry capability and validation error support
const renderError = useCallback(
(error: Error) => {
if (React.isValidElement(ErrorComponent)) {
return React.cloneElement(ErrorComponent as React.ReactElement<any>, {
error,
retry: () => refetch(),
});
}
const Component = ErrorComponent as React.ComponentType<{
error: Error;
retry: () => void;
}>;
return <Component error={error} retry={() => refetch()} />;
},
[ErrorComponent, refetch],
);
// Determine how to call children function
const renderChildren = useCallback(() => {
if (typeof children === "function") {
// Check if it's a function that accepts 2 parameters (data + utils)
try {
const result =
children.length > 1
? (children as any)(data, renderProps)
: (children as any)(data);
return result;
} catch {
// Fallback to simple call if inspection fails
return (children as any)(data);
}
}
return null;
}, [children, data, renderProps]);
return (
<Suspense fallback={LoadingComponent}>
{error ? renderError(error) : renderChildren()}
</Suspense>
);
}
DataLoader.displayName = "DataLoader";
/**
* Hook version of DataLoader for use outside of JSX.
*/
export function useDataLoader<T = unknown>(
url: string,
options?: Omit<
DataLoaderProps<T>,
"children" | "LoadingComponent" | "ErrorComponent"
>,
): ReturnType<typeof useSuspenseQuery<T, Error, T, QueryKey>>;
export function useDataLoader<S extends Schema.Schema<any, any, never>>(
url: string,
options: Omit<
DataLoaderPropsWithSchema<S>,
"children" | "LoadingComponent" | "ErrorComponent"
>,
): ReturnType<
typeof useSuspenseQuery<
Schema.Schema.Type<S>,
Error,
Schema.Schema.Type<S>,
QueryKey
>
>;
export function useDataLoader(url: string, options: any = {}) {
const {
queryOptions = {},
options: fetcherOptions = {},
params = {},
queryKey,
onSuccess,
onError,
transform,
staleTime = 1_000 * 60 * 5,
refetchInterval = 1_000 * 60 * 5,
refetchOnWindowFocus = false,
refetchOnReconnect = true,
schema,
} = options;
const finalQueryKey = useMemo(() => {
if (queryKey) return queryKey;
const headers = (fetcherOptions as any)?.headers;
const timeout = (fetcherOptions as any)?.timeout;
const schemaKey = schema ? `schema:${Schema.format(schema)}` : null;
const keyArray = [
"dataloader",
url,
params,
headers,
timeout,
schemaKey,
].filter(Boolean);
return keyArray;
}, [queryKey, url, params, fetcherOptions, schema]);
const enhancedFetcherOptions = useMemo(() => {
const baseOptions: FetcherOptions<any> = {
retries: 3,
retryDelay: 1_000,
timeout: 30_000,
onError: (err) => {
if (err instanceof ValidationError) {
console.error(
`[useDataLoader]: Validation failed - ${err.getProblemsString()}`,
);
}
if (typeof onError === "function" && err instanceof Error) {
onError(err);
}
},
...(fetcherOptions || {}),
};
if (schema) {
return { ...baseOptions, schema };
}
return baseOptions;
}, [fetcherOptions, onError, schema]);
const queryFn = useCallback(async () => {
const effect = pipe(
get(url, enhancedFetcherOptions, params as QueryParams),
Effect.provide(FetchHttpClient.layer),
);
const result = await Effect.runPromise(effect);
const finalResult =
transform && typeof transform === "function" ? transform(result) : result;
if (typeof onSuccess === "function") {
onSuccess(finalResult);
}
return finalResult;
}, [url, enhancedFetcherOptions, params, transform, onSuccess]);
const queryOptionsWithDefaults = useMemo(() => {
const baseOptions: UseSuspenseQueryOptions<any, Error, any, QueryKey> = {
queryKey: finalQueryKey as QueryKey,
queryFn,
staleTime,
refetchInterval,
refetchOnWindowFocus,
refetchOnReconnect,
retry: (failureCount: number, error: unknown) => {
if (
error instanceof ValidationError ||
(error instanceof FetcherError &&
error.status &&
error.status >= 400 &&
error.status < 500)
) {
return false;
}
return failureCount < 3;
},
retryDelay: (attemptIndex: number) =>
Math.min(1_000 * 2 ** attemptIndex, 30_000),
...queryOptions,
};
return baseOptions;
}, [
finalQueryKey,
queryFn,
staleTime,
refetchInterval,
refetchOnWindowFocus,
refetchOnReconnect,
queryOptions,
]);
return useSuspenseQuery(queryOptionsWithDefaults);
}
export default DataLoader;

effect-schema-data-loader.tsx

File Type: TSX
Lines: 546
Size: 14.3 KB
Generated: 10/13/2025, 7:00:36 PM


Code Analysis: effect-schema-data-loader.tsx

This file defines a React component called DataLoader and a corresponding hook useDataLoader for fetching and validating data using React Query, Effect, and Effect Schema. It aims to simplify data fetching in React applications by providing a type-safe, declarative, and robust solution with built-in error handling, caching, and validation.

Key Components and Functionality:

  1. DataLoader Component:

    • A React component that fetches data from a specified URL and renders its children with the fetched data.
    • Supports both explicitly typed data and data validated by Effect Schema.
    • Provides customizable loading and error components.
    • Offers render props with data and utility functions for manual refetching, cache invalidation, and optimistic updates.
    • Accepts various React Query options for fine-grained control over the data fetching process.
  2. useDataLoader Hook:

    • A React hook that encapsulates the data fetching logic, providing similar functionality to the DataLoader component but in a hook-based API.
    • Offers the same features as the DataLoader component, including schema validation, error handling, and React Query integration.
  3. Type Safety and Validation:

    • Leverages Effect Schema for runtime type validation, ensuring that the fetched data conforms to the expected structure.
    • Provides detailed error messages when validation fails, aiding in debugging and troubleshooting.
    • Offers automatic type inference when a schema is provided, enhancing developer experience.
  4. Error Handling:

    • Includes a default error component (ClientError) that displays error messages and provides a retry button.
    • Allows customization of the error component to match the application's design.
    • Provides detailed error logging, including status codes, response data, and validation errors.
  5. Caching and State Management:

    • Utilizes React Query for caching and state management, improving performance and reducing unnecessary network requests.
    • Supports various caching strategies, including stale-while-revalidate and background refetching.
    • Provides utilities for manual cache invalidation and optimistic updates.
  6. Fetcher Options:

    • Offers a set of fetcher options for configuring the data fetching process, including retries, timeouts, and headers.
    • Integrates with @effect/platform for Effect-based HTTP client functionality.

Architecture and Dependencies:

  • React Query: Used for data fetching, caching, and state management.
  • Effect: Used for managing side effects and providing a functional programming approach.
  • Effect Schema: Used for runtime type validation and schema definition.
  • @effect/platform: Used for Effect-based HTTP client functionality.
  • ./client-error.tsx: Defines the default error component.
  • ./effect-schema-fetcher.ts: Contains the fetcher implementation with Effect Schema support.
  • ./loader.tsx: Defines the default loading component.
  • ./util/utils.ts: Provides utility functions, including parseCodePath for error logging.

Usage:

The DataLoader component and useDataLoader hook can be used to fetch and display data in React components. Here's a basic example:

import { DataLoader } from './effect-data-loader';
import { Schema } from 'effect';

const UserSchema = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
});

function UserList() {
  return (
    <DataLoader url="/api/users" schema={UserSchema}>
      {(users) => (
        <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
      )}
    </DataLoader>
  );
}

In this example, the DataLoader component fetches data from /api/users, validates it against the UserSchema, and renders a list of users.

Potential Improvements:

  • Customizable Fetcher: Allow users to provide their own fetcher implementation for greater flexibility.
  • SSR Support: Add support for server-side rendering to improve initial load times.
  • More Comprehensive Error Handling: Provide more detailed error information and allow users to customize the error handling logic.
  • Simplified API: Consider simplifying the API to make it easier to use for common use cases.

Summary:

The effect-schema-data-loader.tsx file provides a powerful and flexible solution for data fetching in React applications. By combining React Query, Effect, and Effect Schema, it offers type safety, robust error handling, and efficient caching, making it a valuable tool for building modern web applications.


Description generated using AI analysis

@WomB0ComB0
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment