# Cotton Box > Minimal state management library for React and beyond. This file contains all documentation content in a single document following the llmstxt.org standard. ## AsyncSetStateFn ## Overview {DataType.type} {DocConstants.TSDOC_PARAM_DESC_SET_FUNCTION_ASYNC} ```ts type AsyncSetStateFn = (currentState: State, defaultState: State) => State | Promise ``` --- ## AsyncStateManager DoesNotReturnAnything, DoesNotTakeAnyParameters, Returns, } from '@site/src/components/sentence-templates' # AsyncStateManager ## Overview {DataType.class} {DocConstants.TSDOC_DESC_ASYNC_STATE_MANAGER} ```ts class AsyncStateManager extends StateManager ``` ### Constructor ```ts constructor(defaultState: State, options: StateManagerOptions) ``` #### Parameters #### Example ```js const ExampleState = new AsyncStateManager('...') ``` ## Properties ### defaultState {DocConstants.COMMON_DESC_DEFAULT_STATE} ```ts readonly defaultState: State ``` ### name {DocConstants.TSDOC_DESC_OPTIONS_NAME} ```ts readonly name?: string | undefined | null ``` ### isInitializing {DocConstants.TSDOC_DESC_IS_INITIALIZING} ```ts readonly isInitializing: SimpleStateManager ``` ## Methods ### init {DocConstants.TSDOC_METHOD_DESC_INIT} StateManagerInitArgs ```ts init(initFn: (args: StateManagerInitArgs) => void | Promise): Promise ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_INIT} #### Example ```js await ExampleState.init(({ commit }) => { const someValue = localStorage.get('some-value') commit(someValue) }) ``` ### get {DocConstants.TSDOC_METHOD_DESC_GET_ASYNC} ```ts get(): Promise ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_GET_ASYNC} #### Example ```js const state = await ExampleState.get() ``` ### getSync {DocConstants.TSDOC_METHOD_DESC_GET_SYNC} ```ts getSync(): State ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_GET} #### Example ```js const state = ExampleState.getSync() ``` ### set {DocConstants.TSDOC_METHOD_DESC_SET_BY_VALUE} ```ts set(newState: State): Promise ``` {DocConstants.TSDOC_METHOD_DESC_SET_BY_FUNCTION} ```ts set(setStateFn: AsyncSetStateFn): Promise ``` #### Parameters `set` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_SET_ASYNC} #### Example ```js // Assuming that state is of type `number` await ExampleState.set(42) await ExampleState.set((previousState) => previousState + 1) ``` ### reset {DocConstants.TSDOC_METHOD_DESC_RESET} ```ts reset(): Promise ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_RESET_ASYNC} #### Example ```js await ExampleState.reset() ``` ### watch {DocConstants.TSDOC_METHOD_DESC_WATCH} ```ts watch(callback: (state: State) => void): () => void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_WATCH} #### Example ```js const unwatch = ExampleState.watch((state) => { console.log(state) }) // ··· then after some time ··· unwatch() ``` ### unwatchAll {DocConstants.TSDOC_METHOD_DESC_UNWATCH_ALL} ```ts unwatchAll(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_UNWATCH_ALL} #### Example ```js ExampleState.unwatchAll() ``` ### wait {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_VALUE} ```ts wait(expectedValue: State): Promise ``` {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_EVALUATOR} ```ts wait(evaluator: WaitEvaluator): Promise ``` #### Parameters `wait` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_WAIT} #### Example ```js // Promise will resolve when the state value becomes `42`. await ExampleState.wait(42) // Promise will resolve when the function returns `true`. await ExampleState.wait((state) => fulfillsSomeCondition(state)) ``` ### dispose {DocConstants.TSDOC_METHOD_DESC_DISPOSE_STATE_MANAGER} ```ts dispose(): Promise ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_DISPOSE_ASYNC} #### Example ```js await ExampleState.dispose() ``` --- ## EqualityFn {DataType.Function} {DocConstants.TSDOC_TYPE_DESC_EQUALITY_FN} ```ts type EqualityFn = (previousState: State, nextState: State) => boolean ``` --- ## InvalidStateTransitionError ## Overview {DataType.class} {DocConstants.TSDOC_DESC_INVALID_STATE_TRANSITION_ERROR} ```ts export class InvalidStateTransitionError extends Error ``` ### Constructor ```ts constructor(fromState: string, toState: string, stateManager?: string) ``` #### Parameters ### Message Format #### For state manager with name > _Invalid state transition from "`fromState`" to "`toState`" in `stateManager`_ #### For state manager without name > _Invalid state transition from "`fromState`" to "`toState`"_ ### Message Example #### For state manager with name > _Invalid state transition from "A" to "B" in ExampleState_ #### For state manager without name > _Invalid state transition from "A" to "B"_ ## Properties ### fromState {DocConstants.TSDOC_PARAM_DESC_FROM_STATE} ```ts readonly fromState: string ``` ### toState {DocConstants.TSDOC_PARAM_DESC_TO_STATE} ```ts readonly toState: string ``` ### stateManager {DocConstants.TSDOC_PARAM_STATE_MANAGER_NAME} ```ts readonly stateManagerName?: string | undefined | null ``` --- ## ReadOnlyStateManager ## Overview {DataType.type} {DocConstants.TSDOC_TYPE_DESC_READONLY_STATE_MANAGER} ```ts type ReadOnlyStateManager = Pick, 'name' | 'get' | 'watch' | 'wait'> ``` --- ## SetStateFn ## Overview {DataType.type} {DocConstants.TSDOC_PARAM_DESC_SET_FUNCTION} ```ts type SetStateFn = (currentState: State, defaultState: State) => State ``` --- ## SimpleFiniteStateManager DoesNotReturnAnything, DoesNotTakeAnyParameters, Returns, } from '@site/src/components/sentence-templates' {/* Construction formula for this article: Copy everything from SimpleStateManager, then add based on StateManager. */} # SimpleFiniteStateManager ## Overview {DataType.class} {DocConstants.TSDOC_DESC_SIMPLE_FINITE_STATE_MANAGER} ```ts class SimpleFiniteStateManager extends SimpleStateManager ``` ### Constructor ```ts constructor( defaultState: State, allowedStateTransitions: Array<[fromState: State, toState: State]>, options: SimpleStateManagerOptions ) ``` #### Parameters ## Example Consider an example where a playable character in a game: - can stand be standing `idle`, `walking`, or `attacking`; - can only attack only when standing still. ```mermaid stateDiagram-v2 idle --> walking idle --> attacking walking --> idle attacking --> idle ``` This would roughly translate into the following code: ```ts class PlayableCharacter { readonly state = new SimpleFiniteStateManager('idle', [ ['idle', 'walking'], ['idle', 'attacking'], ['walking', 'idle'], ['attacking', 'idle'], ], { name: 'PlayableCharacter', }) } ``` Since the initial state is `'idle'`, changing the state to `'walking'` immediately after instantiating the class would be just fine. ```ts const newCharacter = new PlayableCharacter() newCharacter.set('walking') newCharacter.set('attacking') ``` However, proceeding to change the state from `'walking'` to `'attacking'` would result in the following error: > _InvalidStateTransitionError: from "walking" to "attacking" in PlayableCharacter_ InvalidStateTransitionError ## Properties ### defaultState {DocConstants.COMMON_DESC_DEFAULT_STATE} ```ts readonly defaultState: State ``` ### name {DocConstants.TSDOC_DESC_OPTIONS_NAME} ```ts readonly name?: string | undefined | null ``` ## Methods ### get {DocConstants.TSDOC_METHOD_DESC_GET} ```ts get(): State ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_GET} #### Example ```js const state = ExampleState.get() ``` ### set {DocConstants.TSDOC_METHOD_DESC_SET_BY_VALUE} ```ts set(newState: State): void ``` {DocConstants.TSDOC_METHOD_DESC_SET_BY_FUNCTION} ```ts set(setStateFn: SetStateFn): void ``` #### Parameters `set` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_SET} #### Example ```js ExampleState.set('A') ExampleState.set((previousState) => previousState === 'A' ? 'B' : 'C') ``` ### trySet {DocConstants.TSDOC_METHOD_DESC_TRY_SET_BY_VALUE} ```ts trySet(newState: State): void ``` {DocConstants.TSDOC_METHOD_DESC_TRY_SET_BY_FUNCTION} ```ts trySet(setStateFn: SetStateFn): void ``` #### Parameters `trySet` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_TRY_SET} #### Example ```js ExampleState.trySet('A') ExampleState.trySet((previousState) => previousState === 'A' ? 'B' : 'C') ``` ### reset {DocConstants.TSDOC_METHOD_DESC_RESET} ```ts reset(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_RESET} #### Example ```js ExampleState.reset() ``` ### tryReset {DocConstants.TSDOC_METHOD_DESC_TRY_RESET} ```ts tryReset(): boolean ``` #### Returns {DocConstants.RETURN_DESC_TRY_RESET} #### Parameters #### Example ```js ExampleState.tryReset() ``` ### watch {DocConstants.TSDOC_METHOD_DESC_WATCH} ```ts watch(callback: (state: State) => void): () => void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_WATCH} #### Example ```js const unwatch = ExampleState.watch((state) => { console.log(state) }) // ··· then after some time ··· unwatch() ``` ### unwatchAll {DocConstants.TSDOC_METHOD_DESC_UNWATCH_ALL} ```ts unwatchAll(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_UNWATCH_ALL} #### Example ```js ExampleState.unwatchAll() ``` ### wait {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_VALUE} ```ts wait(expectedValue: State): Promise ``` {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_EVALUATOR} ```ts wait(evaluator: WaitEvaluator): Promise ``` #### Parameters `wait` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_WAIT} #### Example ```js // Promise will resolve when the state value becomes `42`. await ExampleState.wait(42) // Promise will resolve when the function returns `true`. await ExampleState.wait((state) => fulfillsSomeCondition(state)) ``` ### dispose {DocConstants.TSDOC_METHOD_DESC_DISPOSE_STATE_MANAGER} ```ts dispose(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_DISPOSE} #### Example ```js ExampleState.dispose() ``` --- ## SimpleFiniteStateManagerOptions ## Overview {DataType.interface} {DocConstants.TSDOC_DESC_OPTIONS_SIMPLE_FINITE} ## Properties --- ## SimpleStateManager DoesNotReturnAnything, DoesNotTakeAnyParameters, Returns, } from '@site/src/components/sentence-templates' # SimpleStateManager ## Overview {DataType.class} {DocConstants.TSDOC_DESC_SIMPLE_STATE_MANAGER} ```ts class SimpleStateManager ``` ### Constructor ```ts constructor(defaultState: State, options: SimpleStateManagerOptions) ``` #### Parameters #### Example ```js const ExampleState = new SimpleStateManager('...') ``` ## Properties ### defaultState {DocConstants.COMMON_DESC_DEFAULT_STATE} ```ts readonly defaultState: State ``` ### name {DocConstants.TSDOC_DESC_OPTIONS_NAME} ```ts readonly name?: string | undefined | null ``` ## Methods ### get {DocConstants.TSDOC_METHOD_DESC_GET} ```ts get(): State ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_GET} #### Example ```js const state = ExampleState.get() ``` ### set {DocConstants.TSDOC_METHOD_DESC_SET_BY_VALUE} ```ts set(newState: State): void ``` {DocConstants.TSDOC_METHOD_DESC_SET_BY_FUNCTION} ```ts set(setStateFn: SetStateFn): void ``` #### Parameters `set` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_SET} #### Example ```js // Assuming that state is of type `number` ExampleState.set(42) ExampleState.set((previousState) => previousState + 1) ``` ### reset {DocConstants.TSDOC_METHOD_DESC_RESET} ```ts reset(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_RESET} #### Example ```js ExampleState.reset() ``` ### watch {DocConstants.TSDOC_METHOD_DESC_WATCH} ```ts watch(callback: (state: State) => void): () => void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_WATCH} #### Example ```js const unwatch = ExampleState.watch((state) => { console.log(state) }) // ··· then after some time ··· unwatch() ``` ### unwatchAll {DocConstants.TSDOC_METHOD_DESC_UNWATCH_ALL} ```ts unwatchAll(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_UNWATCH_ALL} #### Example ```js ExampleState.unwatchAll() ``` ### wait {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_VALUE} ```ts wait(expectedValue: State): Promise ``` {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_EVALUATOR} ```ts wait(evaluator: WaitEvaluator): Promise ``` #### Parameters `wait` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_WAIT} #### Example ```js // Promise will resolve when the state value becomes `42`. await ExampleState.wait(42) // Promise will resolve when the function returns `true`. await ExampleState.wait((state) => fulfillsSomeCondition(state)) ``` ### dispose {DocConstants.TSDOC_METHOD_DESC_DISPOSE_STATE_MANAGER} ```ts dispose(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_DISPOSE} #### Example ```js ExampleState.dispose() ``` --- ## SimpleStateManagerOptions ## Overview {DataType.interface} {DocConstants.TSDOC_DESC_OPTIONS_SIMPLE} ## Properties --- ## StateManager DoesNotReturnAnything, DoesNotTakeAnyParameters, Returns, } from '@site/src/components/sentence-templates' # StateManager {/* Construction formula for this article: Copy everything from SimpleStateManager, change signature, change `options` param for constructor, then add a section for `init` and mention which properties and methods are inherited */} ## Overview {DataType.class} {DocConstants.TSDOC_DESC_STATE_MANAGER} ```ts class StateManager extends SimpleStateManager ``` ### Constructor ```ts constructor(defaultState: State, options: StateManagerOptions) ``` #### Parameters #### Example ```js const ExampleState = new StateManager('...') ``` ## Properties ### defaultState {DocConstants.COMMON_DESC_DEFAULT_STATE} ```ts readonly defaultState: State ``` ### name {DocConstants.TSDOC_DESC_OPTIONS_NAME} ```ts readonly name?: string | undefined | null ``` ### isInitializing {DocConstants.TSDOC_DESC_IS_INITIALIZING} ```ts readonly isInitializing: SimpleStateManager ``` ## Methods ### init {DocConstants.TSDOC_METHOD_DESC_INIT} StateManagerInitArgs ```ts init(initFn: (args: StateManagerInitArgs) => void | Promise): Promise ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_INIT} #### Example ```js await ExampleState.init(({ commit }) => { const someValue = localStorage.get('some-value') commit(someValue) }) ``` ### get {DocConstants.TSDOC_METHOD_DESC_GET} ```ts get(): State ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_GET} #### Example ```js const state = ExampleState.get() ``` ### set {DocConstants.TSDOC_METHOD_DESC_SET_BY_VALUE} ```ts set(newState: State): void ``` {DocConstants.TSDOC_METHOD_DESC_SET_BY_FUNCTION} ```ts set(setStateFn: SetStateFn): void ``` #### Parameters `set` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_SET} #### Example ```js // Assuming that state is of type `number` ExampleState.set(42) ExampleState.set((previousState) => previousState + 1) ``` ### reset {DocConstants.TSDOC_METHOD_DESC_RESET} ```ts reset(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_RESET} #### Example ```js ExampleState.reset() ``` ### watch {DocConstants.TSDOC_METHOD_DESC_WATCH} ```ts watch(callback: (state: State) => void): () => void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_WATCH} #### Example ```js const unwatch = ExampleState.watch((state) => { console.log(state) }) // ··· then after some time ··· unwatch() ``` ### unwatchAll {DocConstants.TSDOC_METHOD_DESC_UNWATCH_ALL} ```ts unwatchAll(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_UNWATCH_ALL} #### Example ```js ExampleState.unwatchAll() ``` ### wait {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_VALUE} ```ts wait(expectedValue: State): Promise ``` {DocConstants.TSDOC_METHOD_DESC_WAIT_BY_EVALUATOR} ```ts wait(evaluator: WaitEvaluator): Promise ``` #### Parameters `wait` takes either one of the following parameters: #### Returns {DocConstants.RETURN_DESC_WAIT} #### Example ```js // Promise will resolve when the state value becomes `42`. await ExampleState.wait(42) // Promise will resolve when the function returns `true`. await ExampleState.wait((state) => fulfillsSomeCondition(state)) ``` ### dispose {DocConstants.TSDOC_METHOD_DESC_DISPOSE_STATE_MANAGER} ```ts dispose(): void ``` #### Parameters #### Returns {DocConstants.RETURN_DESC_DISPOSE} #### Example ```js ExampleState.dispose() ``` --- ## StateManagerDidSetArgs ## Overview {DataType.interface} {DocConstants.TSDOC_DESC_STATE_MANAGER_DID_SET_ARGS} ## Properties --- ## StateManagerInitArgs ## Overview {DataType.interface} {DocConstants.TSDOC_DESC_STATE_MANAGER_INIT_ARGS} ## Properties --- ## StateManagerLifecycle ## Overview {DataType.interface} {DocConstants.TSDOC_DESC_OPTIONS_LIFECYCLE} ## Properties --- ## StateManagerOptions ## Overview {DataType.interface} {DocConstants.TSDOC_DESC_OPTIONS_STANDARD} ## Properties --- ## StateManagerVisibility :::danger[Deprecation notice] This is not a reliable way to hide sensitive values. While this works for React Dev Tools, the built-in memory inspector in most browsers will still be able to show the value anyway. Please consider storing sensitive information on the server and only expose what's absolutely necessary to the client instead. Starting from version `2.0.0`, this option will be completely ignored. ::: ## Overview {DataType.enum} {DocConstants.TSDOC_DESC_OPTIONS_VISIBILITY_DETAILED} This can be specified in the [`options`](./StateManagerOptions.mdx) parameter in the constructor of the State Manager: ```js const ExampleState = new StateManager('Hello, world!', { visibility: StateManagerVisibility.HIDDEN, }) ``` ## Values | Key | Value | Description | | ------------- | :---: | -------------------------------------------------------- | | `ENVIRONMENT` | `0` | {DocConstants.DESC_STATE_MANAGER_VISIBILITY_ENVIRONMENT} | | `EXPOSED` | `1` | {DocConstants.DESC_STATE_MANAGER_VISIBILITY_EXPOSED} | | `HIDDEN` | `2` | {DocConstants.DESC_STATE_MANAGER_VISIBILITY_HIDDEN} | --- ## StateSelector ## Overview {DataType.type} {DocConstants.TSDOC_TYPE_DESC_STATE_SELECTOR} ```ts type StateSelector = (state: State) => SelectedState ``` --- ## StateTransition ## Overview {DataType.type} {DocConstants.TSDOC_TYPE_DESC_STATE_TRANSITION} ```ts type StateTransition = [fromState: State, toState: State] ``` --- ## WaitEvaluator ## Overview {DataType.type} {DocConstants.TSDOC_TYPE_DESC_WAIT_EVALUATOR} ```ts type WaitEvaluator = (currentState: State, defaultState: State) => boolean ``` --- ## BUILD_HASH {DataType.string} {DocConstants.TSDOC_DESC_BUILD_HASH} This constant is available in {DocConstants.CORE_PACKAGE_NAME} and {DocConstants.REACT_PACKAGE_NAME}. --- ## BUILD_TYPE {DataType.BuildType} {DocConstants.TSDOC_DESC_BUILD_TYPE} This constant is available in {DocConstants.CORE_PACKAGE_NAME} and {DocConstants.REACT_PACKAGE_NAME}. --- ## BuildType {DataType.enum} {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE} This enum is available in {DocConstants.CORE_PACKAGE_NAME} and {DocConstants.REACT_PACKAGE_NAME}. ## Values | Key | Value | Description | | --------- | :---------: | ------------------------------------------------- | | `CJS` | `'CJS'` | {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE_CJS} | | `ES` | `'ES'` | {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE_ES} | | `MJS` | `'MJS'` | {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE_MJS} | | `RN` | `'RN'` | {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE_RN} | | `UMD` | `'UMD'` | {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE_UMD} | | `UMD_MIN` | `'UMD_MIN'` | {DocConstants.TSDOC_DESC_ENUM_BUILD_TYPE_UMD_MIN} | --- ## VERSION {DataType.string} {DocConstants.TSDOC_DESC_VERSION} This constant is available in {DocConstants.CORE_PACKAGE_NAME} and {DocConstants.REACT_PACKAGE_NAME}. --- ## useSimpleStateValue ## Overview {DataType.Function} {DocConstants.TSDOC_DESC_USE_SIMPLE_STATE_VALUE} ```js const state = useSimpleStateValue(stateManager) ```
Overloads (+2) <> ```js const state = useSimpleStateValue(stateManager, selector) ``` ```js const state = useSimpleStateValue(stateManager, selector, active) ```
## Parameters ## Examples ### Basic ### With selector ### Conditionally watch for changes --- ## useSimpleStateValueOnly {DataType.Function} {DocConstants.TSDOC_DESC_USE_SIMPLE_STATE_VALUE_ONLY} ```js const state = useSimpleStateValueOnly(stateManager) ``` ## Parameters ## Example --- ## useStateValue ## Overview {DataType.Function} {DocConstants.TSDOC_DESC_USE_STATE_VALUE} ```js const state = useStateValue(stateManager) ```
Overloads (+4) <> ```js const state = useStateValue(stateManager, selector) ``` ```js const state = useStateValue(stateManager, selector, active) ``` ```js const state = useStateValue(stateManager, selector, equalityFn) ``` ```js const state = useStateValue(stateManager, selector, equalityFn, active) ```
## Parameters ## Examples ### Basic ### With selector ### Custom equality checking ### Conditionally watch for changes --- ## V0.X.X ## v0.14.0 ### cotton-box * `ReadOnlyStateManager` type now exposes the `name` property. ### cotton-box-react * Fixed a bug where the error below might be thrown under some circumstances: > Cannot use 'in' operator to search for 'name' in undefined * `useSimpleStateValueWithReactiveSelector` and `useStateValueWithReactiveSelector` has been marked as deprecated and will most likely be removed in the next major version (v0 -> v1). If you use this hook and find a valid use case, please [create an issue](https://github.com/glyph-cat/cotton-box/issues/new/choose) to explain your use case. ## v0.13.1 ### cotton-box * Internal code optimizations with TypeScript strict mode enabled. ### cotton-box-react * Internal code optimizations with TypeScript strict mode enabled. ## v0.13.0 ### cotton-box * N/A ### cotton-box-react * Added missing export. ## v0.12.0 ### cotton-box * Added experimental hydration API for when server-side rendering is involved. ### cotton-box-react * Added experimental hydration API for when server-side rendering is involved. ## v0.11.0 ### cotton-box * Fixed incorrect type definition for `trySet` ### cotton-box-react * N/A ## v0.10.0 ### cotton-box * Added `trySet` method to `SimpleFiniteStateManager`. * Parameter `stateManagerName` of `InvalidStateTransitionError` has been renamed to `stateManager` for brevity. * Constructor parameters of `InvalidStateTransitionError` (`fromState`, `toState`, `stateManager`) are now also available as public readonly properties. ### cotton-box-react * N/A ## v0.9.0 ### cotton-box * Fixed a bug where `SimpleFiniteStateManager` would throw "TypeError: Cannot read properties of undefined (reading 'has')" when setting state under certain circumstances. * The constructor of `InvalidStateTransitionError` now accepts a third optional parameter `stateManagerName`. * `Watcher.refresh` has now been renamed to `.post`. The `.refresh` method is still available but will be removed in a future version. ### cotton-box-react * N/A ## v0.8.0 ### cotton-box * Added helper type `StateTransition`. ### cotton-box-react * Updated type definitions for hooks to accept `SimpleFiniteStateManager` and `ReadonlyStateManager`. ## v0.7.0 ### cotton-box * Added `SimpleFiniteStateManager`, a sub-class of `SimpleStateManager` that only allows state change according to a set of predefined state transitions. ### cotton-box-react * N/A ## v0.6.0 ### cotton-box * The `Equality` API has been removed — it now exists as a separate package: [`@glyph-cat/equality`](https://github.com/glyph-cat/swiss-army-knife/tree/main/src/packages/equality). ### cotton-box-react * N/A ## v0.5.0 ### cotton-box * N/A ### cotton-box-react * Updated post-install script. * Removed Scope-related APIs (`StateManagerScopeProviderProps`, `StateManagerScopeProvider`, `useScoped`) to reduce complexity of the library. ## v0.4.0 ### cotton-box * The `Watcher` that Cotton Box uses internally is now exposed as a public API. * Internal stability enhancements. ### cotton-box-react * Internal stability enhancements. ## v0.3.0 ### cotton-box * Fixed incorrect export name for UMD builds. * Events captured by `.watch` now includes `StateChangeEventType` as the second parameter. * Events captured by `.wait` now includes `StateChangeEventType` as the third parameter. * Type safety enhancements. * Internal optimizations. ### cotton-box-react * Fixed incorrect export name for UMD builds. * Fixed a bug where error "Cannot read properties of null (reading 'name')" would be thrown in production. * Internal optimizations. ## v0.2.0 ### cotton-box * The `isInitializing` property of `StateManager` and `AsyncStateManager` is now exposed as a `ReadOnlyStateManager`. * Removed method `.waitForInit` in favor of calling `….isInitializing.wait(false)` directly. * State Managers without names will be assigned automatic names (`UnnamedStateManager_001`, `UnnamedStateManager_002`, and so on). ### cotton-box-react `useInitState` has been removed in favor of a more direct way to find out if a State Manager is initializing: ```diff - const isInitializing = useInitState(ExampleState) + const isInitializing = useSimpleStateValue(ExampleState.isInitializing) ``` The way `useInitState` works internally is also similar to calling `useSimpleStateValue`. By having one less hook means the codebase will have less complexity and becomes easier to maintain. On top of that, now that `isInitializing` is exposed as a State Manager itself, this means it is also possible to watch for the changes and even use it with other UI libraries/frameworks such as Vue and Angular with significantly less friction. ## v0.1.0 ### cotton-box * The `wait` method now will only wait for the state to match or evaluator to return true, without checking if the state is currently initializing. * Added new method `waitForInit` to `StateManager` and `AsyncStateManager` * Added new method `reinitialize` to `StateManager` and `AsyncStateManager` * `currentState` is now exposed in the `lifecycle.init` callback * New property `isInitializing` exposed for `StateManager` and `AsyncStateManager` ### cotton-box-react * Added new hook `useInitState` ## v0.0.2 ### cotton-box * Stability enhancements. ### cotton-box-react * Passing `StateManager` and `AsyncStateManager` to `useSimpleStateValue` and `useSimpleStateValueWithReactiveSelector` will *not* cause an error to be thrown. This is the same behavior as v0.0.0. Instead, rely on TypeScript's type-checking to prevent passing the wrong State Manager type. This change is made to reduce some runtime overhead in the hooks by not having to validate the State Manager types. ## v0.0.1 ### cotton-box * Fixed a bug where the `didReset` lifecycle hook is called during initialization via `commit`. ### cotton-box-react * Passing `StateManager` and `AsyncStateManager` to `useSimpleStateValue` and `useSimpleStateValueWithReactiveSelector` will now cause an error to be thrown. ## v0.0.0 * Initial release / pre-release --- ## V1.X.X ## v1.0.0 ### cotton-box - This package now has the following external dependencies: - [`@glyph-cat/foundation`](https://www.npmjs.com/package/@glyph-cat/foundation) - [`@glyph-cat/type-checking`](https://www.npmjs.com/package/@glyph-cat/type-checking) - Fixed `README.md` now displays the correct description: _"This is the core package of cotton-box."_ instead of _"This is the official React bindings for cotton-box."_ - `Watcher` has been moved back to [`@glyph-cat/swiss-army-knife`](https://github.com/glyph-cat/swiss-army-knife) - An internal implementation of "Watcher" still remains, but it is optimized for Cotton Box only (behavior and API can change suddenly), so it no longer makes sense to expose the `Watcher` API from this package. - `.watch` and `.wait` no longer include event type. - State manager names remain optional, but if not provided, automatic names will no longer be generated either. - Internal performance optimizations. ### cotton-box-react - This package now has the following external dependencies: - [`@glyph-cat/foundation`](https://www.npmjs.com/package/@glyph-cat/foundation) - [`@glyph-cat/type-checking`](https://www.npmjs.com/package/@glyph-cat/type-checking) - This package now requires React 19 (or above) to work - There is now a bundle specifically for React Native that excludes SSR logic which is only used for web (dead code in the case of React Native). - No changes are required to take advantage of this optimization - Web compilers (eg: babel, webpack) and React Native bundler (Metro) will be able to automatically select the correct bundle when compiling. - Added new hook `useSimpleStateValueOnly`. It is similar to `useSimpleStateValue` but does not support any additional options or selectors. - `useSimpleStateValueOnly` and `useSimpleStateValue` returns the `never` type when used with `StateManager` or `AsyncStateManager`. They are designed to be used with `SimpleStateManager` or `SimpleFiniteStateManager` only. - Removed previously deprecated APIs: - `useSimpleStateValueWithReactiveSelector` - `useStateValueWithReactiveSelector` - Internal performance optimizations. --- ## V2.X.X ## v2.0.0 ### cotton-box #### 🔥 Breaking Changes - Minimum node version required is **24**. - UNPKG link has changed: ```diff - https://unpkg.com/cotton-box@2.0.0/lib/types/index.d.ts + https://unpkg.com/cotton-box@2.0.0/dist/types/index.d.ts ``` #### Other Changes - `options.visibility` has been deprecated. - It is not a reliable way to hide sensitive values. - While this works for React Dev Tools, the built-in memory inspector in most browsers will still be able to show the value anyway. - Please consider storing sensitive information on the server and only expose what's absolutely necessary to the client instead. - `SimpleFiniteStateManager` will now show a warning when calling `.reset()` if there is no valid state transition from the current state to the default state. Starting from v3, an `InvalidStateTransitionError` will be thrown. - `SimpleFiniteStateManager` now has a new `.tryReset()` method. - Examples have been added to every API via the `@example` TSDoc tag. - Internal performance optimizations. ### cotton-box-react #### 🔥 Breaking Changes - Minimum node version required is **24**. - UNPKG link has changed: ```diff - https://unpkg.com/cotton-box-react@2.0.0/lib/types/index.d.ts + https://unpkg.com/cotton-box-react@2.0.0/dist/types/index.d.ts ``` #### Other Changes - `options.visibility` now no longer does anything in React Developer Tools. - Examples have been added to every API via the `@example` TSDoc tag. - Internal performance optimizations. --- ## Tic Tac Toe This is a game of tic-tac-toe that uses a class to manage the game logic instead of writing it all with React Hooks. State managers are declared as read-only class properties and manage a very narrow scope of logic. React components that need to subscribe to state changes would only have to call the hook corresponding to the state manager class (in this case: `useSimpleStateValue`) with whichever class property of `GameInstance` that has the relevant logic. --- ## Video Camera This is an adapted implementation of [`VideoCamera`](https://github.com/glyph-cat/swiss-army-knife/blob/main/src/packages/core/src/media/video-camera/index.ts) from `@glyph-cat/swiss-army-knife`. --- ## Counter In this example, a counter value is stored in a state manager. When the ring is clicked the `.set` method will be called to increase the counter by 1. When the "Reset" button is clicked, the `.reset` method will be called to reset `CounterState` back to `0`. --- ## Notes App This is an example of a simple notes app that uses the spread operator (`...`) to set state. --- ## Todo List App This is an example of a to-do list app that uses [Immer](https://immerjs.github.io/immer/) for immutability. The `` component also demonstrates the usage of [selectors](../../learn/react/selectors.mdx). --- ## Stress Test The sandboxes below show how many operations per second each library is capable of. How the test is conducted: 1. While `performance.now() - startTime` is less than 1 second, keep running. 2. After loop has completed, calculate total run time as `performance.now() - startTime` because it is not guaranteed that the loop stops perfectly after one second. 3. Operations per second = total operations recorded ÷ total run time × 1000 milliseconds Upon clicking "Start", the sandbox will try to invoke as many set-state calls as possible for 1 second. ## React (built-in state) # Cotton Box # Zustand # Redux --- ## Migration From `react-relink` {/* NOTE: sidebar position is not specified, this should be the last article in the sidebar. */} :::info The usage of `.get`, `.set`, `.reset`, and `.dispose` remains the same. ::: ## Setup ### Before npm install react-relink ### Now npm install {DocConstants.CORE_PACKAGE_NAME} {DocConstants.REACT_PACKAGE_NAME} ## Instantiation ```js title="Before" const ExampleSource = new RelinkSource({ key: 'example-source', default: 'defaultState', lifecycle: { init({ commit, commitNoop, defaultState }) { // ... }, didSet({ state }) { // ... }, didReset() { // ... }, }, options: { suspense: true | false, public: true | false, }, scope: OtherSource, }) ``` ```js title="Now" // highlight-next-line // highlight-start // Default state is passed as first parameter, // remaining options are passed as the second. const ExampleState = new AsyncStateManager('defaultState', { // `key` is no longer needed, but there is a `name` property, // which is *optional* and only used for debugging. // highlight-end lifecycle: { init({ commit, commitNoop, defaultState }) { // ... }, // highlight-start // `defaultState` is exposed here: didSet({ state, defaultState }) { // highlight-end // ... }, didReset() { // ... }, }, // highlight-start // `visibility` is a boolean type in react-relink: visibility: StateManagerVisibility.ENVIRONMENT, // highlight-end suspense: true | false, // highlight-start // Scope is not supported by Cotton Box // highlight-end }) ``` ## Consuming the State ```js title="Before" function App() { const state = useRelinkValue(ExampleSource) return '...' } ``` ```js title="Now" function App() { // highlight-next-line // highlight-next-line const state = useStateValue(ExampleState) return '...' } ``` ## Handling dependencies ```js title="Before" const ExampleSourceA = new RelinkSource({ key: 'example-source-a', default: '...', lifecycle: { /* ... */ }, }) const ExampleSourceB = new RelinkSource({ key: 'example-source-b', default: '...', deps: [ExampleSourceA], lifecycle: { /* ... */ }, }) ``` ```js title="Now" const ExampleStateA = new StateManager('...') const ExampleStateB = new StateManager('...', { lifecycle: { init() { await ExampleStateA.isInitializing.wait(false) // continue to do something }, }, }) ``` ## Re-initialization ```js title="Before" MySource.hydrate(...) ``` ```js title="Now" ExampleState.init(...) ``` ## Scope Cotton Box does not have Scope-related APIs to reduce complexity. Instead, we recommend using [React's Context API](https://react.dev/learn/passing-data-deeply-with-context) as a substitute as it allows more fine-grained control at the same time. --- ## Observing Initialization State To determine if a state is initializing, we can use the `.isInitializing` property. It is a ReadOnlyStateManager where only the `.get`, `.watch` and `.wait` methods are available. In React, we would useSimpleStateValue to subscribe to the changes of this read-only State Manager as demonstrated in the example below: ## Example --- ## Opt Out of Re-renders Due To State Change Sometimes, we might want to temporarily unsubscribe from state changes in a component. This can be done by passing a third parameter. If a selector is not needed, `null` can be passed as the second parameter. ```jsx function App(): ReactNode { const [active, setActive] = useState(true) const state = useStateValue(ExampleState, null, active) return '...' } ``` The third parameter is a `boolean` flag that defaults to `true`. When set to `true`, the hook will observe state changes normally, when set to `false`, it will stop observing for state changes until it is set back to `true` again. :::note Setting the third parameter to `false` does not prevent the State Manager itself from changing state, but rather, it is just blocking the hook from watching for state changes. ::: ## Full Example --- ## Quick Start Let's begin by importing useStateValue from `'cotton-box-react'`. ```js ``` Then, we can pass our State Manager to the hook. ```js const state = useStateValue(ExampleState) ``` The example below shows how we can read from `CounterState` from inside a React component. --- ## Selectors Sometimes, our state might be an object and we only need to the value of certain properties from that object, but when any property is changed, the entire object changes, resulting in components to re-render. This is when selectors can help us overcome this problem. Take `IUserState` for example, we might have a component that is only responsible for showing `luckyNumber` and we do not want it to re-render when `firstName` changes. ```ts interface IUserState { firstName: string lastName: string luckyNumber: number } const UserState = new StateManager({ firstName: 'John', lastName: 'Smith', luckyNumber: 42, }) ``` We can pass `(state) => state.luckyNumber` as the second parameter to useStateValue. ```tsx const luckyNumber = useStateValue(UserState, (state) => state.luckyNumber) ``` By doing so, only the previous and upcoming values of `state.luckyNumber` is compared when the state changes. Therefore, the component will only re-render when `luckyNumber` changes. ## Example In this example, we can confirm that the component does not re-render by verifying that `console.log('Rendering...')` is not executed no matter how many times the value of `firstName` changes, but whenever `luckyNumber` changes, it will be executed. --- ## Setup :::info React **19** (or above) is required. ::: In order to use `cotton-box` with React, we need to install `cotton-box-react`. ## Installation ### Through dependency manager (recommended) npm install {DocConstants.CORE_PACKAGE_NAME} // highlight-next-line npm install {DocConstants.REACT_PACKAGE_NAME} ### With UNPKG #### For development ```html ``` #### For production ```html ``` Then replace `` with the version that you need. --- ## Disposal ## General Usage To dispose a State Manager, we use the `.dispose` method. This is important when the State Manager is not created/used globally. For example, we can have a `GameRoom` class in which each instance has a state. Upon cleaning up the `GameRoom`, we should also dispose the State Manager. ```tsx enum GameRoomState { CREATED, INITIALIZED, STARTED, STOPPED, DISPOSED, } class GameRoom { state = new StateManager(GameRoomState.CREATED) async dispose(): Promise { await this.state.wait((state) => { return state >= GameRoomState.INITIALIZED }) // ^ Pro tip: Wait for initialization to complete before performing teardown this.state.set(GameRoomState.DISPOSED) this.state.dispose() } } ``` ```jsx const GameRoomState = Object.freeze({ CREATED: 0, INITIALIZED: 1, STARTED: 2, STOPPED: 3, DISPOSED: 4, }) class GameRoom { state = new StateManager(GameRoomState.CREATED) async dispose() { await this.state.wait((state) => { return state >= GameRoomState.INITIALIZED }) // ^ Pro tip: Wait for initialization to complete before performing teardown this.state.set(GameRoomState.DISPOSED) this.state.dispose() } } ``` ## Using with React To recap, State Managers are designed to be used globally most of the time, but there are niche cases where they need to be dynamically created (and disposed). One such example is to dynamically create them within React components. In React, we can ["lazily initialize"](https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents) the variables for useRef and State Managers can be instantiated in a similar way too. ```tsx function App(): ReactNode { const ExampleState = useRef>() if (!ExampleState.current) { ExampleState.current = new StateManager('...') } useEffect(() => { return () => { ExampleState.current.dispose() } }, []) console.log(ExampleState.current.get()) return '...' } ``` ```jsx function App() { const ExampleState = useRef(null) if (!ExampleState.current) { ExampleState.current = new StateManager('...') } useEffect(() => { return () => { ExampleState.current.dispose() } }, []) console.log(ExampleState.current.get()) return '...' } ``` However, it can be tricky ([#14490](https://github.com/facebook/react/issues/14490), [#26315](https://github.com/facebook/react/issues/26315), [#27735](https://github.com/facebook/react/issues/27735)) to do this in StrictMode due to how the effects are being fired: 1. Setup effect is called for the first time. 2. Setup effect is called for the second time. 3. Cleanup effect is called for the first time. 4. When component finally unmounts, cleanup effect is called for the second time. Which translates into: 1. Variable is instantiated for the first time and assigned to `useRef`. 2. Variable is instantiated for the _**second**_ time and _**overwrites**_ the _**first**_ value that is already assigned to `useRef`. (You would think `ExampleState.current` already has a value at this point, but for some reason, it is in fact still `null`). 3. Cleanup effect is run for the _**first**_ time on the variable assigned to `useRef` (which is the _**second**_ instance). 4. When component finally unmounts, the cleanup effect is run for the second time on whatever value that `useRef` may still be holding. There are a few workarounds for this problem, but at a higher level, this means that we are in some way still writing "unsafe" code — code that might lead to bugs in future versions of React or when we begin to adopt certain new features of React in the future. Instead of (1)setup -> (2)cleanup -> (3)setup -> (4)cleanup, `StrictMode` triggers (1)setup -> (2)setup -> (3)cleanup -> (4)cleanup. For now, we can think about it this way: it is possible for the same component to be rendered more than once before the first instance even has the chance to cleanup. Even if we intend for a component to only have one rendered instance at a time throughout the entire app, in reality, there is no guarantee that it would be rendered that way, and `StrictMode` helps to simulate such conditions which allows us to identify problems that may occur when components are rendered in such a way. It seems like the only safe and stable way so far is to perform both the instantiation and cleanup in the same effect. The downside to this is that the lazily initialized values will not be accessible in the first render and we will need to implement some sort of fallback UI or perform actions that are related to the lazily initialized value only when it becomes available. ```tsx function App(): ReactNode { const [ExampleState, setExampleState] = useState>(null) useEffect(() => { const newExampleState = new StateManager('...') setExampleState(newExampleState) return () => { newExampleState.dispose() setExampleState(null) } }, []) if (!ExampleState) { return <>Loading... } console.log(ExampleState.get()) return '...' } ``` ```jsx function App() { const [ExampleState, setExampleState] = useState(null) useEffect(() => { const newExampleState = new StateManager('...') setExampleState(newExampleState) return () => { newExampleState.dispose() setExampleState(null) } }, []) if (!ExampleState) { return <>Loading... } console.log(ExampleState.get()) return '...' } ``` --- ## Persisting State [`StateManager`](../../api/core/StateManager.mdx) and [`AsyncStateManager`](../../api/core/AsyncStateManager.mdx) provides three lifecycle hooks: - `init` — triggers when the State Manager is instantiated - `didSet` — triggers each time the state is changed with `.set`, even if the value remains the same - `didReset` — triggers each time `.reset` is called :::tip These lifecycle hooks are are independent of each other and completely optional, but as far as lifecycle management is concerned, it is generally recommended to use all three. ::: ## Persisting & resetting state To begin, we can start by defining `didSet` and `didReset`. By doing so, we would have a better idea of how the state is being persisted, which in return makes it easier for us to figure out how to initialize the state. In this example, we will serialize the state using JSON.stringify and save it to localStorage. ```js const UserState = new StateManager({ firstName: 'John', lastName: 'Smith', luckyNumber: 42, }, { // highlight-start lifecycle: { didSet({ state, defaultState }) { localStorage.removeItem('key', JSON.stringify(state)) }, didReset() { localStorage.removeItem('key') }, }, // highlight-end }) ``` :::danger[Caution!] Do not call `.set` inside the `didSet` lifecycle hook as this will result in infinite recursion. The same applies to calling `.reset` in the `didReset` lifecycle hook and calling `.init` in the `init` lifecycle hook. ::: ## Retrieving state Now that we know how the state is being persisted, all we need to do is to parse it back with JSON.parse in the `init` lifecycle hook. ```js const UserState = new StateManager({ firstName: 'John', lastName: 'Smith', luckyNumber: 42, }, { lifecycle: { // highlight-start init({ commit }) { const persistedState = JSON.parse(localStorage.getItem('key')) commit(persistedState) }, // highlight-end didSet({ state, defaultState }) { localStorage.removeItem('key', JSON.stringify(state)) }, didReset() { localStorage.removeItem('key') }, }, }) ``` ## Putting everything together The code in the section above is a simple example of we can persist data and use it to initialize a State Manager. In reality, however, it could be a little bit more complicated. The code below shows what it might look like in a more realistic scenario. ```js const UserState = new StateManager({ firstName: 'John', lastName: 'Smith', luckyNumber: 42, }, { // 0. Do not run any lifecycle hooks in server environment // (for projects that uses server-side rendering) lifecycle: typeof window === 'undefined' ? {} : { init({ commit, commitNoop, defaultState }) { // 1. Get raw (string) state. const rawState = localStorage.getItem('key') if (rawState) { try { // 2. If it exists, attempt to parse it. const persistedState = JSON.parse(rawState) // 3. Commit the state by merging the default and the persisted states // using the spread operator. commit({ ...defaultState, ...persistedState, }) // 4. Initialization has completed, // execution of the function can be ended early. return } catch (error) { // 5. try-catch is used in case of corrupted JSON string. console.error(error) } } commitNoop() // pronounced as "commit no-op" // 6. This indicates that initialization has been completed but without // the need to change the state (state remains to be the default value). }, didSet({ state, defaultState }) { localStorage.removeItem('key', JSON.stringify(state)) }, didReset() { localStorage.removeItem('key') }, }, }) ``` --- ## Quick Start(Tutorial) ## Creating a new State Manager First, import [`StateManager`](../../api/core/StateManager.mdx) from {DocConstants.CORE_PACKAGE_NAME}: ```js ``` The `StateManager` is a [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class). We can create a new `StateManager` like this: ```ts interface IUserState { firstName: string lastName: string luckyNumber: number } const UserState = new StateManager({ firstName: 'John', lastName: 'Smith', luckyNumber: 42, }) ``` ```js const UserState = new StateManager({ firstName: 'John', lastName: 'Smith', luckyNumber: 42, }) ``` ## Getting the state To get the current state, use the [`.get`](../../api/core/StateManager.mdx#get) method. ```js const user = UserState.get() ``` To use the State Manager with React, we can import the [`useStateValue`](../../api/react/useStateValue.mdx) hook and use it like this: ```js function App() { const user = useStateValue(UserState) // ... } ``` More on [using with React](../react/setup.mdx). ## Setting the state With a value: ```js UserState.set(64) ``` With a function: ```js UserState.set((state) => ({ ...state, // <-- preserve other state properties luckyNumber: Math.round(Math.random() * 100), // ^ while only changing the value of `luckyNumber` })) ``` ## Resetting the state ```js UserState.reset() ``` :::tip It is safe to call all of the State Manager's methods (`.get`, `.set`, `.reset`, etc.) anywhere, both inside and outside of React components. ::: --- ## Setup(Tutorial) ## Installation ### Through dependency manager (recommended) npm install {DocConstants.CORE_PACKAGE_NAME} ### With UNPKG #### For development ```html ``` #### For production ```html ``` Then replace `` with the version that you need. --- ## Waiting For State Change Waiting is an observation strategy that utilizes `.watch` under the hood. It allows us to watch for state value changes until it matches a certain value or fulfils a certain condition then stops watching and returns a snapshot of that state. Consider the code below, there are two things that we can do with it. ```js const CounterState = new StateManager(0) setInterval(() => { const randomNumber = Math.round(Math.random() * 100) CounterState.set(randomNumber) }, 1000) ``` ## Waiting for a exact value We can pass a value to `.wait` and it will resolve when the state matches that value. The state is compared with the expected value using Object.is. In this example, the promise will resolve when the state equals to `42`. ```js await CounterState.wait(42) ``` :::tip The `.isInitializing` property of StateManager and AsyncStateManager can be used in a similar way to wait for initialization to complete (Example: `ExampleState.isInitializing.wait(false)`). ::: ## Waiting for a condition to be fulfilled We can also pass a callback to `.wait` to fine tune the waiting behavior. The state snapshot will be passed to the function each time the state changes and the callback should return either `true` or `false`. In this example, the promise will resolve when the state is a number that is divisible by 5. ```js await CounterState.wait(state => state % 5 === 0) ``` :::tip When the condition is fulfilled, the `.wait` method returns a Promise that resolves into a snapshot of the state that fulfilled the condition. This is useful when we only know what condition to expect but not a specific value, as we could capture the value right after waiting. ::: --- ## Watching For State Changes We can observe state changes with the `.watch` method. It accepts a callback that will be invoked whenever the state changes. It also returns a callback, that when called, will unsubscribe the watch handler from future state changes. ## Example In the example below, we have an interval that sets the `CounterState` with random numbers every second. A watcher is added to observe these changes and it will push the state values into an array. Once the array has 10 numbers, the unwatch function is called to stop watching. --- ## Which State Manager Should I Use? The package comes with three types of State Managers, each with slightly different characteristics, tailored for different use cases. | | `SimpleStateManager` | `StateManager` | `AsyncStateManager` | | ----------------------------------------- | :------------------: | :------------------: | :-----------------: | | Inherits from | -/- | `SimpleStateManager` | `StateManager` | | Async set-state | No | No | Yes | | `get`/`set`/`reset`/`dispose` methods | Synchronous | Synchronous | Asynchronous | | Lifecycle hooks | No | Yes | Yes | | Can hide values from [React Dev Tools][1] | No | Yes | Yes | | [Computing overhead][2] | Very low | Low | High | ## `SimpleStateManager` - A bare-bones state manager. - Very fast and lightweight. - No lifecycle management. - State values cannot be hidden from React Developer Tools. - When `.set` calls are nested, only the outermost one will be effective. The nested `.set` calls will only result in a momentary state change and get overwritten by its outer `.set` calls right after. - Has the least flexibility and number of features, and is unlikely to have new features in future updates. - Suitable for general use cases (UIs, forms, lists, etc.) that do not require persisting state or hiding state values from React Developer Tools. - Go to [API reference](../../api/core/SimpleStateManager.mdx). ## `StateManager` - Inherits everything from `SimpleStateManager`. - Slightly slower than `SimpleStateManager`, but still responsive enough to accommodate scenarios that involve rapid key presses and mouse events. - Supports lifecycle hooks. - State values can be hidden from React Developer Tools. - When `.set` calls are nested, they will be triggered in order, from outermost to innermost. - Has more flexibility over `SimpleStateManager` and is more likely to be updated to work with new external features where it makes sense to integrate them. (For example, new features from libraries such as React or browser APIs.) - Suitable for general use cases (UIs, forms, lists, etc.). This is the default recommendation. - Go to [API reference](../../api/core/StateManager.mdx). ## `AsyncStateManager` - Similar to `StateManager` except `.get`, `.set`, `.reset`, and `.dispose` operations are asynchronous. - Slower than `StateManager` since it needs to accommodate asynchronous set-state functions. - Not suitable for scenarios that involve rapid state changes as it may result in noticeable lag. - Only use this as an escape hatch if you need to call asynchronous functions inside the set-state function for the `.set` method. `AsyncStateManager` will wait for these actions to be completed before setting the state. - Example: calling a REST API that requires awareness about the previous state in order to derive the next state. - Go to [API reference](../../api/core/AsyncStateManager.mdx). :::info It is strongly advised to move async logic out of `.set` calls whenever possible and only use `AsyncStateManager` as a last resort. ::: ## `SimpleFiniteStateManager` ✨ - Special state manager that also inherits everything from `SimpleStateManager`. - Behaves similarly to `SimpleStateManager` and has similar computing overhead. - State changes are only allowed based on predetermined state transitions. - Go to [API reference](../../api/core/SimpleFiniteStateManager.mdx). {/* #region Links */} [1]: https://react.dev/learn/react-developer-tools [2]: https://en.wikipedia.org/wiki/Overhead_(computing) {/* #endregion Links */} --- ## Reincarnation of an old package In 2020, [`react-relink`](https://github.com/glyph-cat/react-relink) was released. It was heavily inspired by [`recoil`](https://github.com/facebookexperimental/Recoil) and had a similar API design. `react-relink` was created with the goals of: 1. not needing to wrap components in [Providers](https://react.dev/reference/react/createContext#provider) while still allowing states to be shared across different React components trees, and 2. make it easy to transition from `recoil` by introducing similar concepts and APIs. As time goes on, the concepts of `recoil` were slowly disregarded, new features were added, and `react-relink` began to evolve into a state management library of it's own. At that point, there were three main problems... - The codebase became bloated and difficult to maintain - It became so slow, that in some scenarios, it results in very noticeable lagging - There was no easy way to use `react-relink` as a state management tool outside of React because the dependencies are tied together. Eventually, the better solution was to create ~~a new package~~ two new packages — a core package and another one for its React bindings. This allows non-React projects to use this new state management library without needing to install [`react`](https://www.npmjs.com/package/react) as a dependency, and the core package is called `cotton-box`. `cotton-box` exposes three different types of State Managers that are built on top of one another for different needs: - `SimpleStateManager` — the most lightweight state manager - `StateManager` - generic state manager with lifecycle hooks for persisting data - `AsyncStateManager` - similar to `StateManager` but supports async set-state functions More information on the differences between these State Managers are available [here](/cotton-box/docs/learn/tutorial/which-state-manager-should-i-use). If you're migrating from `react-relink`, it is highly recommended to check out the [migration guide](/cotton-box/docs/learn/react/migration-from-react-relink). --- ## v1 is finally out Hello there, it's been quite a while. Two years, in fact, since the first blog post. In this one, I will be writing from an "author's note" perspective. After countless rounds of testing, observation, and tweaking, Cotton Box's API has finally become stable enough that breaking changes no longer need to be introduced in every new release. ## Recap It was originally created as the successor of React Relink with a cleaner API, faster performance, and independence from React. However, it remained experimental and there was no concrete roadmap for further development. ## The Current Situation After testing it with some of my private side projects (from simple form applications, to projects with [Live2D](https://www.live2d.com) integration, and even to VR games that use [MediaPipe](https://ai.google.dev/edge/mediapipe/solutions/guide) + [`react-three-fiber`](https://r3f.docs.pmnd.rs/getting-started/introduction)), the results have been quite satisfactory. There were almost no lags, and even when there were, state management was never the bottleneck. Combining the above factor with the fact that there is now a stable pattern in how the library can be used / how it should be used, it made sense to do some cleanup, remove some deprecated APIs, and release it as v1. ## Future Plans The APIs of this library will remain fairly consistent unless a future version of React with breaking changes is introduced or if any bug/vulnerability has been found. Of course, there's also the experimental `` component, which is not listed in the documentation intentionally. More time is needed to test this out. Considering the purpose of this library, it is deemed near-complete, with only some rare cases that require writing custom React hooks as a workaround. I intend to keep this library simple and lightweight. However, feature suggestions are always welcomed, and I will consider adding them as long as they make sense and do not compromise performance. ## Closing Note Though this is not a popular library since its first release and neither is it at the time of writing. To anyone who comes across this post or has been using Cotton Box, thank you for putting your trust in this literally nameless state management library. I hope you continue to use and support it, and may you always find it a joy to use Cotton Box in your projects.