0.2.0 • Published 10 months ago

@isograph/react-disposable-state v0.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

@isograph/react-disposable-state

Primitives for managing disposable items in React state.

This library's purpose is to enable safely storing disposable items in React state. These hooks seek to guarantee that each disposable item is eventually destroyed when it is no longer used and that no disposable item is returned from a library hook after it has been disposed.

This library's goals do not include being ergonomic. A library built on top of react-disposable-state should expose easier-to-use hooks for common cases. Application developers can use the hooks exposed in react-disposable-state when more complicated cases arise.

This is unstable, alpha software. The API is likely to change.

Conceptual overview

What is a disposable item?

A disposable item is anything that is either explicitly created or must be explicitly cleaned up. That is, it is an item with a lifecycle.

A disposable item is safe to use as long as its destructor has not been called.

Code that manages disposable items (such as the useDisposableState hook) should also ensure that each destructor is eventually called, and should not provide access to the underlying item once the destructor has been called.

Disposable items are allowed to have side effects when created or when destroyed.

What is disposable state?

Disposable state is React state that contains a disposable item.

Examples of disposable items

  • A subscription that periodically updates a displayed stock price. When the component where the stock price is displayed is unmounted, the subscription should be disposed, so as to avoid doing unproductive work.
  • References to items that are stored externally. For example, consider a centralized store of profile photos. Photos are stored centrally to ensure consistency, meaning that every component displaying a given profile photo displays the same photo. In order to avoid the situation where no profile photo is ever garbage collected, individual components' "claims" to profile photos must be explicitly created and disposed.
  • Items which you want to create exactly once when a functional React component is first rendered, such as a network request.
    • Due to how React behaves, this state must be stored externally. Hence, this can be thought of as an example of the previous bullet point.
    • Other frameworks make different choices. For example, a SolidJS component function is called exactly once. In these cases, the network request can easily be executed once, without being stored in external state.

How does disposable state differ from regular React state?

Disposable state stands in contrast to "regular" React state (e.g. if {isVisible: boolean, currentlySelectedItem: Item} was stored in state), where

  • creating the JavaScript object is the only work done when creating the regular state, and therefore it is okay to create the state multiple times; and
  • the only necessary cleanup work is garbage collection of the underlying memory.

In particular, it is unobservable to the outside world if a piece of "regular" state is created multiple times.

Can React primitives handle disposable state?

The primitives provided by React are a poor fit for storing disposable items in state. An upcoming blog post will explore this in more detail.

This library

Guarantees

This library guarantees that:

  • First, each disposable item that is created is eventually disposed.

    React and suspense prevent this library from ensuring that each disposable item is disposed immediately when the hook unmounts. Instead, the best we can do if a component suspends is often dispose after a configurable timeout.

  • Second, no disposable item is returned from a library hook after it has been disposed.

  • Third, if a component has committed, no disposable item returned from a library hook will be disposed while it is accessible from a mounted component.

    Colloquially, this means that disposable items returned from library hooks are safe to use in event callbacks.

    This guarantee is not upheld if an item returned from a library hook is re-stored in another state hook. So, don't do that!

Supported behaviors

The hooks in this library enable the following behavior:

  • Lazily creating a disposable item. In this context, "lazily" means creating the item during the render phase of a component, before that component has committed. The item is then available in the functional component.

    Note that this is how Relay uses the term lazy. Libraries like react-query use the word lazy differently.

  • Creating a disposable item outside of the render phase and after a hook's initial commit and storing the item in React state, making it available during the next render of that functional component.

API Overview

useLazyDisposableState

A hook that:

  • Takes a mutable parent cache and a loader function, and returns a { state: T }.
  • The returned T is guaranteed to not be disposed during the tick of the render.
  • If this hook commits, the returned T will not be disposed until the component unmounts.
const { state }: { state: T } = useLazyDisposableState<T>(
  parentCache: ParentCache<T>,
  factory: Loader<T>,
  options: ?Options,
);

useUpdatableDisposableState

A hook that:

  • Returns a { state, setState } object.
  • setState throws if called before the initial commit.
  • The state (a disposable item) is guaranteed to be undisposed during the tick in which it is returned from the hook. It will not be disposed until after it can no longer be returned from this hook, even in the presence of concurrent rendering.
  • Every time the hook commits, a given disposable item is currently exposed in the state. All items previously passed to setState are guaranteed to never be returned from the hook, so they are disposed at that time.
  • When the hook unmounts, all disposable items passed to setState are disposed.
