0.3.1 • Published 7 months ago

@isograph/react-disposable-state v0.3.1

Weekly downloads
-
License
MIT
Repository
github
Last release
7 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-11804ee7

10 months ago

0.0.0-main-1fad6d74

10 months ago

0.0.0-main-72f66445

12 months ago

0.0.0-main-338d386b

11 months ago

0.0.0-main-f57df69d

10 months ago

0.0.0-main-053f33e0

10 months ago

0.0.0-main-7075e43e

11 months ago

0.0.0-main-ca861a44

11 months ago

0.0.0-main-0dd05219

11 months ago

0.0.0-main-a05acfa1

11 months ago

0.0.0-main-aa3240a3

10 months ago

0.0.0-main-dc615175

11 months ago

0.0.0-main-1e6efe41

12 months ago

0.0.0-main-c1486166

11 months ago

0.0.0-main-c3b173c5

10 months ago

0.0.0-main-9cb0ac6d

10 months ago

0.0.0-main-2643f64b

12 months ago

0.0.0-main-009118dc

10 months ago

0.0.0-main-55840369

10 months ago

0.0.0-main-b6e3fa5c

10 months ago

0.0.0-main-1a7fad8a

10 months ago

0.0.0-main-edefa22a

10 months ago

0.0.0-main-6b4f9048

10 months ago

0.0.0-main-b63b546e

10 months ago

0.0.0-main-cd7d2fe9

11 months ago

0.0.0-main-45f1d510

10 months ago

0.0.0-main-bcc86bdf

10 months ago

0.0.0-main-8a2ecd05

12 months ago

0.0.0-main-1426567c

10 months ago

0.0.0-main-b18976db

12 months ago

0.0.0-main-63adc9b6

12 months ago

0.0.0-main-329b99ea

10 months ago

0.0.0-main-0133223b

10 months ago

0.0.0-main-f9d40cf5

10 months ago

0.0.0-main-e8f38c44

11 months ago

0.0.0-main-62a3a15d

10 months ago

0.0.0-main-619418b0

10 months ago

0.0.0-main-e3842ae9

11 months ago

0.0.0-main-1ee228e6

12 months ago

0.0.0-main-f47d436f

11 months ago

0.0.0-main-a31f8cdc

11 months ago

0.0.0-main-93dbf1b1

11 months ago

0.0.0-main-b35176a2

11 months ago

0.0.0-main-a28a4a47

11 months ago

0.0.0-main-d8a5abe6

10 months ago

0.0.0-main-21615445

10 months ago

0.0.0-main-ed8f1aa9

11 months ago

0.0.0-main-bff05d0a

10 months ago

0.0.0-main-422bbf64

11 months ago

0.0.0-main-af93df77

11 months ago

0.0.0-main-581bb44b

11 months ago

0.0.0-main-a706b60f

11 months ago

0.0.0-main-4903eaa6

10 months ago

0.0.0-main-a6d48388

11 months ago

0.0.0-main-17115d24

10 months ago

0.0.0-main-bfd98f0e

11 months ago

0.0.0-main-f31f329f

11 months ago

0.0.0-main-9cd55b35

10 months ago

0.0.0-main-429d0819

11 months ago

0.3.0

1 year ago

0.3.1

10 months ago

0.0.0-main-7be347e5

11 months ago

0.0.0-main-1445d4be

10 months ago

0.0.0-main-a8d365e0

11 months ago

0.0.0-main-dd11f25a

10 months ago

0.0.0-main-4ed3a229

10 months ago

0.0.0-main-94aca6ab

12 months ago

0.0.0-main-36c759db

12 months ago

0.0.0-main-ec67a499

10 months ago

0.0.0-main-6714b0ba

12 months ago

0.0.0-main-58449359

10 months ago

0.0.0-main-92419f97

11 months ago

0.0.0-main-15990be5

11 months ago

0.0.0-main-3a405a0b

12 months ago

0.0.0-main-b51a9d36

10 months ago

0.0.0-main-9844c47b

10 months ago

0.0.0-main-a7025560

10 months ago

0.0.0-main-15dff200

11 months ago

0.0.0-main-a3ddc46b

10 months ago

0.0.0-main-32ab815d

11 months ago

0.0.0-main-6815ceff

10 months ago

0.0.0-main-7e562882

10 months ago

0.0.0-main-bc162b7f

11 months ago

0.0.0-main-8c911568

10 months ago

0.0.0-main-54353213

10 months ago

0.0.0-main-91dd3221

11 months ago

0.0.0-main-ddee3f13

10 months ago

