Skip to main content

createSource

createSource is a function that creates shared states. It accepts three parameters:

PropertyTypeDescription
keystringThe unique identifier of the state
producerProducer<T, A, E>Returns the state value of type T
configurationProducerConfig<T, A, E>The configuration of the state

Signature

createSource is defined and used as follows:

export function createSource<T, A extends unknown[] = [], E = Error>(
key: string,
producer?: Producer<T, A, E> | undefined | null,
config?: ProducerConfig<T, A, E>
): Source<T, A, E>;


let counter = createSource("counter", null, { initialValue: 0 });
let userDetails = createSource("user-details", fetchUserDetailsProducer, {
runEffect: "debounce",
runEffectDurationMs: 300,
skipPendingDelayMs: 200,
// ... other config we'll see in a few
});

key

The key is a plain string and unique identifier of the state.

Giving the same key to multiple times to createSource will return the same source object.

producer

The producer was detailed in the previous section.

Configuration

The whole configuration is optional.

initialValue

// T = TData, A = TArgs, E = TError
type typeOfInitialValue = T | ((cache: Record<string, CachedState<T, A, E>> | null) => T)

The initial value held by the state when status is initial.

It can be also a function that allows you to initialize the state from the cache. More on cache later.

runEffect

type RunEffect = "debounce" | "throttle";

The effect to apply when running the producer. It is either debounce or throttle.

note

The run effect isn't applied if runEffectDurationMs isn't given or is 0.

runEffectDurationMs

type runEffectDurationMs = number;

The runEffect duration in milliseconds.

skipPendingDelayMs

type skipPendingDelayMs = number;

The delay in ms under which the transition to pending state is skipped. This comes in handy when you the request may be very fast and you don't want to show a pending indicator if so.

keepPendingForMs

type skipPendingDelayMs = number;

This is the reserve of the previous property, if you enter the pending state, it prevents any further updates until this delay is passed, to avoid showing the pending indicator for few milliseconds for example.

It reads as: If you enter the pending state, stay in it at least for this value.

skipPendingStatus

type skipPendingStatus = boolean;

This will prevent your state to have a pending state at all.

cacheConfig

type CacheConfig<T, A extends unknown[], E> = {
enabled: boolean;
timeout?: ((currentState: State<T, A, E>) => number) | number;
hash?(
args: A | undefined,
payload: Record<string, unknown> | null | undefined
): string;
auto?: boolean;
persist?(cache: Record<string, CachedState<T, A, E>>): void;
load?():
| Record<string, CachedState<T, A, E>>
| Promise<Record<string, CachedState<T, A, E>>>;
onCacheLoad?({ cache, setState }: OnCacheLoadProps<T, A, E>): void;
}

The library supports caching state values, but it is opt-in and not enabled by default.

enabled

Will enable cache for this state.

timeout

The duration under which the cached state is considered still valid.

If this value is omitted, first, the library will check if you have a cache-control header with a max-age defined. If present it will be used. Or else, Infinity is used.

auto

Indicates that we should automatically re-run the producer to get a new value after timeout is elapsed.

note
  • auto doesn't work with Initity.
  • auto will remove the cached state from cache.
  • auto will only run again if the removed cached state is the current state.

hash

Each cached state is identified by a string hash that's computed by this function. If omitted, it is calculated automatically like this:

export function defaultHash<A extends unknown[]>(
args: A | undefined,
payload: Record<string, unknown> | null | undefined
): string {
return JSON.stringify({ args, payload });
}

persist

Called everytime a new cache entry is added or removed. Its purpose is to allow you to persist the cache then load it later. In local storage for example.

load

Loads the cache when the state is constructed

onCacheLoad

A callback fired when the cache is loaded.

retryConfig

When running the producer and it fails, you can retry it.

type RetryConfig<T, A extends unknown[], E> = {
enabled: boolean;
maxAttempts?: number;
backoff?: number | ((attemptIndex: number, error: E) => number);
retry?: boolean | ((attemptIndex: number, error: E) => boolean);
};

enabled

Opt into retry, this is not enabled by default.

maxAttempts

Defines the max retries to perform per run.

backoff

The backoff between retries.

retry

A boolean or a function that receives the current attempt count and the error and returns whether we should retry or not.

resetStateOnDispose

type resetStateOnDispose = boolean;

The dispose event is when all subscribers unsubscribe from a state.

If this property is true, the state will be altered to its initial value.

context

This is a plain object, it should be a valid WeakMap key.

To perform isolation and allowing to have multiple states with the same key, in the server for example, the context api comes in.

When provided, the state will be created and only visible to that context.

storeInContext

If this is provided and is false, the state instance won't be stored in its context.

hideFromDevtools

Defines whether to show this state in the devtools or not.

The Source

The resulting object from createSource has the following shape:

key

The used key to create the state.

uniqueId

Each state has a unique id defining it. This is an auto incremented number.