const {
  state,
  setState,
}: {
  state: T | null,
  setState: (ItemCleanupPair<T>) => void,
} = useUpdatableDisposableState<T>(
  options: ?Options,
);

useDisposableState

This could properly be called useLazyUpdatableDisposableState, but that's quite long!

A hook that combines the behavior of the previous two hooks:

const {
  state,
  setState,
}: {
  state: T,
  setState: (ItemCleanupPair<T>) => void,
} = useDisposableState<T>(
  parentCache: ParentCache<T>,
  factory: Loader<T>,
  options: ?Options,
);

Miscellaneous notes

Runtime overhead

  • The hooks in this library are generic, and the type of the disposable items T is mostly as unconstrained as possible.
    • The only constraint we impose on T is to disallow T from including the value UNASSIGNED_STATE. This is for primarily for ergonomic purposes. However, it does prevent some runtime overhead.
  • This incurs some runtime overhead. In particular, it means we need to keep track of an index (and create a new short-lived object) to distinguish items that can overthise be === to each other. Consider, a component that uses useDisposableState or useUpdatableDiposableState. If we execute setState([1, cleanup1]) followed by setState([1, cleanup2]), we would expect cleanup1 to be called when the hook commits. This index is required to distinguish those two, otherwise indistinguishable items.
    • This problem also occurs if disposable items are re-used, but their cleanup functions are distinct. That can occur if items are shared references held in a reference counted wrapper!
  • However, client libraries may not require this flexbility! For example, if every disposable item is a newly-created object, then all disposable items are !== to each other!
  • A future version of this library should expose alternative hooks that disallow null and do away with the above check. They may be
0.0.0-main-27c54dfe

10 months ago

0.0.0-main-e3e8bd35

10 months ago

0.0.0-main-2eb325ad

10 months ago

0.0.0-main-f967d0d8

10 months ago

0.0.0-main-a779c3fa

10 months ago

0.0.0-main-27d27904

10 months ago

0.0.0-main-7e518adc

10 months ago

0.0.0-main-5786fd9a

10 months ago

0.0.0-main-33f55790

10 months ago

0.0.0-main-cd6f81ae

10 months ago

0.0.0-main-312396ca

10 months ago

0.0.0-main-81ce1b46

10 months ago

0.0.0-main-2a45cb8e

10 months ago

0.0.0-main-bc3a4c4e

10 months ago

0.0.0-main-2d80240d

10 months ago

0.0.0-main-635653ad

10 months ago

0.0.0-main-6ea7b362

10 months ago

0.0.0-main-a33a3ba5

10 months ago

0.0.0-main-adb0955b

10 months ago

0.0.0-main-ba9f1dc0

10 months ago

0.0.0-main-1ed9144e

10 months ago

0.0.0-main-c08fea92

10 months ago

0.0.0-main-1c9d51fd

10 months ago

0.0.0-main-24a3a309

10 months ago

0.0.0-main-9977b173

10 months ago

0.0.0-main-cebaca69

10 months ago

0.0.0-main-00af1d07

10 months ago

0.0.0-main-eca51643

10 months ago

0.0.0-main-49a3b791

10 months ago

0.0.0-main-86b60cac

10 months ago

0.0.0-main-5da1ab92

10 months ago

0.0.0-main-6bd3135f

10 months ago

0.0.0-main-fa95f207

10 months ago

0.0.0-main-e7d6b095

10 months ago

0.0.0-main-7f0213de

10 months ago

0.0.0-main-a60cb5f4

10 months ago

0.0.0-main-03306d26

10 months ago

0.0.0-main-ed225a2f

10 months ago

0.0.0-main-95d68bdb

10 months ago

0.0.0-main-9624b77b

10 months ago

0.0.0-main-f60a695a

10 months ago

0.0.0-main-3590e7ff

10 months ago

0.0.0-main-3a0c06c0

10 months ago

0.0.0-main-fde72b43

10 months ago

0.0.0-main-359621bd

10 months ago

0.0.0-main-ec963c15

10 months ago

0.0.0-main-deae48c9

10 months ago

0.0.0-main-bcb17610

10 months ago

0.0.0-main-5046aeef

11 months ago

