0.3.1 • Published 10 months ago

@isograph/react-disposable-state v0.3.1

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-549bff1d

12 months ago

0.0.0-main-ae9a00d2

12 months ago

0.0.0-main-5758e2c0

12 months ago

0.0.0-main-f77e140e

11 months ago

0.0.0-main-98177d8c

12 months ago

0.0.0-main-488d6104

12 months ago

0.0.0-main-276e7abd

11 months ago

0.0.0-main-2762501e

12 months ago

0.0.0-main-82460922

11 months ago

0.0.0-main-c06adfbf

12 months ago

0.0.0-main-cdb89d27

12 months ago

0.0.0-main-7e3ab1be

12 months ago

0.0.0-main-92ca6fc4

12 months ago

0.0.0-main-137ca49c

12 months ago

0.0.0-main-2d6f39f7

10 months ago

0.0.0-main-a341073e

12 months ago

0.0.0-main-f7b25931

12 months ago

0.0.0-main-6327b36d

10 months ago

0.0.0-main-3496e86d

12 months ago

0.0.0-main-d547cc32

12 months ago

0.0.0-main-d97039cc

12 months ago

0.0.0-main-6fb53fcf

12 months ago

0.0.0-main-59eae5bc

12 months ago

0.0.0-main-934bae6b

12 months ago

0.0.0-main-9811abc4

11 months ago

0.0.0-main-70411e5a

12 months ago

0.0.0-main-d18d38e8

12 months ago

0.0.0-main-c8b9bce7

12 months ago

0.0.0-main-5eae621e

12 months ago

0.0.0-main-ee54941f

11 months ago

0.0.0-main-3b6fcdcf

10 months ago

0.0.0-main-6e1c4859

12 months ago

0.0.0-main-ba2ac4a0

10 months ago

0.0.0-main-4cbde8a4

12 months ago

0.0.0-main-09420510

12 months ago

0.0.0-main-43b6da08

12 months ago

0.0.0-main-c4e804c1

12 months ago

0.0.0-main-97e88713

12 months ago

0.0.0-main-d8a9821b

12 months ago

0.0.0-main-cf1581f2

12 months ago

0.0.0-main-1bbe1d36

11 months ago

0.0.0-main-e3a223b9

12 months ago

0.0.0-main-93df77c3

12 months ago

0.0.0-main-276e48f3

12 months ago

0.0.0-main-8714cfae

12 months ago

0.3.0

1 year ago

0.0.0-main-66a4ed6e

10 months ago

0.3.1

1 year ago

0.0.0-main-029266a5

12 months ago

0.0.0-main-58aa5ad6

12 months ago

0.0.0-main-5fcfcce3

12 months ago

0.0.0-main-7c6b713a

11 months ago

0.0.0-main-162af2e3

10 months ago

0.0.0-main-1bff6332

12 months ago

0.0.0-main-c5d57cff

12 months ago

0.0.0-main-7a98939d

12 months ago

0.0.0-main-0ec3e7e8

12 months ago

0.0.0-main-a2cf999d

11 months ago

0.0.0-main-c73610ca

10 months ago

0.0.0-main-e3bd6adc

11 months ago

0.0.0-main-1444410c

12 months ago

0.0.0-main-881191b4

12 months ago

0.0.0-main-01e7b439

11 months ago

0.0.0-main-1e42bbd0

12 months ago

0.0.0-main-e078bc1b

11 months ago

0.0.0-main-4f51f46f

12 months ago

0.0.0-main-2d180771

12 months ago

0.0.0-main-72d2f566

11 months ago

0.0.0-main-726e2a51

12 months ago

0.0.0-main-ef8d9137

12 months ago

0.0.0-main-23a1e9c6

12 months ago

0.0.0-main-0cfd72fa

12 months ago

0.0.0-main-88ca38d2

10 months ago

0.0.0-main-27bdb038

12 months ago

0.0.0-main-45804ba8

12 months ago

0.0.0-main-81c49bba

11 months ago

0.0.0-main-e7cba6b6

12 months ago

0.0.0-main-79ed5691

12 months ago

0.0.0-main-9f72a354

11 months ago

0.0.0-main-e36c475b

12 months ago

0.0.0-main-4cd8ea77

12 months ago

0.0.0-main-4ecafb6b

12 months ago

0.0.0-main-39db008b

12 months ago

0.0.0-main-1a20e538

12 months ago

0.0.0-main-eaa1d223

10 months ago

0.0.0-main-f38015b7

11 months ago

0.0.0-main-14b6ea4e

12 months ago

0.0.0-main-f36ebec1

12 months ago

0.0.0-main-a0f868b8

11 months ago

0.0.0-main-93900eea

12 months ago

0.0.0-main-ccdbbf2a

11 months ago

0.0.0-main-02c86a8a

12 months ago

0.0.0-main-d0688030

12 months ago

0.0.0-main-22b7c5aa

12 months ago

0.0.0-main-852bb22e

12 months ago

0.0.0-main-242659cd

11 months ago

0.0.0-main-6f4e7bad

12 months ago

0.0.0-main-93c6943f

12 months ago

0.0.0-main-61621f25

12 months ago

0.0.0-main-12a79147

12 months ago

0.0.0-main-53462644

10 months ago

0.2.0

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago

0.0.4

3 years ago