🎸 Dreamstate book (v4)
Summary
API
-
Management
-
Signaling / Queries
-
Utils
-
Testing
Examples
Applications:
Usage:
- Checking manager disposal status
- Decoupling managers logics
- Dynamic manager provision
- Forcing manager update
- Getting manager scope
- Listen signals from component
- Providing initial state
- Provision query from component
- Provision query from manager
- Query data from component
- Query data from manager
- Send signal from component
- Send signal from manager
- Testing components
- Testing managers
- Using default context value
- Using parts of context
- Using scope
Links
Licence
MIT
Pros and const
Pros
No boilerplate code
- .... tbd
Separation of view and data
- .... tbd
Separate single-responsibility stores, no large store
- .... tbd
No dependency injection or heavy logic, just react Context API
- .... tbd
Scalable modular architecture united by scope
- .... tbd
Subscribe only to needed data
- .... tbd
Simple updates with signals and queries
- .... tbd
Simple communication between application components
- .... tbd
Cons
tbd
Requirements:
- React 16.8+ (hooks) required
- Typescript / javascript
Usage
Install package
npm install dreamstate
Create a ContextManager
Create a manager by extending the ContextManager class.
This manager defines your initial context and any logic.
import { ContextManager, createActions } from 'dreamstate';
export interface ISimpleContext {
simpleActions: {
increment(): void;
}
value: number;
}
export class SimpleManager extends ContextManager<ISimpleContext> {
public context: ISimpleContext = {
simpleActions: createActions({
increment: () => this.increment(),
}),
value: 0,
};
public increment(): void {
this.setContext(({ value }) => ({ value: value + 1 }));
}
}
Create a Provider
Generate a React provider using the createProvider function, passing your managers classes as arguments.
import { createProvider } from 'dreamstate';
import { SimpleManager } from './SimpleManager';
export const Provider = createProvider(SimpleManager);
Wrap your React tree with Scope and Context providers
Wrap your application (or a portion of it) with the provider to make the manager's context available to all child components.
import { ScopeProvider } from "dreamstate";
import React, { ReactElement } from "react";
import { Provider } from "./Provider";
import { ExampleComponent } from "./ExampleComponent";
export function Application(): ReactElement {
return (
<ScopeProvider>
<Provider>
<ExampleComponent/>
</Provider>
</ScopeProvider>
);
}
Use your context and update it
Later you can access your manager context data from component and call manager actions in hooks/event handlers.
import { useManager } from "dreamstate";
import React, { ReactElement } from "react";
import { SimpleManager } from "./SimpleManager";
export function ExampleComponent({
simpleContext: { value, simpleActions } = useManager(SimpleManager),
}): ReactElement {
return (
<div>
<div>
value: {value}
</div>
<div>
<button onClick={simpleActions.increment}>
increment
</button>
</div>
</div>
);
}
Advanced usage
For advanced usage check API documentation and examples sections in doc book.
Changelog
4.5.0 (2/15/2025)
Changed
- Updated lib dependencies, use latest version of rollout/babel/ts
- Use scoped providers by default instead of single combined provider (still configurable value)
- Added JSDoc blocks for exported functions and classes, corrected existing ones
- Corrected and simplified some types (mostly reflects on JSDoc)
- Removed unnecessary calls to
getDefaultContext
- Smaller optimizations with map lookups (has+get vs get+if)
- Consistent displayName for dev bundles components
- Do not set displayNames for prod bundles
- Exclude sourcemaps from lib builds
4.4.1 (5/13/2024)
Changed
- re-export PartialTransformer type
4.4.0 (9/13/2023)
Changed
- useContextWithMemo simplified
- useContextWithMemo support of React 18 strict mode
- method calls do not print warning about disposal
4.3.1 (8/2/2022)
Added:
- getDefaultContext static method to provide default context when manager is not provided
Changed:
- dreamstate error code mapping to error message
- dreamstate errors throwing instead of default TypeError
4.3.0 (2/17/2022)
Added:
- DreamstateError class for internal errors handling
- DreamstateErrorCode enum added containing all internal error codes from library
- 'getScope' method for context manager instances
Changed:
- Errors from all methods are wrapped now and contain message and code information
4.2.0 (12/9/2021)
Added:
- mockRegistry test-util added
- mockManagerInitialContext test-util added
Updated:
- HMR, case when tree was not synced with actual store data when subscribed with simple useContext managers (useManager without memo)
- setContext and forceUpdate methods can be called out of scope. In this case simply modify class and continue working
- Signal/query methods correctly throw exception if called out of scope (created with new or when doing it from constructor)
- Validate signals types for signals decorators and methods
- Validate queries types for query decorators and methods
- Register method now allows overriding default initial context for mocking/testing
- mockScope method now uses config object as first param instead of boolean variable
4.1.1 (11/3/2021)
Added:
- New scope method 'getContextOf' introduced
4.1.0 (8/20/2021)
Added:
- IS_DISPOSED field for ContextManager instances to indicate current state
- General testing utils flow was reviewed, revisited methods for testing and simplified it
- Mock scope provider test util for scope testing
- Mock manager test util for isolated scope mocking
- Mock managers test util for isolated scope mocking
Updated:
- Do not affect scope after disposing with setContext and forceUpdate methods
- Return signal event from 'emitSignal' manager method
- Warn in console when signal handler fails
- Mocked scope can toggle lifecycle now
4.0.0 (7/2/2021)
Added:
- mockScope test-util added
- mockScopeProvider test-util added
- mockManagerWithScope test-util added
Removed:
- register/observing methods (test-utils) removed
- getCurrentContext (test-utils) removed
- IS_SINGLE removed in favor of scoped storages without global storing // related to HMR problems and best approach for data management
- ContextService removed in favor of ContextManager
- Provide decorator removed
- Consume decorator removed
- withProvision HoC removed
- withConsumption HoC removed
- before/after update lifecycle methods removed
- useSignals method removed
- queryDataAsync stopped supporting array of queries
Updated:
- getCurrent moved to test-utils
- queryData renamed to queryDataAsync
- mount/unmount order now matches react components
- signals/queries cannot be sent from disposed context manager class
- global methods/getters moved to scope context
- sync emitSignal calls
- correct inheritance of signals/queries metadata
- less verbose typing for queries and signals events
- lifecycle events made public for easier testing
- more optimized loadable/nested values
- ContextManager supports default state without manual initialization (empty object)
3.3.2 (4/11/2021)
- 'partialHotReplacement' parameter for created provider elements that are disabled by default -> allow hot updates and partial reload of context managers
- deprecated multiple queries at once for queryData methods
3.3.1 (4/6/2021)
- 'registerQueryProvider' method added
- 'unRegisterQueryProvider' method added
- syncQuery improvements
- types improvements
3.3.0 (4/5/2021)
- 'initialState' for context providers while constructing servers before provision
- 'queryDataSync' for sync queries execution without promises
- 'useSignals' now subscribes to provided dependencies and does proper re-subscription in useEffect
- 'createActions' util for actions packing without update checking
- Signal type update without optional data parameter
- Default param value for 'asMerged' method for nested objects
- Better typing/type derivation for Signal/SignalEvent types
3.2.0 (3/3/2021)
- Separated provision logic and logic of observing
- Support of hot modules replacement
- Support of sources dynamic changes for observer elements (HMR part)
3.1.4 (2/26/2021)
- No looped hooks creation for provider hooks
3.1.3 (9/17/2020)
- Added resolving promise as return value for emit signal method/function
3.1.1 (9/2/2020)
- Fixed issues with numeric signal types check (0)
- Removed null from default queryData typing - now you should add it manually
3.1.0 (8/20/2020)
- Better typing - d.ts files will be bundled in a few files instead of whole project tree re-exporting
- Better package building - no garbage inside library dist
- Services now listen own signals and queries, but can be filtered manually
- Conditional bundles for dev[dev assistance and better errors messaging] and prod[faster and smaller] environment
- queryData method exported for external usage
- createMutable -> createNested renamed to match method usage
- createComputed introduced for computed values selectors
3.0.1 (5/12/2020)
- Arrays as queryData method parameters for multiple queries fetching
- Core update:
- ContextWorker -> ContextService to reduce confusion with workers and responsibility scope
- Test utils update:
- registerWorker -> registerService
- unRegisterWorker -> unRegisterService
- getWorkerObserversCount -> getServiceObserversCount
- isWorkerProvided -> isServiceProvided
- addManagerObserver -> addServiceObserver
- removeManagerObserver -> removeServiceObserver
- Minor implementation fixes - shorter code samples/simplified call checks.
3.0.0 (5/9/2020)
- Tests added, stricter check of library for production builds
- Included CJS and ESM bundles into library
- Tree shaking
- Added "dreamstate/test-utils" for lib testing
- Signals API added
- OnSignal decorator added
- useSignals hook added
- unsubscribeFromSignals method added
- subscribeToSignals method added
- emitSignal method added
- ContextManager::emitSignal method added
- Query API added
- OnQuery decorator added
- ContextManager::queryData method added
- ContextWorker added
- Exposed base abstract class for signals and queries observing with provision lifecycle
- getCurrentContext method added
- getCurrent method added
- createSetter method added
- createNested method added with related type
- ContextManager related react context is named same as manager class with DS. prefix
- ContextManager::REACT_CONTEXT field added
- ContextManager::IS_SINGLE field added for singleton instances
- removed ContextManager::static::current method
- removed ContextManager::static::currentContext method
- removed ContextManager::static::getSetter method
- removed ContextManager::static::getContextType method
- removed asInitial method from Loadable
- added optional parameters for createLoadable constructor (value, isLoading, error)
- shared methods for all loadable values
- shallow check will apply only to Mutable or Loadable values inside context (not all objects)
- useManager now has optional second parameter for dependencies check
- exported types are not prefixed with T or I now
- removed odd REGISTRY object for global scope
- cleaned up base ContextManagerClass
- shared logic for decorators implementation
- added check for base class, ID and REACT_TYPE references will throw error now
- better @Consume performance, shouldUpdate checks for selectors
- @Provide and @Consume now require array parameter, not variadic parameters
- @Provide and @Consume now correctly validate input parameters
- @Bind decorator does not allow direct property re-writing on runtime now
- @Bind decorator still can be modified with Object.defineProperty for testing and 'special' cases
Scope
What is scope
In Dreamstate, a scope represents an isolated context that manages a set of states, signals, and queries within a React application. It encapsulates all the context managers and their associated state, ensuring that updates and changes remain confined to a specific area of your app.
Purpose
The primary purpose of scopes in Dreamstate is to enhance state management by isolating state, defining lifetimes, and localizing signals and queries. The idiomatic use of queries, signals, and isolated stores enables the creation of reusable state containers without circular references or hard dependencies.
Registry
The registry in Dreamstate acts as a centralized store that tracks all active context managers, listeners, signals, and queries. It facilitates state updates, provisioning, signaling, and subscriptions.
The registry instance is hidden behind the Dreamstate implementation but can be easily accessed for testing or debugging purposes. Direct runtime access is not recommended and is usually unnecessary.
Signals
Signals are event notifications used within Dreamstate to communicate changes or trigger actions between different parts of your application. By emitting signals, context managers can inform subscribers about state changes without creating tight coupling, fostering a more reactive and decoupled architecture.
Signals can be emitted by managers, React components, or virtually any part of the application that has access to the scope reference.
Similarly, signal subscriptions can be established from any location with a scope reference, including managers and React components.
Queries
Queries in Dreamstate provide a mechanism to retrieve data from the current scope. They allow you to access state data without relying on specific implementation details, as they are handled by matching query response handlers within the current scope.
Testing
Dreamstate includes testing utilities designed to facilitate isolated testing of your application's state management. By creating isolated scopes and mocking context managers, you can simulate state transitions, signals, and queries in a controlled environment, leading to more reliable and maintainable code.
ScopeContext
Type
Object interface.
About
The ScopeContext
interface describes the methods and data available within the current Dreamstate scope.
All active providers, managers, signals, and queries operate within this scope and do not function outside of it.
Accessing the scope allows you to work with specialized methods for signal handling, query processing,
and context management.
Interface signature
export interface ScopeContext {
INTERNAL: ScopeContextInternals;
getContextOf<
T extends AnyObject,
D extends ContextManagerConstructor<T>
>(
manager: D
): T;
emitSignal<
D = undefined
>(
base: BaseSignal<D>,
emitter?: AnyContextManagerConstructor | null
): SignalEvent<D>;
subscribeToSignals<
D = undefined
>(
listener: SignalListener<D>
): TCallable;
unsubscribeFromSignals<
D = undefined
>(
listener: SignalListener<D>
): void;
registerQueryProvider<
T extends QueryType
>(
queryType: T,
listener: QueryListener<T, AnyValue>
): TCallable;
unRegisterQueryProvider<
T extends QueryType
>(
queryType: T,
listener: QueryListener<T, AnyValue>
): void;
queryDataSync<
D,
T extends QueryType,
Q extends OptionalQueryRequest<D, T>
>(
query: Q
): QueryResponse<AnyValue>;
queryDataAsync<
D,
T extends QueryType,
Q extends OptionalQueryRequest<D, T>
>(
query: Q
): Promise<TQueryResponse<TAnyValue>>;
}
Notes
- Scope creation: the scope is automatically created by the Dreamstate ScopeProvider and can also be mocked for testing purposes
- Isolation: the scope isolates the application from external components, storing all relevant data within its context instead of using global state
- Signal and Query Processing: all signals and queries are processed within the scope, ensuring that only components within the same scope interact with each other
- Component Access: React components can access the scope to work with signals and queries, facilitating efficient data management and communication
- Testing and Mocking: The scope can be accessed directly for testing or mocking purposes, providing flexibility during development and unit testing
Fields
INTERNAL
- Type:
IScopeContextInternals
- Description:
Contains library internals. This field is not intended for regular use and is primarily available for unit testing and debugging purposes.
Methods
getContextOf
- Description: Retrieves the current context snapshot for the specified manager class.
- Usage: Use this method to access the state managed by a specific ContextManager within the current scope.
- Returns: Context of manager class if it is provided or default context value.
emitSignal
- Description: Emits a signal for all subscribers within the current Dreamstate scope.
- Parameters:
- base: The base signal object that contains a type and an optional data field.
- emitter (optional): The reference of the signal emitter.
- Returns: A signal event instance that wraps the emitted signal.
subscribeToSignals
- Description: Subscribes a listener to signal events within the current scope. The listener is invoked on each signal event.
- Parameters:
- listener: A callback function that handles signal events.
- Returns: A callable function that, when invoked, unsubscribes the listener.
unsubscribeFromSignals
- Description: Unsubscribes the specified listener from signal events within the current scope.
- Parameters:
- listener: The callback function to remove from the subscription list.
registerQueryProvider
- Description: Registers a query provider callback that will answer query data requests for the specified query type.
- Parameters:
- queryType: The type of query for which the provider will supply data.
- listener: The callback that processes queries and returns the required data.
- Returns: A callable function to unregister the query provider when invoked.
unRegisterQueryProvider
- Description: Unregisters a previously registered query provider callback so that it no longer handles query data requests.
- Parameters:
- queryType: The type of query for which the provider was registered.
- listener: The callback to be unregistered.
queryDataSync
- Description: Synchronously queries data from the current scope. The handler for the specified query type is executed and its result is wrapped as a query response.
- Parameters:
- query: An object representing the query request, including the query type and an optional data field.
- Returns: A query response object if a valid handler is found, or null if no handler exists.
queryDataAsync
- Description: Asynchronously queries data from the current scope. The handler for the specified query type is executed, and its result is returned as a promise that resolves with a query response.
- Parameters:
- query: An object representing the query request, including the query type and an optional data field.
- Returns: A promise that resolves with the query response if a valid handler is found, or null if no handler exists.
ScopeProvider
Type
React component.
About
The ScopeProvider
component is responsible for isolating the Dreamstate scope.
It ensures that signaling, context managers, and queries operate within a specific provider instance.
When a ScopeProvider
unmounts, all associated managers stop working, freeing up memory.
This component provides access to the current state of managers, as well as signaling and query methods.
Method Signature
export interface ScopeProviderProps {
children?: ReactNode;
}
function ScopeProvider(
props: ScopeProviderProps
): ReactElement;
Props
- props.children: React nodes that will be wrapped by the ScopeProvider
Notes
- Provides a dedicated Dreamstate scope for context isolation
- Signals and queries function strictly within the given scope
- Nested ScopeProvider instances create isolated sub-scopes, preventing cross-scope interference
- ScopeProvider should always be rendered above any context manager providers.
Usage:
To enable Dreamstate's functionality, wrap your application's providers and consumers with ScopeProvider. Typically, a single ScopeProvider is used at the root level of the application.
import { createProvider, ScopeProvider } from "dreamstate";
// ... other imports ...
const StoreProvider = createProvider([SampleManager]);
export function ApplicationProvider({
children
}: PropsWithChildren<Record<string, unknown>>): ReactElement {
return (
<ScopeProvider>
<StoreProvider>{children}</StoreProvider>
</ScopeProvider>
);
}
useScope
Type
Function.
About
The useScope
hook provides access to the current Dreamstate scope.
It returns a ScopeContext
object or null
if no scope is available.
Scope enables interaction with context managers, signals, and queries without triggering unnecessary re-renders.
Call Signature
function useScope(): ScopeContext;
Returns
ScopeContext – The current Dreamstate scope, if available. The returned scope is a constant reference and does not update. It can be regenerated with Hot Module Replacement updates.
Usage
Using useScope
allows retrieving manager values without triggering effects on every update:
export function SomeComponent(): ReactElement {
const scope: ScopeContext = useScope();
// Manager updates will not trigger effect every time, but we still have access to latest context values:
useEffect(() => {
return scope.subscribeToSignals(() => {
const someContext: ISomeContext = scope.getContextOf(SomeContextManager);
// Handle the retrieved context...
});
}, [scope]);
return <div>content</div>;
}
Signals can be dispatched to notify subscribers in the current scope:
export function SomeComponent(): ReactElement {
const scope: ScopeContext = useScope();
const onClick = useCallback(() => {
scope?.emitSignal({ type: "SOME_SIGNAL" });
}, [scope]);
return <button onClick={onClick}>Emit Signal</button>;
}
A component can register itself as a provider for specific queries:
export function SomeComponent(): ReactElement {
const scope: ScopeContext = useScope();
useEffect(() => {
return scope.registerQueryProvider("SOME_QUERY_TYPE", () => "some_query_response");
}, [scope]);
return <div>Query provider active</div>;
}
Data queries can be executed synchronously within the current scope:
export function SomeComponent(): ReactElement {
const scope: ScopeContext = useScope();
const [state, setState] = useState(0);
const onClick = useCallback(() => {
const response: QueryResponse<number> | null = scope.queryDataSync({ type: "SOME_NUMBER" });
if (response) {
setState(response.data);
}
}, [scope]);
return <button onClick={onClick}>Query data</button>;
}
ContextManager
Type
Abstract class.
About
ContextManager
is the core abstract class for managing application data in Dreamstate. It should be extended
by your application store managers to encapsulate state, handle updates, emit signals, and process queries.
- To provide specific
ContextManager
classes into the React tree, see the createProvider method - To consume data from a specific
ContextManager
in the React tree, see the useManager hook - For detailed information on how shallow comparisons are performed during context updates, refer to utility methods like createNested, createActions, and others
Class Signature
abstract class ContextManager<
T extends AnyObject,
S extends AnyObject
> {
public static get REACT_CONTEXT(): Context<T>;
public static getDefaultContext(): AnyObject | null
public IS_DISPOSED: boolean;
public context: T;
public constructor(initialState?: S);
public onProvisionStarted(): void;
public onProvisionEnded(): void;
public getScope(): ScopeContext;
public forceUpdate(): void;
public setContext(next: Partial<T> | PartialTransformer<T>): void;
public emitSignal<D = undefined>(baseSignal: BaseSignal<D>): void;
public queryDataAsync<
D extends AnyValue,
T extends QueryType,
Q extends OptionalQueryRequest<D, T>
>(
queryRequest: Q
): Promise<TQueryResponse<AnyValue>>;
public queryDataSync<
D extends AnyValue,
T extends QueryType,
Q extends OptionalQueryRequest<D, T>>(
queryRequest: Q
): TQueryResponse<any>;
}
Notes
- Constructor best practices:
- Avoid heavy logic or subscriptions in the constructor; use it primarily for synchronous initialization of class fields
- Lifecycle management:
- Context managers should begin operations on the provision start event and perform cleanup on the provision end event
- State organization:
- Avoid creating deeply nested objects in stores; flatter state structures are easier to maintain
- Hot module replacement:
- Managers should be modular. Clean all data on provision end and avoid reliance on the current environment for easier HMR updates during development
Static methods and properties
getDefaultContext
- Description:
Returns the default React context value. This method provides a placeholder value for context consumers when no specific manager is provided. - Return type:
AnyObject | null
- Notes:
Defaults to
null
if no custom default is specified.
REACT_CONTEXT
- Description:
Retrieves the associated React context for the currentContextManager
. This context is lazily initialized and is useful for manual rendering or testing. - Return Type:
Context<AnyValue>
- Notes:
Throws an error if accessed directly on the baseContextManager
class, as direct references to ContextManager statics are forbidden.
Instance fields
IS_DISPOSED
- Description:
A flag that indicates whether the manager is still active or has been disposed.
Once disposed, the manager cannot be reused, and scope-related methods (signals, queries) will throw exceptions if used. - Type:
boolean
- Default Value:
false
context
- Description:
Stores the current state of the manager. This field is synchronized with React providers viasetContext
.
It should always be an object, and while nested field mutations are possible, they are not recommended. - Type:
T
- Notes:
Meta fields (e.g., created bycreateActions
orcreateNested
) may use a different comparison mechanism than the standard shallow check.
Constructor
constructor(initialState?: S)
- Description:
Initializes a new instance of theContextManager
with an optional initial state.
This initial state may be used for synchronous initialization or provided from SSR. - Parameters:
initialState
(optional): The initial state used to set up the manager.
- Notes:
CallsprocessComputed
on the context to ensure that computed fields are calculated before the manager is provisioned.
Instance Methods
onProvisionStarted
- Description:
Lifecycle method invoked when the first provider is injected into the React tree.
It is analogous tocomponentWillMount
in class-based components. - Usage:
Used for data initialization and setting up subscriptions. - Return Type:
void
onProvisionEnded
- Description:
Lifecycle method called when the last provider is removed from the React tree.
It functions similarly tocomponentWillUnmount
. - Usage:
Ideal for cleanup tasks such as disposing of data and unsubscribing from events, especially during hot module replacement (HMR) or dynamic managers provision. - Return Type:
void
getScope
- Description:
Retrieves the current manager scope, which contains methods for managing context and retrieving manager instances. - Return Type:
ScopeContext
- Throws:
Throws an error if the manager is out of scope.
forceUpdate
- Description:
Forces an update and re-render of subscribed components by creating a new shallow copy of the current context.
This ensures that updates are propagated to the provider. - Usage:
Useful for ensuring that all subscribers are updated when necessary. - Return Type:
void
- Notes:
Only forces an update of the provider; components usinguseManager
selectors will not be forced to re-render.
setContext
- Description:
Updates the current context using a partial state object or a functional selector.
It performs a shallow comparison with the existing context and updates the provider only if changes are detected. - Parameters:
next
: A partial context object or a function that returns a partial context.
- Return Type:
void
emitSignal
- Description:
Emits a signal to other managers and subscribers within the current scope.
A signal includes a type and optional data. - Parameters:
baseSignal
: An object containing a signal type and optional data.
- Return Type:
SignalEvent<D>
- Throws:
Throws an error if the manager is out of scope.
queryDataAsync
- Description:
Sends an asynchronous query to retrieve data from query handler methods.
Particularly useful for async providers. - Parameters:
queryRequest
: An object containing the query type and optional data.
- Return Type:
Promise<QueryResponse<AnyValue>>
- Usage:
Resolves with a query response object if a valid handler is found, ornull
if not.
queryDataSync
- Description:
Sends a synchronous query to retrieve data from query handler methods.
Ideal for synchronous providers; async providers may still return a promise within the data field. - Parameters:
queryRequest
: An object containing the query type and optional data.
- Return Type:
QueryResponse<AnyValue> | null
- Usage:
Returns a query response if a valid handler is found, ornull
if no handler exists in the current scope.
Usage
Below is an example of creating an authentication manager that stores user information and provides methods for state mutations:
import { ContextManager } from "dreamstate";
export interface IAuthContext {
authActions: {
randomizeUser(): void;
changeAuthenticationStatus(): void;
};
isAuthenticated: boolean;
user: string;
}
export class AuthManager extends ContextManager<IAuthContext> {
public readonly context: IAuthContext = {
authActions: createActions({
changeAuthenticationStatus: () => this.changeAuthenticationStatus(),
randomizeUser: () => this.randomizeUser()
}),
isAuthenticated: true,
user: "anonymous"
};
public changeAuthenticationStatus(): void {
this.setContext(({ isAuthenticated }) => ({ isAuthenticated: !isAuthenticated }));
}
public randomizeUser(): void {
this.setContext({ user: "user-" + Math.random() });
}
}
To integrate this manager with other context managers, provide it at the application level using Dreamstate's scope:
import { ScopeProvider, createProvider } from "dreamstate";
// ...
const ApplicationProvider = createProvider([CustomService, AuthManager]);
export function ApplicationRoot(): ReactElement {
return (
<ScopeProvider>
<ApplicationProvider>
<ApplicationRouter/>
</ApplicationProvider>
</ScopeProvider>
);
}
For consuming the context in your components, use the useManager hook:
import { useManager } from "dreamstate";
// ...
export function SomeNestedComponent(): ReactElement {
const { user } = useManager(AuthManager);
return (
<div>
Current user is: {user}
</div>
);
}
createProvider
Type
Function.
About
createProvider
is a factory function for combining context managers and creating a single provider component.
It enables components within the provider to consume the related context seamlessly.
Call Signature
interface ProviderProps<T> {
initialState?: T;
children?: ReactNode;
}
export interface CreateProviderProps {
/**
* A flag that determines whether to observe the context changes in one large React node
* or as smaller scoped nodes for better performance.
*/
isCombined?: boolean;
}
function createProvider(
sources: Array<ContextManagerConstructor>,
config?: CreateProviderProps,
): FunctionComponent<ProviderProps<T>>;
Parameters
- sources: an array of context manager classes to be provided
- config: configuration object for adjusting created provider
Returns
A React component that provides the specified context managers within a component tree. The returned component supports an initialState property for initializing context.
Notes:
- Do not create providers inside render functions
- Providers should be created once per combination as a constant reference, globally or in some cached storage
- It is preferable to have a few provision points rather than providing ContextManagers everywhere
initialState
property can be passed on provider on rendering, it will be supplied for managers on constructionisCombined
config value can be passed on provider creation to adjust how managers are provisioned
Throws
- TypeError: thrown if the parameter is not an array
- TypeError: thrown if any member of the array does not extend ContextManager
Usage
For example, to provide context services with a pre-calculated initial state:
// Create the provider as a static value, should not be inside a render function.
const RootProvider: FunctionComponent = createProvider([
AuthContextManager,
MediaContextManager,
SomeService,
]);
const initialState = { a: 1, b: 2 };
function SampleComponent(): ReactElement {
return (
<RootProvider initialState={initialState}>
<ApplicationRouter/>
</RootProvider>
);
}
useManager
Type
Function.
About
useManager
is a hook for consuming ContextManagers. It serves as a specialized alias for
React's useContext
, tailored for use with ContextManagers. Additionally, it accepts an optional memo selector
function that returns an array of observed dependencies, allowing optimized re-rendering
based on specific context properties.
Call Signature
function useManager<
S extends AnyObject,
M extends ContextManagerConstructor<S>
>(
Manager: M,
depsSelector?: (context: S) => Array<any>
): S;
Parameters
- Manager: The class reference of the ContextManager to consume within the current component scope
- depsSelector (optional): A callback function that extracts an array of dependencies from the context. This selector is used for diff checking to optimize re-rendering by triggering updates only when the specified dependencies change
Returns
The current context value provided by the specified ContextManager in the active scope or value declared in static
gedDefaultContext method. If static method was not overrode in manager class, returns null
.
Throws
- TypeError: Thrown if the supplied parameter is not an instance of a ContextManager
Notes
- Using a selector callback as the second parameter can help optimize rendering in components that consume large contexts by focusing updates on only the necessary fields
- Supplying the manager as a default parameter makes the component easier to test, as it allows you to mock the context without wrapping the component in a Context.Provider. Also it can help to visually distinguish internal component hooks and subscription to managers
Usage
For example, to consume context provided by AuthManager:
export function SomeComponent(): ReactElement {
const { username } = useManager(AuthManager);
// ... component logic and rendring
}
By default, every update to the auth context will trigger a re-render of the component (as is typical with React's useContext). To observe only specific fields and avoid unnecessary re-renders, supply a memo selector:
export function SomeComponent(): ReactElement {
const { username } = useManager(
AuthManager,
({ username }: IAuthContext) => [username]
);
// ... component logic
}
Variant with props:
export function SomeComponent({
authContext: { username } = useManager(AuthManager)
}): ReactElement {
// ... component logic and rendring
}
OnQuery
Type
Method decorator.
About
Marks method as query handler. Every query request with provided type will be handled by service instance method.
Services handlers priority is same as instantiation order.
Mainly used to register query requests handlers with generic getter logic that does some job and returns results.
Call Signature
type QueryType<T extends string = string> = symbol | string | number | T;
interface QueryRequest<D extends any = undefined, T extends QueryType = QueryType> {
type: T;
data?: D;
}
@OnQuery(queryType: QueryType): MethodDecorator;
Parameters
- queryType - specific query type to be handled by decorated method
Throws
- TypeError : no query type parameter(s) present
- TypeError : decorated method does not belong to ContextManager class
Parameters
- Should be used to mark some service getter methods as query handlers
- Handlers can be both sync and async
- Queries should be simple getters without side effects
Usage
If few services need some data that should not be observed or handled by another class, but is needed for one-time operation, queries can help.
As an example, some service has method that logs some important data.
It is not important where it is placed and who handles it, only specific data is needed.
import { QueryResponse, ContextManager } from "dreamstate";
export class LoggingService extends ContextManager {
public logImportantData(): void {
const information: QueryResponse<string> = this.queryDataSync({ type: "IMPORTANT_DATA" });
if (information) {
console.info("Got some data for you:", information.data);
} else {
console.info("Could not get data, no one responded.");
}
}
}
Current implementation of 'logImportantData' method tries to get some data, but it cannot guarantee if anyone will respond.
@OnQuery decorated methods always return value packed in a 'QueryResponse' object.
Manager with important data that handles query can look like this:
import { QueryRequest, OnQuery, ContextManager } from "dreamstate";
export class ImportantDataService extends ContextManager {
@OnQuery("IMPORTANT_DATA")
public onImportantDataRequested(queryRequest: QueryRequest<void>): string {
return "Really important data!";
}
}
Or it could be event simple react component:
import { QueryRequest, OnQuery, ContextManager, useScope, ScopeContext } from "dreamstate";
// ...
export function SomeComponent(): ReactElement {
const scope: ScopeContext = useScope();
useEffect(() => {
return scope.registerQueryProvider("IMPORTANT_DATA", () => {
return "Some important data.";
})
}, []);
return "Some label";
}
So, if instance of ImportantDataService is provided (or component is rendered) by the moment when LoggingService needs some important data, it will respond on the query.
With parameters:
Query parameters can be provided when sending query:
export class LoggingService extends ContextManager<{}> {
public context: {} = {};
public logMultipliedData(): void {
const multiplied: QueryResponse<number> = this.queryDataSync({
type: "MULTIPLIED_BY_TWO",
data: 16
});
if (multiplied) {
console.info("Multiplied value:", information.data); // 32
} else {
console.info("Could not get data, no one responded.");
}
}
}
export class MultiplicationService extends ContextManager {
@OnQuery("MULTIPLIED_BY_TWO")
public onImportantDataRequested(queryRequest: QueryRequest<number>): number {
return queryRequest.data * 2;
}
}
Async:
If getter should be async, alternative would look as simple as sync one:
export class LoggingService extends ContextManager {
public async logImportantData(): Promise<void> {
const information: QueryResponse<string> = await this.queryDataAsync({ type: "IMPORTANT_DATA" });
if (information) {
console.info("Got some data for you:", information.data); // "Really important data!"
} else {
console.info("Could not get data, no one responded.");
}
}
}
OnSignal
Type
Method decorator.
About
Marks method as signal handler. Every signal event with provided type will be handled by a service instance.
Services handlers priority is same as instantiation order.
Mainly used to register signal events handlers with generic logic that reacts on application events.
Call Signature
type SignalType = symbol | string | number;
@OnSignal(signalType: Array<SignalType> | SignalType): MethodDecorator;
Parameters
- signalType - specific signal type or array of signal types to be handled by decorated method
Throws
- TypeError : no signal type parameter(s) present
- TypeError : decorated method does not belong to ContextManager class
Parameters
- Should be used to mark some service methods as signal event handlers
- Signal handlers are used to notify every service at once about something
- Same approach as with 'reducer-action' but without functional data stores
Usage
If services need to handle some global events that would require manual action calls each time, signals can be used.
Also useful when one context service requires reaction from others.
For example, connection manager can notify other services about state change or network issues.
enum EGlobalSignal {
CONNECTION_STATE_CHANGE = "CONNECTION_STATE_CHANGE"
}
export class NetworkManager extends ContextManager {
public onConnectionStateChanged(connectionState: boolean): void {
this.emitSignal({ type: "CONNECTION_STATE_CHANGE", data: connectionState });
}
}
Current implementation of 'onConnectionStateChanged' emits signal when being called. Emitted signal contains type and current connection state information.
Manager that handles signal can look like this:
type TConnectionStateSignal = SignalEvent<EGlobalSignal.CONNECTION_STATE_CHANGE, boolean>;
export class HandlerManager extends ContextManager {
@OnSignal(EGlobalSignal.CONNECTION_STATE_CHANGE)
public onConnectionStateChanged(signalEvent: TConnectionStateSignal): string {
if (signalEvent.data) {
log.info("Network state changed to connected:", signalEvent);
} else {
log.info("Network state changed to disconnected:", signalEvent);
}
}
}
Every time something emits 'CONNECTION_STATE_CHANGE' event, HandlerManager will react with strictly declared logic.
Bind
Type
Class method decorator.
About
The @Bind
decorator automatically binds class methods to their instance.
This ensures that the this
keyword
within the method always refers to the instance that owns the method, eliminating the need
for manual binding (e.g., using .bind()
in the constructor or arrow functions).
This decorator is especially useful in scenarios where methods are passed as callbacks or used in event handlers, as it preserves the correct context.
Call Signature
@Bind();
Throws
- Error: thrown if an attempt is made to reassign a method decorated with @Bind at runtime
Notes
- Intended use: designed to work seamlessly with ContextManagers
- Restrictions: should not be used with static methods
Usage
When you decorate a method with @Bind, all references to that method are bound to the instance that defines it. This means that even if the method is detached from its original context, it will still operate correctly.
class Manager extends ContextManager {
public numberValue: number = 42;
public stringValue: string = "justForExample";
@Bind()
public getValue(): number {
return this.value
}
}
// Even after losing `this` context, method still is bound to it.
const manager: Manager = new Manager();
const getValue: number = manager.getValue;
console.log(getValue()); // Still returns 42
Reference
All credits to 'https://www.npmjs.com/package/autobind-decorator'.
createActions
Type
Function.
About
The createActions
function creates an actions store - a container for read-only method references.
This store is visually and programmatically distinct, ensuring that the actions it contains are not
included in shallow comparisons during state updates. This separation helps maintain the immutability
of actions, preventing them from affecting the re-rendering process.
Call Signature
function createActions<T extends AnyObject>(actions: T): Readonly<T>;
Parameters
- actions: an object containing the methods exposed by a context manager
Notes
- Intention: this function creates a container that is both visually and programmatically distinguishable as a sub-storage for actions. Marking an object as an actions store ensures that it is excluded from the shallow comparisons performed during setContext calls, thereby keeping its reference unchanged across updates
Usage
Dreamstate performs a shallow comparison on every setContext call before updating the React tree. By marking a specific object as an actions store using createActions, you help the library distinguish it from generic context fields. As a result, actions objects remain immutable and are not considered during state update comparisons.
export class SampleManager extends ContextManager<ISampleContext> {
public context: ISampleContext = {
// The actions field is marked as an actions store.
// It will not be checked during context updates and will always remain the same.
sampleActions: createActions({
incrementSampleNumber: () => this.incrementSampleNumber(),
setSampleString: (value: string) => this.setSampleString(value)
}),
sampleNumber: 0,
sampleString: "default"
};
public setSampleString(value: string): void {
this.setContext({ sampleString: value });
}
public incrementSampleNumber(): void {
this.setContext(({ sampleNumber }) => {
return { sampleNumber: sampleNumber + 1 };
});
}
}
createComputed
Type
Function.
About
The createComputed
function creates a computed value that automatically recalculates when the context updates.
This is particularly useful when you have derived data that depends on the current state and should be refreshed
without manual intervention. The computed value is generated via a selector function, with an optional memo
function to determine when recalculation is necessary.
Call Signature
function createComputed<
T extends TAnyObject,
C extends TAnyObject
>(
selector: (context: C) => T,
memo?: (context: C) => Array<unknown>
): TComputed<T, C>;
Parameters
- selector: a function that takes the current context and returns an object representing the computed values
- memo: an optional function that returns an array of dependencies. This array is used to decide if the computed value should be recalculated after a context update
Throws
- TypeError: thrown if the computed value is not initialized with a valid functional selector, or if the provided memo parameter is not a function
Notes
- Automatic Calculation: designed to automatically compute values rather than requiring manual updates each time data is needed
- Pure Computation: the computed value should be a simple getter without side effects, always returning an object
- Exclusion from Updates: computed values are excluded from the shallow comparisons made during setContext calls, ensuring they do not trigger unnecessary updates
Usage
When you have data that can be computed at runtime and needs to update automatically as the underlying data changes, createComputed offers an elegant solution.
For example, to compute a value based on an array in the context:
class ComputedManager extends ContextManager<IComputedContext> {
public context: IComputedContext = {
// Every time any context value changes, computed.greaterThanFive will be recalculated.
computed: createComputed((context: IComputedContext) => ({
greaterThanFive: context.numbers.filter((it: number) => it > 5)
})),
numbers: [0, 2, 4, 6, 8, 12],
strings: ["a", "b", "c"]
};
}
If you want the computed value to update only when specific dependencies change, you can add a memo function:
class ComputedManager extends ContextManager<IComputedContext> {
public context: IComputedContext = {
// Every time the numbers array updates, computed.greaterThanFive will be recalculated.
computed: createComputed(
(context: IComputedContext) => ({
greaterThanFive: context.numbers.filter((it: number) => it > 5)
}),
({ numbers }) => [numbers]
),
numbers: [0, 2, 4, 6, 8, 12],
someStringValue: "example",
someNumberValue: 5000,
};
}
createLoadable
Type
Function.
About
The createLoadable
function is a utility for handling simple asynchronous data. It encapsulates the state of
an asynchronous operation by maintaining the current value along with its loading and error states. Each mutation
(such as setting loading, ready, or error states) creates a new shallow copy of the loadable object, which helps
to manage state updates in a predictable way while reducing boilerplate code.
Call Signature
function createLoadable<T, E>(
value?: T | null,
isLoading?: boolean,
error?: E | null
): Loadable<T, E>;
interface Loadable<T, E = Error> {
readonly value: T | null;
readonly isLoading: boolean;
readonly error: E | null;
asReady(value?: T): Loadable<T, E>;
asUpdated(value: T, isLoading?: boolean, error?: E | null): Loadable<T, E>;
asLoading(value?: T): Loadable<T, E>;
asFailed(error: E, value?: T): Loadable<T, E>;
}
Notes
- Immutable operations: do not assign mutable values manually. Instead, use the provided methods (asLoading, asReady, asFailed, and asUpdated) to update the loadable state
- Method integrity: do not reassign loadable methods manually
- Extension: the Loadable type is not a generic object; it extends from NestedStore
- Performance: Dreamstate applies shallow comparison to NestedStore objects, so invoking the same method multiple times with identical data will not trigger unnecessary re-renders
Usage
The createLoadable function is particularly useful when you need to manage asynchronous data within a manager class. For example, consider a scenario where you need to load user data from an API:
export interface IAuthContext {
user: Loadable<string>;
}
export class AuthManager extends ContextManager<IAuthContext> {
public readonly context: IAuthContext = {
user: createLoadable("anonymous")
};
public async randomizeUser(): Promise<void> {
// Initially: { isLoading: false, error: null, value: "anonymous" }
const { user } = this.context;
// As loading: { isLoading: true, error: null, value: "anonymous" }
this.setContext({ user: user.asLoading() });
await this.forMillis(1000);
// As ready: { isLoading: false, error: null, value: "loadedUser" }
this.setContext({ user: user.asReady("loadedUser") });
}
private forMillis(millis: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, millis));
}
}
In this example, createLoadable is used to initialize the user state. The manager then updates the state by calling asLoading before starting the asynchronous operation and asReady once the new data is available. This approach handles the loading state (and any potential errors) without additional boilerplate code.
createNested
Type
Function.
About
The createNested
function is a utility for handling nested data within a Dreamstate context.
It creates a nested sub-state designed for deeper shallow checking, allowing individual fields of
a nested object to be compared during state updates. This optimization utility does not add functional
value by itself but helps Dreamstate determine which parts of the nested state have changed.
Call Signature
function createNested<T extends AnyObject>(
initialValue: T
): Nested<T>;
interface NestedBase<T> {
asMerged(state: Partial<T>): Nested<T>;
}
type Nested<T> = T & NestedBase<T>;
Parameters
- initialValue: the initial state of the nested value object. This must be an object because nested stores are intended to hold state that will be compared field by field during context updates
Throws
- TypeError: thrown if the nested store is initialized with a value that is not an object
Notes
The primary purpose of createNested is to mark a nested context object as a sub-store. Dreamstate will include every field of this nested object in its shallow comparison process before updating the React tree, rather than only comparing the object reference. This means that changes to individual properties within the nested object will trigger re-renders appropriately, without needing to replace the entire nested object.
Usage
For example, if we want nested value to be shallow checked every time instead of only 'example' object reference check, we can write following code:
export class ManagerWithUtils extends ContextManager<IContextWithUtils> {
public readonly context: IContextWithUtils = {
exampleActions: createActions({}),
example: createNested({
loadable: createLoadable(10),
simpleString: "10"
}),
computed: createComputed((context: IContextWithUtils) => ({
booleanSwitch: context.nested.simpleString.length > 2,
concatenated: context.nested.simpleString + context.nested.simpleString + "!"
})),
basicObject: {
first: 1,
second: 2
},
primitive: 10,
};
}
How comparison will happen after "setContext call":
- Check
context.exampleActions
, looks like it is actions store so skipping it - Check
context.example
, looks like it is nested, so:- Check
example.loadable
by reference - Check
example.sampleString
by reference
- Check
- Check
computed
, looks like computed so skipping it - Check
basicObject
by reference - Check
primitive
by value
It means that react tree update will happen after setContext call only after following cases: - example.loadable changed
- example.simpleString changed
- basicObject reference changed
- primitive value changed
OR context
has new key addedexample
has new key added
getCurrent
Type
Function.
About
Test utility to get currently provided service instance.
Returns null if parameter class is not service or is not created.
Call Signature
function getCurrent<T extends TAnyContextManagerConstructor>(
ManagerClass: T,
scope: IScopeContext
): InstanceType<T> | null;
Parameters
- Helps to test context services in most cases
- Return value is optional - null if provision of requested service was not started
Usage
For example, I want to get auth manager instance in scope and test some method:
const scope: ScopeContext = mockScope();
const map: ManagerInstanceMap = mockManagers([TestManager], null, scope);
expect(map.get(TestManager)).toBeInstanceOf(TestManager);
expect(map.get(ExtendingManager)).toBeUndefined();
expect(getCurrent(TestManager, scope)).toBeInstanceOf(TestManager);
expect(getCurrent(ExtendingManager, scope)).toBeNull();
getReactConsumer
Type
Function.
About
Item description.
Call/Class Signature
Methods (for classes)
Throws
- Error : case
Notes
- Note 1
- Note 2
- Note 3
Usage
Usage examples.
Reference
getReactProvider
Type
Function.
About
Item description.
Call/Class Signature
Methods (for classes)
Throws
- Error : case
Notes
- Note 1
- Note 2
- Note 3
Usage
Usage examples.
Reference
mockManager
Type
Function.
About
Function to mock manager instance in testing context.
Call Signature
function mockManager<T extends TAnyObject, S extends TAnyObject, M extends IContextManagerConstructor<T, S>>(
ManagerClass: M,
initialState?: S | null,
scope: IScopeContext = initializeScopeContext()
): InstanceType<M>
Parameters
- ManagerClass - class definition reference to mock instance
- initialState - optional initial state used in constructor
- scope - optional scope for manager mocking
Returns
Mocked manager instance for testing in provided scope.
Usage
Used for testing of managers functionality.
describe("SomeManager functionality", () => {
it("should correctly handle some updates", () => {
const manager: ComputedManager = mockManager(ComputedManager);
expect(manager.context.numbers).toHaveLength(6);
expect(manager.context.computed.greaterThanFive).toHaveLength(3);
manager.setContext({ numbers: [1, 2, 10] });
expect(manager.context.numbers).toHaveLength(3);
expect(manager.context.computed.greaterThanFive).toHaveLength(1);
});
});
mockRegistry
Type
Function.
About
Item description.
Call/Class Signature
Methods (for classes)
Parameters (for functions)
Returns (for functions)
Throws
- Error : case
Notes
- Note 1
- Note 2
- Note 3
Usage
Usage examples.
Reference
mockScopeProvider
Type
Function.
About
Item description.
Call/Class Signature
Methods (for classes)
Parameters (for functions)
Returns (for functions)
Throws
- Error : case
Parameters
- Note 1
- Note 2
- Note 3
Usage
Usage examples.
Reference
mockScope
Type
Function.
About
Function to mock scope object in testing context. Mocked scope can be used to emit fake signals and query stored data.
Call Signature
interface IMockScopeConfig {
isLifecycleDisabled?: boolean;
applyInitialContexts?: Array<[TAnyContextManagerConstructor, TAnyObject]>;
}
function mockScope(
mockConfig: IMockScopeConfig = {},
registry: IRegistry = createRegistry()
): IScopeContext;
Parameters
- mockConfig - configuration object for mocked scope
- mockConfig.applyInitialContexts - array of mocked contexts to be applied for picked manager classes
- mockConfig.isLifecycleDisabled - whether lifecycle in scope is enabled
- registry - mocked object if internal registry should be replaced
Returns
Mocked ScopeContext object for testing.
Usage
Mocking util can be used to verify some signals or scope-level functionality with queries and managers interaction.
describe("Some manager functionality", () => {
it("Should correctly handle some signals", () => {
const scope: IScopeContext = mockScope();
const manager: TestManager = mockManager(TestManager, {}, scope);
expect(manager.context.field).toBe(1);
scope.emitSignal({ type: "SET_FIELD", data: 2 });
expect(manager.context.field).toBe(2);
});
});
nextAsyncQueue
Type
Function.
About
Item description.
Call/Class Signature
Methods (for classes)
Throws
- Error : case
Notes
- Note 1
- Note 2
- Note 3
Usage
Usage examples.