Skip to content

Commit

Permalink
chore(jotai): add jotai for local reference
Browse files Browse the repository at this point in the history
  • Loading branch information
dmaskasky committed Sep 24, 2024
1 parent b53fe9e commit fd31026
Show file tree
Hide file tree
Showing 33 changed files with 2,677 additions and 0 deletions.
2 changes: 2 additions & 0 deletions jotai/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './vanilla'
export * from './react'
1 change: 1 addition & 0 deletions jotai/mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MODE: 'development' | 'production' = 'development'
4 changes: 4 additions & 0 deletions jotai/react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Provider, useStore } from './react/Provider'
export { useAtomValue } from './react/useAtomValue'
export { useSetAtom } from './react/useSetAtom'
export { useAtom } from './react/useAtom'
39 changes: 39 additions & 0 deletions jotai/react/Provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createContext, createElement, useContext, useRef } from 'react'
import type { FunctionComponentElement, ReactNode } from 'react'
import { createStore, getDefaultStore } from '../vanilla'

type Store = ReturnType<typeof createStore>

type StoreContextType = ReturnType<typeof createContext<Store | undefined>>
const StoreContext: StoreContextType = createContext<Store | undefined>(
undefined,
)

type Options = {
store?: Store
}

export const useStore = (options?: Options): Store => {
const store = useContext(StoreContext)
return options?.store || store || getDefaultStore()
}

export const Provider = ({
children,
store,
}: {
children?: ReactNode
store?: Store
}): FunctionComponentElement<{ value: Store | undefined }> => {
const storeRef = useRef<Store>()
if (!store && !storeRef.current) {
storeRef.current = createStore()
}
return createElement(
StoreContext.Provider,
{
value: store || storeRef.current,
},
children,
)
}
56 changes: 56 additions & 0 deletions jotai/react/useAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {
Atom,
ExtractAtomArgs,
ExtractAtomResult,
ExtractAtomValue,
PrimitiveAtom,
SetStateAction,
WritableAtom,
} from '../vanilla'
import { useAtomValue } from './useAtomValue'
import { useSetAtom } from './useSetAtom'

type SetAtom<Args extends unknown[], Result> = (...args: Args) => Result

type Options = Parameters<typeof useAtomValue>[1]

export function useAtom<Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
options?: Options,
): [Awaited<Value>, SetAtom<Args, Result>]

export function useAtom<Value>(
atom: PrimitiveAtom<Value>,
options?: Options,
): [Awaited<Value>, SetAtom<[SetStateAction<Value>], void>]

export function useAtom<Value>(
atom: Atom<Value>,
options?: Options,
): [Awaited<Value>, never]

export function useAtom<
AtomType extends WritableAtom<unknown, never[], unknown>,
>(
atom: AtomType,
options?: Options,
): [
Awaited<ExtractAtomValue<AtomType>>,
SetAtom<ExtractAtomArgs<AtomType>, ExtractAtomResult<AtomType>>,
]

export function useAtom<AtomType extends Atom<unknown>>(
atom: AtomType,
options?: Options,
): [Awaited<ExtractAtomValue<AtomType>>, never]

export function useAtom<Value, Args extends unknown[], Result>(
atom: Atom<Value> | WritableAtom<Value, Args, Result>,
options?: Options,
) {
return [
useAtomValue(atom, options),
// We do wrong type assertion here, which results in throwing an error.
useSetAtom(atom as WritableAtom<Value, Args, Result>, options),
]
}
168 changes: 168 additions & 0 deletions jotai/react/useAtomValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/// <reference types="react/experimental" />
import ReactExports, { useDebugValue, useEffect, useReducer } from 'react'
import type { ReducerWithoutAction } from 'react'
import type { Atom, ExtractAtomValue } from '../vanilla'
import { useStore } from './Provider'
import { MODE } from '../mode'

type Store = ReturnType<typeof useStore>

const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
typeof (x as any)?.then === 'function'

const attachPromiseMeta = <T>(
promise: PromiseLike<T> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
reason?: unknown
},
) => {
promise.status = 'pending'
promise.then(
(v) => {
promise.status = 'fulfilled'
promise.value = v
},
(e) => {
promise.status = 'rejected'
promise.reason = e
},
)
}

const use =
ReactExports.use ||
(<T>(
promise: PromiseLike<T> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
reason?: unknown
},
): T => {
if (promise.status === 'pending') {
throw promise
} else if (promise.status === 'fulfilled') {
return promise.value as T
} else if (promise.status === 'rejected') {
throw promise.reason
} else {
attachPromiseMeta(promise)
throw promise
}
})

const continuablePromiseMap = new WeakMap<
PromiseLike<unknown>,
Promise<unknown>
>()

