Skip to content

Commit

Permalink
Merge fc0e678 into 3e56eb7
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremybanka authored Jul 1, 2024
2 parents 3e56eb7 + fc0e678 commit e71f288
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-comics-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"atom.io": patch
---

🐛 Fix bug where, when using `useO` in `ephemeral` stores, a state would not be created as needed in React components.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { fireEvent, render } from "@testing-library/react"
import type { CtorToolkit, Func, Logger } from "atom.io"
import {
atomFamily,
makeMolecule,
makeRootMolecule,
moleculeFamily,
} from "atom.io"
import * as Internal from "atom.io/internal"
import * as AR from "atom.io/react"
import type { FC } from "react"

import * as Utils from "../../__util__"

const LOG_LEVELS = [null, `error`, `warn`, `info`] as const
const CHOOSE = 2

let logger: Logger

beforeEach(() => {
Internal.clearStore(Internal.IMPLICIT.STORE)
Internal.IMPLICIT.STORE.config.lifespan = `immortal`
Internal.IMPLICIT.STORE.loggers[0].logLevel = LOG_LEVELS[CHOOSE]
logger = Internal.IMPLICIT.STORE.logger
vitest.spyOn(logger, `error`)
vitest.spyOn(logger, `warn`)
vitest.spyOn(logger, `info`)
vitest.spyOn(Utils, `stdout`)
})

describe(`family usage`, () => {
const setters: Func[] = []
const scenario = () => {
const letterAtoms = atomFamily<string, string>({
key: `letter`,
default: `A`,
})
const componentMolecules = moleculeFamily({
key: `component`,
new: class Component {
public constructor(
tools: CtorToolkit<string>,
public key: string,
public letterState = tools.bond(letterAtoms),
) {}
},
})

const Letter: FC = () => {
const setLetter = AR.useI(letterAtoms, `letter`)
const letter = AR.useO(letterAtoms, `letter`)
setters.push(setLetter)
return (
<>
<div data-testid={letter}>{letter}</div>
<button
type="button"
onClick={() => {
setLetter(`B`)
}}
data-testid="changeStateButton"
/>
</>
)
}
function run() {
return render(
<AR.StoreProvider>
<Letter />
</AR.StoreProvider>,
)
}
return { run, componentMolecules }
}

it(`successfully finds a state preexisting in the store`, () => {
const { run, componentMolecules } = scenario()
const rootMolecule = makeRootMolecule(`root`)
makeMolecule(rootMolecule, componentMolecules, `letter`)
const { getByTestId } = run()
const changeStateButton = getByTestId(`changeStateButton`)
fireEvent.click(changeStateButton)
const option = getByTestId(`B`)
expect(option).toBeTruthy()
expect(setters.length).toBe(2)
expect(setters[0]).toBe(setters[1])
})

it(`throws an error if the state is not found`, () => {
const { run } = scenario()
expect(run).toThrowError(
`Atom Family "letter" member "letter" not found in store "IMPLICIT_STORE".`,
)
})
})
43 changes: 43 additions & 0 deletions packages/atom.io/react/src/parse-state-overloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type {
ReadableFamilyToken,
ReadableToken,
WritableFamilyToken,
WritableToken,
} from "atom.io"
import type { Store } from "atom.io/internal"
import { findInStore, NotFoundError, seekInStore } from "atom.io/internal"
import type { Json } from "atom.io/json"

export function parseStateOverloads<T, K extends Json.Serializable>(
store: Store,
...rest: [WritableFamilyToken<T, K>, K] | [WritableToken<T>]
): WritableToken<T>

export function parseStateOverloads<T, K extends Json.Serializable>(
store: Store,
...rest: [ReadableFamilyToken<T, K>, K] | [ReadableToken<T>]
): ReadableToken<T>

export function parseStateOverloads<T, K extends Json.Serializable>(
store: Store,
...rest: [ReadableFamilyToken<T, K>, K] | [ReadableToken<T>]
): ReadableToken<T> {
let token: ReadableToken<any>
if (rest.length === 2) {
const family = rest[0]
const key = rest[1]

if (store.config.lifespan === `immortal`) {
const maybeToken = seekInStore(family, key, store)
if (!maybeToken) {
throw new NotFoundError(family, key, store)
}
token = maybeToken
} else {
token = findInStore(family, key, store)
}
} else {
token = rest[0]
}
return token
}
17 changes: 6 additions & 11 deletions packages/atom.io/react/src/use-i.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ReadableToken, WritableFamilyToken, WritableToken } from "atom.io"
import { findInStore, setIntoStore } from "atom.io/internal"
import type { WritableFamilyToken, WritableToken } from "atom.io"
import { setIntoStore } from "atom.io/internal"
import type { Json } from "atom.io/json"
import * as React from "react"

import { parseStateOverloads } from "./parse-state-overloads"
import { StoreContext } from "./store-context"

export function useI<T>(
Expand All @@ -15,22 +16,16 @@ export function useI<T, K extends Json.Serializable>(
): <New extends T>(next: New | ((old: T) => New)) => void

export function useI<T, K extends Json.Serializable>(
token: WritableFamilyToken<T, K> | WritableToken<T>,
key?: K,
...params: [WritableFamilyToken<T, K>, K] | [WritableToken<T>]
): <New extends T>(next: New | ((old: T) => New)) => void {
const store = React.useContext(StoreContext)
const stateToken: ReadableToken<any> =
token.type === `atom_family` ||
token.type === `mutable_atom_family` ||
token.type === `selector_family`
? findInStore(token, key as K, store)
: token
const token = parseStateOverloads(store, ...params)
const setter: React.MutableRefObject<
(<New extends T>(next: New | ((old: T) => New)) => void) | null
> = React.useRef(null)
if (setter.current === null) {
setter.current = (next) => {
setIntoStore(stateToken, next, store)
setIntoStore(token, next, store)
}
}
return setter.current
Expand Down
28 changes: 7 additions & 21 deletions packages/atom.io/react/src/use-o.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type { ReadableFamilyToken, ReadableToken } from "atom.io"
import {
getFromStore,
NotFoundError,
seekInStore,
subscribeToState,
} from "atom.io/internal"
import { getFromStore, subscribeToState } from "atom.io/internal"
import type { Json } from "atom.io/json"
import * as React from "react"

import { parseStateOverloads } from "./parse-state-overloads"
import { StoreContext } from "./store-context"

export function useO<T>(token: ReadableToken<T>): T
Expand All @@ -18,24 +14,14 @@ export function useO<T, K extends Json.Serializable>(
): T

export function useO<T, K extends Json.Serializable>(
token: ReadableFamilyToken<T, K> | ReadableToken<T>,
key?: K,
...params: [ReadableFamilyToken<T, K>, K] | [ReadableToken<T>]
): T {
const store = React.useContext(StoreContext)
const stateToken: ReadableToken<any> | undefined =
token.type === `atom_family` ||
token.type === `mutable_atom_family` ||
token.type === `selector_family` ||
token.type === `readonly_selector_family`
? seekInStore(token, key as K, store)
: token
if (!stateToken) {
throw new NotFoundError(token, store)
}
const token = parseStateOverloads(store, ...params)
const id = React.useId()
return React.useSyncExternalStore<T>(
(dispatch) => subscribeToState(stateToken, dispatch, `use-o:${id}`, store),
() => getFromStore(stateToken, store),
() => getFromStore(stateToken, store),
(dispatch) => subscribeToState(token, dispatch, `use-o:${id}`, store),
() => getFromStore(token, store),
() => getFromStore(token, store),
)
}

0 comments on commit e71f288

Please sign in to comment.