stunk
Version:
Stunk is a lightweight, framework-agnostic state management library for JavaScript and TypeScript. It uses chunk-based state units for efficient updates, reactivity, and performance optimization in React, Vue(WIP), Svelte(Coming soon), and Vanilla JS/TS a
213 lines (206 loc) • 9.58 kB
TypeScript
import { C as Chunk, R as ReadOnlyChunk } from '../core-DAc7hZla.js';
import { P as PaginatedAsyncChunk, a as PaginationState, A as AsyncChunk, I as InfiniteAsyncChunk, M as Mutation, b as MutationResult } from '../mutation-DLvwAQ6L.js';
/**
* Subscribes to a chunk and returns its current value along with setters,
* reset, and destroy. Accepts both writable `Chunk<T>` and read-only
* `ReadOnlyChunk<T>` (e.g. derived chunks from `.derive()` or `select()`).
*
* For read-only chunks, the returned `set` and `reset` will be no-ops at
* runtime — prefer `useChunkValue` for derived/read-only chunks.
*
* @example
* const count = chunk(0);
* const [value, setValue] = useChunk(count);
*
* @example
* // Works with derived chunks too
* const doubled = count.derive(n => n * 2);
* const [value] = useChunk(doubled);
*/
declare function useChunk<T, S = T>(chunk: Chunk<T> | ReadOnlyChunk<T>, selector?: (value: T) => S): readonly [S, (valueOrUpdater: T | ((currentValue: T) => T)) => void, () => void, () => void];
/**
* Subscribes to a chunk and returns only its current value.
* Accepts both writable `Chunk<T>` and read-only `ReadOnlyChunk<T>`
* (e.g. derived chunks from `.derive()`, `select()`, or `computed()`).
*
* Prefer this over `useChunk` when you only need to read — it makes
* the read-only intent explicit and works correctly with derived chunks.
*
* @example
* const isAuthenticated = userChunk.derive(u => u !== null);
* const auth = useChunkValue(isAuthenticated); // ✅ no type error
*
* @example
* const total = computed(() => price.get() * qty.get());
* const value = useChunkValue(total); // ✅ no type error
*/
declare function useChunkValue<T, S = T>(chunk: Chunk<T> | ReadOnlyChunk<T>, selector?: (value: T) => S): S;
interface UseAsyncChunkResult<T, E extends Error, P extends Record<string, any>> {
data: T | null;
loading: boolean;
error: E | null;
lastFetched?: number;
isPlaceholderData: boolean;
reload: (params?: Partial<P>) => Promise<void>;
refresh: (params?: Partial<P>) => Promise<void>;
mutate: (mutator: (currentData: T | null) => T | null) => void;
reset: (refetch?: boolean) => void;
}
interface UseAsyncChunkResultWithParams<T, E extends Error, P extends Record<string, any>> extends UseAsyncChunkResult<T, E, P> {
setParams: (params: Partial<Record<keyof P, P[keyof P] | null>>) => void;
clearParams: () => void;
}
interface UseAsyncChunkResultWithPagination<T, E extends Error, P extends Record<string, any>> extends UseAsyncChunkResult<T, E, P> {
pagination?: PaginationState;
nextPage: () => Promise<void>;
prevPage: () => Promise<void>;
goToPage: (page: number) => Promise<void>;
resetPagination: () => Promise<void>;
}
interface UseAsyncChunkResultWithParamsAndPagination<T, E extends Error, P extends Record<string, any>> extends UseAsyncChunkResultWithParams<T, E, P>, Omit<UseAsyncChunkResultWithPagination<T, E, P>, keyof UseAsyncChunkResult<T, E, P>> {
}
interface UseAsyncChunkOptions<T = any, E extends Error = Error, P extends Record<string, any> = {}> {
/**
* Parameters to pass to the fetcher. When these change between renders,
* the chunk automatically re-fetches with the new values.
*/
params?: Partial<P>;
/**
* Force a fetch on mount even when the chunk has no params.
* Ignored if params is provided.
* (default: false)
*/
fetchOnMount?: boolean;
/**
* Whether the chunk is enabled.
* If false, the chunk will not fetch data.
* (default: true)
*/
enabled?: boolean;
/**
* Called after every successful fetch at the hook level.
* Has full access to React context — safe to call navigate(), setState(), etc.
*/
onSuccess?: (data: T) => void;
/**
* Called when a fetch fails at the hook level.
* Has full access to React context — safe to call navigate(), setState(), etc.
*/
onError?: (error: E) => void;
}
declare function useAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(asyncChunk: PaginatedAsyncChunk<T, E> & {
setParams: (params: Partial<P>) => void;
}, options?: UseAsyncChunkOptions<T, E, P>): UseAsyncChunkResultWithParamsAndPagination<T, E, P>;
declare function useAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(asyncChunk: PaginatedAsyncChunk<T, E>, options?: UseAsyncChunkOptions<T, E, P>): UseAsyncChunkResultWithPagination<T, E, P>;
declare function useAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(asyncChunk: AsyncChunk<T, E> & {
setParams: (params: Partial<P>) => void;
}, options?: UseAsyncChunkOptions<T, E, P>): UseAsyncChunkResultWithParams<T, E, P>;
declare function useAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(asyncChunk: AsyncChunk<T, E>, options?: UseAsyncChunkOptions<T, E, P>): UseAsyncChunkResult<T, E, P>;
interface UseInfiniteAsyncChunkOptions<P extends Record<string, any>> extends Omit<UseAsyncChunkOptions<P>, 'initialParams' | 'params'> {
/**
* @deprecated Use `params` instead. Will be removed in v3 stable.
*/
initialParams?: Omit<Partial<P>, 'page' | 'pageSize'>;
/** Reactive parameters — page and pageSize are managed automatically. Re-fetches from page 1 when these change. */
params?: Omit<Partial<P>, 'page' | 'pageSize'>;
/** Automatically load next page when sentinel enters viewport (default: true) */
autoLoad?: boolean;
/** IntersectionObserver threshold — 0.0 to 1.0 (default: 1.0) */
threshold?: number;
}
interface UseInfiniteAsyncChunkResult<T, E extends Error, P extends Record<string, any>> {
data: T[] | null;
loading: boolean;
error: E | null;
lastFetched?: number;
isPlaceholderData: boolean;
/** True when fetching a new page while existing data is already loaded */
isFetchingMore: boolean;
/** True if more pages are available */
hasMore: boolean;
reload: (params?: Partial<P>) => Promise<void>;
refresh: (params?: Partial<P>) => Promise<void>;
mutate: (mutator: (currentData: T[] | null) => T[]) => void;
reset: () => void;
nextPage: () => Promise<void>;
prevPage: () => Promise<void>;
goToPage: (page: number) => Promise<void>;
resetPagination: () => Promise<void>;
/** Manually trigger loading the next page */
loadMore: () => void;
/** Attach this ref to a sentinel element at the bottom of your list */
observerTarget: React.RefObject<HTMLElement>;
}
/**
* Subscribes to an infinite async chunk and wires up automatic infinite scroll.
*
* Attach `observerTarget` to a sentinel element at the bottom of your list —
* the next page loads automatically when it enters the viewport.
* Use `loadMore()` for manual triggering.
*
* @param chunk - An `InfiniteAsyncChunk` instance.
* @param options.autoLoad - Auto-load on scroll (default: true).
* @param options.threshold - IntersectionObserver threshold 0.0–1.0 (default: 1.0).
* @param options.params - Reactive params excluding `page` and `pageSize`. Resets to page 1 and refetches when changed.
*
* @example
* const { data, loading, hasMore, observerTarget, loadMore } = useInfiniteAsyncChunk(postsChunk);
*
* @example
* // Reactive search — resets to page 1 automatically when searchTerm changes
* const { data } = useInfiniteAsyncChunk(companiesChunk, {
* params: { search: searchTerm },
* });
*
* return (
* <>
* {data?.map(post => <Post key={post.id} {...post} />)}
* <div ref={observerTarget} />
* </>
* );
*/
declare function useInfiniteAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(chunk: InfiniteAsyncChunk<T, E, P>, options?: UseInfiniteAsyncChunkOptions<P>): UseInfiniteAsyncChunkResult<T, E, P>;
interface UseMutationResult<TData, TError extends Error = Error, TVariables = void> {
/** Current mutation state */
loading: boolean;
data: TData | null;
error: TError | null;
isSuccess: boolean;
/**
* Execute the mutation. Always resolves — never throws.
* Safe to fire and forget, or await for local UI control.
*
* @example
* // Fire and forget
* mutate({ title: 'Hello' });
*
* // Await for local control — no try/catch needed
* const { data, error } = await mutate({ title: 'Hello' });
* if (!error) router.push('/posts');
*/
mutate: TVariables extends void ? () => Promise<MutationResult<TData, TError>> : (variables: TVariables) => Promise<MutationResult<TData, TError>>;
/** Reset mutation state back to initial */
reset: () => void;
}
/**
* Subscribes to a mutation instance and returns its reactive state with `mutate` and `reset`.
*
* @param mutation - A `mutation()` instance from `stunk/query`.
*
* @example
* const createPost = mutation(
* async (data: NewPost) => fetchAPI('/posts', { method: 'POST', body: data }),
* { invalidates: [postsChunk] }
* );
*
* function CreatePostForm() {
* const { mutate, loading, error, isSuccess } = useMutation(createPost);
*
* const handleSubmit = async (data: NewPost) => {
* const { error } = await mutate(data);
* if (!error) router.push('/posts');
* };
* }
*/
declare function useMutation<TData, TError extends Error = Error, TVariables = void>(mutation: Mutation<TData, TError, TVariables>): UseMutationResult<TData, TError, TVariables>;
export { type UseMutationResult, useAsyncChunk, useChunk, useChunkValue, useInfiniteAsyncChunk, useMutation };