0.0.0-main-feb39d09

11 months ago

0.0.0-main-d25e8223

11 months ago

0.0.0-main-f19a405a

11 months ago

0.0.0-main-41e5b427

11 months ago

0.0.0-main-88b54795

11 months ago

0.0.0-main-b82d48f7

11 months ago

0.0.0-main-afeb0123

11 months ago

0.0.0-main-c682402c

11 months ago

0.0.0-main-85244746

11 months ago

0.0.0-main-5a08f41e

11 months ago

0.0.0-main-eadfb992

11 months ago

0.0.0-main-df52a61c

11 months ago

0.0.0-main-7be74a4a

11 months ago

0.0.0-main-3246ac6f

11 months ago

0.0.0-main-c42c585e

11 months ago

0.0.0-main-8f1270e1

11 months ago

0.0.0-main-a8fa50f7

11 months ago

0.0.0-main-9ba8daa2

11 months ago

0.0.0-main-2f4311e8

11 months ago

0.0.0-main-2eacee40

11 months ago

0.0.0-main-a1719163

11 months ago

0.0.0-main-ce58404c

11 months ago

0.0.0-main-e0dec233

11 months ago

0.0.0-main-a35490bf

11 months ago

0.0.0-main-c938c1af

11 months ago

0.0.0-main-a94cf56c

11 months ago

0.0.0-main-13cb8421

11 months ago

0.0.0-main-dcf30dfc

11 months ago

0.0.0-main-3f50636d

11 months ago

0.0.0-main-3179ac5f

11 months ago

0.0.0-main-e981e49a

11 months ago

0.0.0-main-bc897185

11 months ago

0.0.0-main-4693ce16

11 months ago

0.0.0-main-003fa338

11 months ago

0.0.0-main-c82422d5

11 months ago

0.0.0-main-2fa0e011

11 months ago

0.0.0-main-46f02f4f

11 months ago

0.0.0-main-366b2007

11 months ago

0.0.0-main-a4b92149

11 months ago

0.0.0-main-2a2d5556

11 months ago

0.0.0-main-8fa74274

11 months ago

0.0.0-main-d43121ce

11 months ago

0.0.0-main-a7e15428

11 months ago

0.0.0-main-c10b5e98

11 months ago

0.0.0-main-dfe22b10

11 months ago

0.0.0-main-3dfc0c7a

11 months ago

0.0.0-main-ad610d36

11 months ago

0.0.0-main-4823854b

11 months ago

0.0.0-main-f6c1187d

11 months ago

0.0.0-main-09c35a96

11 months ago

0.0.0-main-03be6563

11 months ago

0.0.0-main-7d4bae3e

11 months ago

0.0.0-main-2ff9f31d

11 months ago

0.0.0-main-04cbf235

11 months ago

0.0.0-main-451cf238

11 months ago

0.0.0-main-e5ca30ca

11 months ago

0.0.0-main-18c54425

11 months ago

0.0.0-main-d57417f0

11 months ago

0.0.0-main-ca0c0f86

11 months ago

0.0.0-main-3913e7d8

11 months ago

0.0.0-main-d8aff718

11 months ago

0.0.0-main-46027826

12 months ago

0.0.0-main-b7b79634

11 months ago

0.0.0-main-8b64a409

11 months ago

0.0.0-main-fab8db7b

12 months ago

0.0.0-main-4b08a983

11 months ago

0.0.0-main-67785c60

12 months ago

0.0.0-main-752db914

11 months ago

0.0.0-main-6abaad06

11 months ago

0.0.0-main-2c66f42d

11 months ago

0.0.0-main-7e245bae

11 months ago

0.0.0-main-7986369f

11 months ago

0.0.0-main-fa61d1ea

11 months ago

0.0.0-main-351c9868

11 months ago

0.0.0-main-7f20f7d1

12 months ago

0.0.0-main-bf1400f8

11 months ago

0.0.0-main-75a6eaf1

11 months ago

0.0.0-main-d09a3258

11 months ago

0.0.0-main-7f0b6f51

12 months ago

0.0.0-main-0cc5b694

11 months ago

0.2.0

12 months ago

0.0.0-main-94c46ec4

12 months ago

0.0.0-main-1a5308d3

12 months ago

0.1.1

1 year ago

0.1.0

2 years ago

0.0.4

2 years ago