Cotton Box

website·Indexed 30 days ago

https://glyph-cat.github.io/cotton-box

A minimal state management library for React and beyond. It provides a suite of state managers including SimpleStateManager, AsyncStateManager, SimpleFiniteStateManager, and the base StateManager class, along with utilities like AsyncSetStateFn.

Tokens
6.2K
Snippets
49
Records
63
Agent score
75%

What's inside Cotton Box

  1. Handle InvalidStateTransitionError

    The InvalidStateTransitionError is thrown when an illegal transition occurs between two states. It provides details about the source state, target state, and the name of the state manager involved.
    // Constructor: new InvalidStateTransitionError(fromState: string, toState: string, stateManager?: string)
    
    try {
      // transition logic
    } catch (e) {
      if (e instanceof InvalidStateTransitionError) {
        console.error(`Error in ${e.stateManagerName}: ${e.fromState} -> ${e.toState}`);
      }
    }
  2. Initialize and use the StateManager class

    The StateManager<State> class provides a way to manage application state with support for asynchronous initialization. It extends SimpleStateManager and adds an isInitializing property to track the initialization status.
    const ExampleState = new StateManager('initial_value', { name: 'myStateManager' });
  3. Instantiate StateManager dynamically in React components

    When creating a StateManager within a React component, using useRef for lazy initialization can be unstable in StrictMode because effects may fire multiple times before the first cleanup occurs. The safe and stable approach is to perform both instantiation and cleanup within a single useEffect and manage the instance via useState. This requires a fallback UI (e.g., a loading state) since the state manager will not be available on the first render.
    function App(): ReactNode {
      const [ExampleState, setExampleState] = useState<StateManager<string>>(null)
    
      useEffect(() => {
        const newExampleState = new StateManager<string>('...')
        setExampleState(newExampleState)
    
        return () => {
          newExampleState.dispose()
          setExampleState(null)
        }
      }, [])
    
      if (!ExampleState) { return <>Loading...</> }
      console.log(ExampleState.get())
      return '...'
    }
  4. Implement a finite state machine with SimpleFiniteStateManager

    SimpleFiniteStateManager is used to manage a state where only specific transitions between states are permitted. If a transition is attempted that is not defined in the allowedStateTransitions array, an InvalidStateTransitionError is thrown.
    class PlayableCharacter {
      readonly state = new SimpleFiniteStateManager('idle', [
        ['idle', 'walking'],
        ['idle', 'attacking'],
        ['walking', 'idle'],
        ['attacking', 'idle'],
      ], {
        name: 'PlayableCharacter',
      })
    }
    
    const newCharacter = new PlayableCharacter();
    newCharacter.set('walking'); // Valid: idle -> walking
    newCharacter.set('attacking'); // Invalid: walking -> attacking (throws InvalidStateTransitionError)
  5. Install Cotton Box core package

    Install the core package via a dependency manager or include it via UNPKG for development or production environments.
    npm install cotton-box-core # Replace with actual package name from DocConstants.CORE_PACKAGE_NAME

    <!-- UNPKG Development --> <script src="https://www.unpkg.com/cotton-box-core@<VERSION>/dist/umd/index.js" crossorigin></script>

    <!-- UNPKG Production --> <script src="https://www.unpkg.com/cotton-box-core@<VERSION>/dist/umd/index.min.js" crossorigin></script>

  6. Dispose of a StateManager

    Use the .dispose() method to clean up StateManagers that are not global. It is recommended to wait for initialization to complete using .wait() before performing teardown.
    class GameRoom {
      state = new StateManager<GameRoomState>(GameRoomState.CREATED);
    
      async dispose(): Promise<void> {
        // Wait for initialization to complete before teardown
        await this.state.wait((state) => state >= GameRoomState.INITIALIZED);
        this.state.set(GameRoomState.DISPOSED);
        this.state.dispose();
      }
    }
  7. Instantiate a StateManager

    Create a state manager by passing the default state as the first argument and an options object as the second. The name property is optional and used for debugging. The lifecycle object allows defining init, didSet, and didReset hooks. visibility can be set using StateManagerVisibility.ENVIRONMENT.
    const ExampleState = new AsyncStateManager('defaultState', {
      name: 'example-state',
      lifecycle: {
        init({ commit, commitNoop, defaultState }) {
          // Initialization logic
        },
        didSet({ state, defaultState }) {
          // Logic after state is set
        },
        didReset() {
          // Logic after state is reset
        },
      },
      visibility: StateManagerVisibility.ENVIRONMENT,
      suspense: true,
    });
  8. Initialize state asynchronously with StateManager.init

    The init method allows you to perform asynchronous setup (e.g., loading from localStorage) and commit the resulting value to the state. It takes an initialization function that receives StateManagerInitArgs.
    await ExampleState.init(({ commit }) => {
      const someValue = localStorage.get('some-value');
      commit(someValue);
    });
  9. Migrate from react-relink to Cotton Box

    Core methods .get, .set, .reset, and .dispose remain unchanged. Key changes include:

    • RelinkSource is replaced by AsyncStateManager or StateManager.
    • Default state is now the first argument of the constructor instead of a default property in the options object.
    • key is replaced by an optional name property.
    • visibility is now a boolean/enum instead of a simple boolean.
    • deps array is replaced by manual awaiting of .isInitializing.wait(false) in the init hook.
    • hydrate(...) is replaced by .init(...).
    • useRelinkValue is replaced by useStateValue.
    • Scope is no longer supported; use React Context API instead.