Skip to main content

Stateful vs Stateless Hooks

Intrig generates two hook variants for each REST endpoint: stateful hooks that integrate with global NetworkState storage, and stateless hooks that return promises without persistent state. Selection between variants depends on state management requirements and component lifecycle patterns.


Stateful Hooks

Stateful hooks bind endpoints to Intrig's global state store, maintaining NetworkState throughout the component lifecycle and enabling state observation across multiple components.

Signature

function useOperation<P, B, T>(
options?: HookOptions<P, B>
): [
NetworkState<T>, // Current state
(body?: B, params?: P) => void, // Execute function
() => void // Clear function
]

State Lifecycle

NetworkState transitions through four states:

init → pending → success | error

State persists in the global store indexed by (sourceId, operationId, key) until explicitly cleared or component unmounts with clearOnUnmount enabled.

Hook Options

OptionTypeDefaultDescription
keystring'default'State isolation key for managing independent instances
clearOnUnmountbooleanfalseReset state to init on component unmount
fetchOnMountbooleanfalseExecute request on component mount
paramsPParameters for fetchOnMount execution
bodyBRequest body for fetchOnMount execution

Example Implementation

const [productState, fetchProduct, clearProduct] = useGetProduct({
key: `product:${productId}`,
fetchOnMount: true,
clearOnUnmount: true,
params: { id: productId }
});

if (isPending(productState)) return <Loading />;
if (isError(productState)) return <Error error={productState.error} />;
if (isSuccess(productState)) return <ProductView product={productState.data} />;
return null;

State Sharing

Multiple components using identical key values observe the same NetworkState:

// Component A - initiates request
const [product] = useGetProduct({
key: 'product:123',
fetchOnMount: true,
params: { id: '123' }
});

// Component B - observes same state
const [product] = useGetProduct({ key: 'product:123' });

No duplicate network requests occur. Both components receive state updates.

Use Cases

Stateful hooks are appropriate for:

Data Display: Components rendering API responses with loading and error states

State Sharing: Multiple components requiring access to identical data

Caching Behavior: Data that should persist across re-renders within view lifetime

Refresh Operations: Scenarios requiring manual refetch or clear operations


Stateless Hooks

Stateless hooks return promise-based functions without persistent state storage. State management responsibility transfers to the calling component.

Signature

function useOperationAsync<P, B, T>(
options?: HookOptions<P, B>
): [
(body?: B, params?: P) => Promise<T>, // Async function
() => void // Cancel function
]

Execution Model

Function invocation returns a promise resolving to the response or rejecting with an error. No state persists in Intrig's store.

Example Implementation

const [createProduct] = useCreateProductAsync();

const handleSubmit = async (formData: ProductFormData) => {
try {
const product = await createProduct(formData);
toast.success(`Created product ${product.id}`);
navigate(`/products/${product.id}`);
} catch (error) {
toast.error('Product creation failed');
}
};

React 18 Integration

Stateless hooks integrate with React 18 concurrent features:

const [createProduct] = useCreateProductAsync();
const [isPending, startTransition] = useTransition();

const handleSubmit = (formData: ProductFormData) => {
startTransition(async () => {
try {
const product = await createProduct(formData);
navigate(`/products/${product.id}`);
} catch (error) {
setError(error);
}
});
};

Use Cases

Stateless hooks are appropriate for:

Mutation Operations: Create, update, delete operations without persistent state requirements

Form Submissions: One-time operations triggered by user actions

Batch Operations: Multiple sequential requests managed as a transaction

Custom State Management: Scenarios requiring application-specific state handling


Selection Criteria

RequirementStatefulStateless
Display loading/error states in UIManual implementation required
Share state across componentsNot supported
Persist data during view lifetimeNot applicable
Manual retry/refresh operationsRe-invoke function
Form submission workflows
Sequential mutation operations
React 18 useTransition integration
Minimal state management overhead

NetworkState Integration

Stateful hooks expose NetworkState<T> requiring type guards for safe access:

import { isPending, isError, isSuccess } from '@intrig/react';

const [ordersState, fetchOrders] = useGetOrders({ fetchOnMount: true });

if (isPending(ordersState)) return <Spinner />;
if (isError(ordersState)) return <ErrorView error={ordersState.error} />;
if (isSuccess(ordersState)) return <OrdersTable orders={ordersState.data} />;
return null;

Stateless hooks bypass NetworkState, returning promises directly:

const [fetchOrders] = useGetOrdersAsync();
const [orders, setOrders] = useState<Order[]>([]);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
setLoading(true);
fetchOrders()
.then(setOrders)
.catch(setError)
.finally(() => setLoading(false));
}, []);

Lifecycle Patterns

Stateful with fetchOnMount

Automatic data loading on component mount:

const [userData] = useGetUser({
fetchOnMount: true,
clearOnUnmount: true,
params: { id: userId }
});

State automatically loads and clears with component lifecycle.

Stateless with useEffect

Manual lifecycle management:

const [fetchUser] = useGetUserAsync();

useEffect(() => {
const abortController = new AbortController();

fetchUser({ id: userId }, { signal: abortController.signal })
.then(handleSuccess)
.catch(handleError);

return () => abortController.abort();
}, [userId]);

Common Issues

Key Collision (Stateful)

Symptom: Unexpected data appears in components

Cause: Multiple components using identical keys unintentionally

Resolution: Use unique keys based on component-specific identifiers:

// Correct - unique per product
const [product] = useGetProduct({ key: `product:${productId}` });

// Incorrect - shared across all instances
const [product] = useGetProduct({ key: 'product' });

Missing Cleanup (Stateful)

Symptom: Stale data persists after component unmounts

Cause: State not cleared on unmount

Resolution: Enable clearOnUnmount or call clear in cleanup:

const [productState, fetchProduct, clearProduct] = useGetProduct();

useEffect(() => {
return () => clearProduct();
}, []);

Overusing Stateful for Mutations

Symptom: Unnecessary state management complexity

Cause: Using stateful hooks for one-time operations

Resolution: Use stateless hooks for mutations:

// Prefer stateless for mutations
const [deleteProduct] = useDeleteProductAsync();

await deleteProduct({ id: productId });

Interoperability

Both hook types can coexist for the same endpoint:

// Display with stateful
const [productsState, fetchProducts] = useGetProducts({
fetchOnMount: true
});

// Mutate with stateless
const [createProduct] = useCreateProductAsync();
const [deleteProduct] = useDeleteProductAsync();

const handleCreate = async (data: ProductData) => {
await createProduct(data);
fetchProducts(); // Refresh list after creation
};

This pattern separates concerns: stateful for display, stateless for actions.