152 lines
4.0 KiB
TypeScript
152 lines
4.0 KiB
TypeScript
/**
|
|
* React helpers for creating Atom instances that belong to one component
|
|
* subtree. `make` returns a scoped atom with a provider, context, and `use`
|
|
* accessor. Each provider creates its own Atom once, so different subtrees can
|
|
* use the same scoped atom definition without sharing state.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
"use client"
|
|
|
|
import type * as Atom from "effect/unstable/reactivity/Atom"
|
|
import * as React from "react"
|
|
|
|
/**
|
|
* Literal type used as the `ScopedAtom` type identifier.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Used as the computed property key and marker value stored on `ScopedAtom`
|
|
* objects.
|
|
*
|
|
* @category type IDs
|
|
* @since 4.0.0
|
|
*/
|
|
export type TypeId = "~@effect/atom-react/ScopedAtom"
|
|
|
|
/**
|
|
* Type identifier for ScopedAtom.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Used as the computed property key and marker value stored on `ScopedAtom`
|
|
* objects.
|
|
*
|
|
* @category type IDs
|
|
* @since 4.0.0
|
|
*/
|
|
export const TypeId: TypeId = "~@effect/atom-react/ScopedAtom"
|
|
|
|
/**
|
|
* Scoped Atom interface with a provider-backed instance.
|
|
*
|
|
* **Example** (Providing and reading a scoped atom)
|
|
*
|
|
* ```ts
|
|
* import { make, useAtomValue } from "@effect/atom-react"
|
|
* import { Atom } from "effect/unstable/reactivity"
|
|
* import * as React from "react"
|
|
*
|
|
* const Counter = make(() => Atom.make(0))
|
|
*
|
|
* function View() {
|
|
* const atom = Counter.use()
|
|
* const value = useAtomValue(atom)
|
|
* return React.createElement("div", null, value)
|
|
* }
|
|
*
|
|
* export function App() {
|
|
* return React.createElement(Counter.Provider, null, React.createElement(View))
|
|
* }
|
|
* ```
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface ScopedAtom<A extends Atom.Atom<any>, Input = never> {
|
|
readonly [TypeId]: TypeId
|
|
use(): A
|
|
Provider: [Input] extends [never] ? React.FC<{ readonly children?: React.ReactNode | undefined }>
|
|
: React.FC<{ readonly children?: React.ReactNode | undefined; readonly value: Input }>
|
|
Context: React.Context<A>
|
|
}
|
|
|
|
/**
|
|
* Creates a ScopedAtom from a factory function.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to create an atom instance that is owned by a React provider and scoped
|
|
* to a component subtree.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The returned scoped atom includes a `Provider`, `Context`, and `use`
|
|
* accessor. The provider creates the atom once for its lifetime, passing the
|
|
* `value` prop to the factory when the scoped atom expects input.
|
|
*
|
|
* **Gotchas**
|
|
*
|
|
* `use` must run under the matching provider. Changing the provider `value`
|
|
* prop after mount does not recreate the atom.
|
|
*
|
|
* **Example** (Creating a scoped atom with input)
|
|
*
|
|
* ```ts
|
|
* import { make, useAtomValue } from "@effect/atom-react"
|
|
* import { Atom } from "effect/unstable/reactivity"
|
|
* import * as React from "react"
|
|
*
|
|
* const User = make((name: string) => Atom.make(name))
|
|
*
|
|
* function UserName() {
|
|
* const atom = User.use()
|
|
* const value = useAtomValue(atom)
|
|
* return React.createElement("span", null, value)
|
|
* }
|
|
*
|
|
* export function App() {
|
|
* return React.createElement(
|
|
* User.Provider,
|
|
* { value: "Ada" },
|
|
* React.createElement(UserName)
|
|
* )
|
|
* }
|
|
* ```
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const make = <A extends Atom.Atom<any>, Input = never>(
|
|
f: (() => A) | ((input: Input) => A)
|
|
): ScopedAtom<A, Input> => {
|
|
const Context = React.createContext<A>(undefined as unknown as A)
|
|
|
|
const use = (): A => {
|
|
const atom = React.useContext(Context)
|
|
if (atom === undefined) {
|
|
throw new Error("ScopedAtom used outside of its Provider")
|
|
}
|
|
return atom
|
|
}
|
|
|
|
const Provider: React.FC<{ readonly children?: React.ReactNode | undefined; readonly value?: Input }> = (props) => {
|
|
const atom = React.useRef<A | null>(null)
|
|
if (atom.current === null) {
|
|
if ("value" in props) {
|
|
atom.current = (f as (input: Input) => A)(props.value as Input)
|
|
} else {
|
|
atom.current = (f as () => A)()
|
|
}
|
|
}
|
|
return React.createElement(Context.Provider, { value: atom.current }, props.children)
|
|
}
|
|
|
|
return {
|
|
[TypeId]: TypeId,
|
|
use,
|
|
Provider: Provider as any,
|
|
Context
|
|
}
|
|
}
|