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
paramsP—Parameters for fetchOnMount execution
bodyB—Request 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 UI✓Manual implementation required
Share state across components✓Not supported
Persist data during view lifetime✓Not applicable
Manual retry/refresh operations✓Re-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.