getState

returns the current state.

setState

Will alter the state to the desired value with the given status. The updater can be either a value or a function that will receive the current state.

setState(
updater: StateFunctionUpdater<T, A, E> | T,
status?: Status,
callbacks?: ProducerCallbacks<T, A, E>
): void;
note

When you provide a function updater to setState, it is given the current state.

danger

Although setState gives you the previous state object as a whole, it expects you to return only the value.

The second parameter allows you to pass the status if needed.

setState is used internally by the library and from the devtools to allow you to go to any desired state. It is kept for backward compatibility and historical reasons.

tip

If you only need the previous successful data and you will be setting ti to a success state, use useData and not useState.

The implication on the difference between setState and setData are:

  • You need to check on the state status in setState
  • You need to take the data property.
let source = createSource("count", null, { initialValue: 0 }):

source.setState(prevState => (prevState.data ?? 0) + 1});

setData

setData will change the state to a success state with the desired value.

setState(
updater: T | ((prevData: T | null) => T);
): void;
note

When you provide a function updater to setData, it is given the latest succeeded data, the initial data if status is initial and this value is provided, or else it is given null.

let source = createSource("count", null, { initialValue: 0 }):

source.setData(prev => prev! + 1);

getVersion

The library implements an optimistic lock internally via a value that is auto-incremented each time the state changes.

getVersion(): number;

run

Allows you to run the producer with the given args.

It returns a function that will abort the related run.

run(...args: TArgs): AbortFn;

runc

runc(
props: {
args?: TArgs,
onSuccess?(successState: SuccessState<TData, TArgs>): void;
onError?(errorState: ErrorState<TData, TArgs, TError>): void;
}
): AbortFn;

Will run the producer with the given args and executed the given callbacks.

It returns a function that will abort the related run.

runp

runp(...args: A): Promise<State<TData, TArgs, TError>>;

Similar to run, but returns a Promise to resolve.

This promise resolves even if the producer throws, and gives you a state with error status in this case.

replay

replay(): AbortFn;

Will run again using the latest args and payload.

abort

abort(reason?: any): void;

Will call any registered abort callbacks from the latest run.

If a run is pending, it will be aborted and the previous state is restored.

replaceProducer

replaceProducer(newProducer: Producer<T, A, E> | null): void;

Allows you to replace the producer of a state.

getConfig

getConfig(): ProducerConfig<T, A, E>;

Returns the current config held by the state instance.

patchConfig

patchConfig(partialConfig?: Partial<ProducerConfig<T, A, E>>): void;

Allows you to partially add config to the defined state.

getPayload

The payload is a mutable area inside the state that's accessible anytime, anywhere and by all subscribers.

getPayload(): Record<string, unknown>;

Returns the payload object. If not defined, it will be initialized by an empty object then returned.

mergePayload

mergePayload(partialPayload?: Record<string, unknown>): void;

Adds the given payload to the existing payload inside the instance.

subscribe

subscribe(cb: (s: State<T, A, E>) => void): UnsubscribeFn;

Allows you to subscribe to state updates in this state.

note

If you are using hooks, you won't need this.

invalidateCache

invalidateCache(cacheKey?: string): void;

Will invalidate an entry from the cache by its key.

It the cache key is omitted, the whole cache is removed.

replaceCache

replaceCache(cacheKey: string, cache: CachedState<T, A, E>): void;

type CachedState<T, A extends unknown[], E> = {
state: State<T, A, E>;
addedAt: number;
deadline: number;
// when auto refresh is enabled, we store its timeoutid in this
id?: ReturnType<typeof setTimeout>;
};

Replaces a single cache entry.

on

on(
eventType: InstanceChangeEvent,
eventHandler: InstanceChangeEventHandlerType<T, A, E>
): () => void;
on(
eventType: InstanceDisposeEvent,
eventHandler: InstanceDisposeEventHandlerType<T, A, E>
): () => void;
on(
eventType: InstanceCacheChangeEvent,
eventHandler: InstanceCacheChangeEventHandlerType<T, A, E>
): () => void;

Allows you to register events for this state instance.

The supported events are:

  • change: When the state value changes, you receive the new state.
  • cache-change: When a cache entry changes, you receive the whole cache.
  • dispose: When disposing the instance occurs.

dispose

dispose(): boolean;

getLane

lanes are Source objects attached to the same state instance. They share the same cache.

getLane(laneKey?: string): Source<T, A, E>;

If the request lane doesn't exist, it is created and returned.

danger

The lane source's key should be considered as unique too, because it will be attached to the same context and uses the same config.

If an state with the same lane key already exists, it is returned.

hasLane

hasLane(laneKey: string): boolean;

Returns true if the source has a lane with that key.

removeLane

removeLane(laneKey?: string): boolean;

Will detach the lane from its parent.

getAllLanes

getAllLanes(): Source<T, A, E>[];

Will return all the lanes attached to the source.