Skip to main content

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.

import { StateManager } from 'cotton-box'

enum GameRoomState {
CREATED,
INITIALIZED,
STARTED,
STOPPED,
DISPOSED,
}

class GameRoom {

state = new StateManager<GameRoomState>(GameRoomState.CREATED)

async dispose(): Promise<void> {
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" the variables for useRef and State Managers can be instantiated in a similar way too.

function App(): JSX.Element {
const ExampleState = useRef<StateManager<string>>()
if (!ExampleState.current) {
ExampleState.current = new StateManager<string>('...')
}
useEffect(() => {
return () => {
ExampleState.current.dispose()
}
}, [])
console.log(ExampleState.current.get())
return '...'
}

However, it can be tricky (#14490, #26315, #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.

function App(): JSX.Element {
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 '...'
}