const createContinuablePromise = <T>(promise: PromiseLike<T>) => {
let continuablePromise = continuablePromiseMap.get(promise)
if (!continuablePromise) {
continuablePromise = new Promise<T>((resolve, reject) => {
let curr = promise
const onFulfilled = (me: PromiseLike<T>) => (v: T) => {
if (curr === me) {
resolve(v)
}
}
const onRejected = (me: PromiseLike<T>) => (e: unknown) => {
if (curr === me) {
reject(e)
}
}
const registerCancelHandler = (p: PromiseLike<T>) => {
if ('onCancel' in p && typeof p.onCancel === 'function') {
p.onCancel((nextValue: PromiseLike<T> | T) => {
if (MODE !== 'production' && nextValue === p) {
throw new Error('[Bug] p is not updated even after cancelation')
}
if (isPromiseLike(nextValue)) {
continuablePromiseMap.set(nextValue, continuablePromise!)
curr = nextValue
nextValue.then(onFulfilled(nextValue), onRejected(nextValue))
registerCancelHandler(nextValue)
} else {
resolve(nextValue)
}
})
}
}
promise.then(onFulfilled(promise), onRejected(promise))
registerCancelHandler(promise)
})
continuablePromiseMap.set(promise, continuablePromise)
}
return continuablePromise
}

type Options = Parameters<typeof useStore>[0] & {
delay?: number
}

export function useAtomValue<Value>(
atom: Atom<Value>,
options?: Options,
): Awaited<Value>

export function useAtomValue<AtomType extends Atom<unknown>>(
atom: AtomType,
options?: Options,
): Awaited<ExtractAtomValue<AtomType>>

export function useAtomValue<Value>(atom: Atom<Value>, options?: Options) {
const store = useStore(options)

const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] =
useReducer<
ReducerWithoutAction<readonly [Value, Store, typeof atom]>,
undefined
>(
(prev) => {
const nextValue = store.get(atom)
if (
Object.is(prev[0], nextValue) &&
prev[1] === store &&
prev[2] === atom
) {
return prev
}
return [nextValue, store, atom]
},
undefined,
() => [store.get(atom), store, atom],
)

let value = valueFromReducer
if (storeFromReducer !== store || atomFromReducer !== atom) {
rerender()
value = store.get(atom)
}

const delay = options?.delay
useEffect(() => {
const unsub = store.sub(atom, () => {
if (typeof delay === 'number') {
const value = store.get(atom)
if (isPromiseLike(value)) {
attachPromiseMeta(createContinuablePromise(value))
}
// delay rerendering to wait a promise possibly to resolve
setTimeout(rerender, delay)
return
}
rerender()
})
rerender()
return unsub
}, [store, atom, delay])

useDebugValue(value)
// The use of isPromiseLike is to be consistent with `use` type.
// `instanceof Promise` actually works fine in this case.
if (isPromiseLike(value)) {
const promise = createContinuablePromise(value)
return use(promise)
}
return value as Awaited<Value>
}
42 changes: 42 additions & 0 deletions jotai/react/useSetAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCallback } from 'react'
import type {
ExtractAtomArgs,
ExtractAtomResult,
WritableAtom,
} from '../vanilla'
import { useStore } from './Provider'
import { MODE } from '../mode'

type SetAtom<Args extends unknown[], Result> = (...args: Args) => Result
type Options = Parameters<typeof useStore>[0]

export function useSetAtom<Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
options?: Options,
): SetAtom<Args, Result>

export function useSetAtom<
AtomType extends WritableAtom<unknown, never[], unknown>,
>(
atom: AtomType,
options?: Options,
): SetAtom<ExtractAtomArgs<AtomType>, ExtractAtomResult<AtomType>>

export function useSetAtom<Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
options?: Options,
) {
const store = useStore(options)
const setAtom = useCallback(
(...args: Args) => {
if (MODE !== 'production' && !('write' in atom)) {
// useAtom can pass non writable atom with wrong type assertion,
// so we should check here.
throw new Error('not writable atom')
}
return store.set(atom, ...args)
},
[store, atom],
)
return setAtom
}
4 changes: 4 additions & 0 deletions jotai/react/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useResetAtom } from './utils/useResetAtom'
export { useReducerAtom } from './utils/useReducerAtom'
export { useAtomCallback } from './utils/useAtomCallback'
export { useHydrateAtoms } from './utils/useHydrateAtoms'
17 changes: 17 additions & 0 deletions jotai/react/utils/useAtomCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useMemo } from 'react'
import { useSetAtom } from '../useSetAtom'
import { atom } from '../../vanilla'
import type { Getter, Setter } from '../../vanilla'

type Options = Parameters<typeof useSetAtom>[1]

export function useAtomCallback<Result, Args extends unknown[]>(
callback: (get: Getter, set: Setter, ...arg: Args) => Result,
options?: Options,
): (...args: Args) => Result {
const anAtom = useMemo(
() => atom(null, (get, set, ...args: Args) => callback(get, set, ...args)),
[callback],
)
return useSetAtom(anAtom, options)
}
Loading

0 comments on commit fd31026

Please sign in to comment.