jotaijs / jotai-scope

MIT License
55 stars 4 forks source link

Difficulty Managing Dynamic `atomFamily` Instances in `ScopeProvider` #50

Closed Yarden-Ankonina closed 1 month ago

Yarden-Ankonina commented 1 month ago

I'm encountering challenges while managing dynamic atomFamily instances within the ScopeProvider.

Currently, I'm unable to directly pass the entire atomFamily into the atom array of ScopeProvider. Instead, I'm forced to extract each individual atom and pass them separately. This approach becomes cumbersome in my application, where I heavily rely on atomFamily for its functionality. Manually tracking and passing every single atom instance is quite tedious and doesn't seem an option currently.

// Current workaround (tedious and error-prone):

const keys = [...]; // Array containing dynamic keys
const atomsFromFooFamily = keys.map(key => fooFamily(key));

<ScopeProvider atoms={[...normalAtoms, ...atomsFromFooFamily]}>

Ideally, I'd like to pass an entire atomFamily group into the ScopeProvider.

// Ideally, we would like to do this:
<ScopeProvider atoms={[fooFamily]}>  // Not currently supported

Is there a recommended approach to address this scenario? Any suggestions or workarounds would be greatly appreciated.

dmaskasky commented 1 month ago

I don't have any suggestions outside of the current tedious approach you mentioned above.

Brainstorming: AtomFamily is a just a function but we can assume it returns atoms since we want to pass it in to atoms prop. Perhaps jotai-scope could expose a utility to scope functions.

import { scopableFn, ScopeProvider } from 'jotai-scope'

const scopableAtomFamily = scopableFn(atomFamily((id) => atom(id)))

<ScopeProvider atoms={[scopableAtomFamily]}>{children}</ScopeProvider>

Impl might look something like this:

type AtomFn = <T = unknown>(...args:T[]) => Atom<T>
const SCOPABLE = Symbol()
function ScopeFnAtom(fn: AtomFn, sub: Store['sub']) {
  return Object.assign(
    Atom(null),
    fn,
    { type: SCOPABLE, sub }
  )
}

export function scopableFn<T extends AtomFn>(fn: T): T & Atom<ReturnType<T>> {
  const store = createStore()
  const i = function interceptor(...args: unknown[]) {
    const r = fn(...args)
    store.set(a, r)
    return r;
  }
  cons sub = store.sub.bind(a)
  const a = ScopeFnAtom(i, sub)
  return a
}

export function ScopeProvider({ atoms }) {
  const [rAtoms, setRAtoms] = useState(new Set())
  const scopedAtoms = new Set([...atoms, ...rAtoms])
  function initialize(scopedAtoms) {
    return { scopedAtoms, store: createScopedStore() }
  }
  const [state, setState] = useState(initialize())
  if (!isSameSet(state.scopedAtoms, scopedAtoms) {
    setState(initialize(scopedAtoms))
  }
  // subscribe to new rAtoms
  useEffect(() => {
    const unsubs = Array.from(state.scopedAtoms)
    .filter((a) => SCOPABLE in a)
    .map((a) => {
      a.sub((r) => {
        setRAtoms((rSet) => {
          new Set([...rSet, r])
        })
      })
    })
    return () => unsubs.forEach(unsub => unsub())
  }, [state])
  const memoizedChildren = useMemo(() => children, [state])
  return <Provider store={state.store}>{children}</Provider>
}