0.0.0-main-3b053bd1

10 months ago

0.0.0-main-0a63b51d

12 months ago

0.0.0-main-caf6212b

10 months ago

0.0.0-main-41b0910c

10 months ago

0.0.0-main-f012beaa

10 months ago

0.0.0-main-95f1797a

10 months ago

0.0.0-main-e6e5bbee

10 months ago

0.0.0-main-63c03947

11 months ago

0.0.0-main-0e6deb22

10 months ago

0.0.0-main-ae4b2d9e

11 months ago

0.0.0-main-ad756712

10 months ago

0.0.0-main-2029a7fd

10 months ago

0.0.0-main-1ae3a60f

10 months ago

0.0.0-main-04148ee3

10 months ago

0.0.0-main-16d04ff2

10 months ago

0.0.0-main-9a69a0ec

11 months ago

0.0.0-main-1161420c

10 months ago

0.0.0-main-3ed64eb0

10 months ago

0.0.0-main-89410ff8

11 months ago

0.0.0-main-c136ccb2

10 months ago

0.0.0-main-ea95eb02

10 months ago

0.0.0-main-794e835b

11 months ago

0.0.0-main-e524ebe1

11 months ago

0.0.0-main-b9561697

10 months ago

0.0.0-main-e6730023

10 months ago

0.0.0-main-368b39c5

10 months ago

0.0.0-main-d8e1c851

11 months ago

0.0.0-main-b8fe0aa9

11 months ago

0.0.0-main-421d6faf

11 months ago

0.0.0-main-8e2f249b

10 months ago

0.0.0-main-77d745e6

11 months ago

0.0.0-main-0503387b

11 months ago

0.0.0-main-8928eb1a

11 months ago

0.0.0-main-569a8fb2

10 months ago

0.0.0-main-70814870

11 months ago

0.0.0-main-2db6e226

10 months ago

0.0.0-main-b9ec7bd6

12 months ago

0.0.0-main-ac41ff11

11 months ago

0.0.0-main-24737b7d

11 months ago

0.0.0-main-8947b9d7

10 months ago

0.0.0-main-0d7c9e21

11 months ago

0.0.0-main-a5a81588

11 months ago

0.0.0-main-addc6619

10 months ago

0.0.0-main-8821fa52

10 months ago

0.0.0-main-a8794279

11 months ago

0.0.0-main-e43abb57

10 months ago

0.0.0-main-21425b7e

10 months ago

0.0.0-main-b135a55a

10 months ago

0.0.0-main-1bc48a3d

11 months ago

0.0.0-main-b39aba1c

11 months ago

0.0.0-main-29e04a3a

10 months ago

0.0.0-main-0f04a24b

10 months ago

0.0.0-main-4d0311c4

10 months ago

0.0.0-main-2d807fe9

11 months ago

0.0.0-main-9897848c

11 months ago

0.0.0-main-58085cb8

11 months ago

0.0.0-main-a8143b3f

12 months ago

0.0.0-main-e970ef76

12 months ago

0.0.0-main-e6ed7144

10 months ago

0.0.0-main-a1b66e5d

11 months ago

0.0.0-main-34913258

10 months ago

0.0.0-main-47d46523

12 months ago

0.0.0-main-de7554c5

12 months ago

0.0.0-main-0e6e4170

10 months ago

0.0.0-main-dc39b519

10 months ago

0.0.0-main-db101241

12 months ago

0.0.0-main-37c3574c

12 months ago

0.0.0-main-d1387f8e

10 months ago

0.0.0-main-7faadacd

10 months ago

0.0.0-main-be0441b1

12 months ago

0.0.0-main-fe543c49

11 months ago

0.0.0-main-2ab38254

10 months ago

0.0.0-main-fe41f671

10 months ago

0.0.0-main-ed5c5147

10 months ago

0.0.0-main-aa925b7c

10 months ago

0.0.0-main-4be0dc5c

11 months ago

0.0.0-main-0a3fce19

11 months ago

0.0.0-main-21275f7c

12 months ago

0.0.0-main-1e3f3965

11 months ago

0.0.0-main-3092d4a2

10 months ago

0.0.0-main-b586c45f

11 months ago

0.0.0-main-1c94afd7

10 months ago

0.0.0-main-090aeaf5

10 months ago

0.0.0-main-ca10f781

10 months ago

0.0.0-main-f55db82a

10 months ago

0.0.0-main-379aece4

11 months ago

0.2.0

1 year ago

0.1.1

2 years ago

0.1.0

2 years ago

0.0.4

3 years ago