jotaijs / jotai-optics

Jotai integration library for optics-ts
MIT License
24 stars 5 forks source link

Dynamic / parameterized optics #15

Open kevinschaich opened 3 months ago

kevinschaich commented 3 months ago

Is it possible to create a generic optic or selector for an object, similar to how splitAtom works for arrays?

For both selectAtom and focusAtom, the examples on the website work nicely if you know the property you want in advance, but sometimes you do not.

// selectAtom example from the website
const personAtom = atom(defaultPerson)
const nameAtom = selectAtom(personAtom, (person) => person.name)

// focusAtom example from the website
const baseAtom = atom({ a: 5 }) // PrimitiveAtom<{a: number}>
const derivedAtom = focusAtom(baseAtom, (optic) => optic.prop('a')) // PrimitiveAtom<number>

Is it possible for us to add another primitive that does something like the following?

const baseAtom = atom({ a: 5 }) // PrimitiveAtom<{a: number}>
const derivedAtom = dynamicFocusAtom<T>(baseAtom, (optic, prop: T) => optic.prop(prop)) // (prop: string) => PrimitiveAtom<T>

// ...

const aAtom = derivedAtom('a') // PrimitiveAtom<T>

This would allow passing more narrowly scoped atoms down to children, but I'm not sure if we could expect any reasonable performance gains if we don't know the accessor in advance. Would be curious to hear about expected perf gains for splitAtom and maybe we can infer from that.

merisbahti commented 3 months ago

Hmm, something like this maybe:

const dynamicFocusAtom =
  <T extends object>(baseAtom: PrimitiveAtom<T>) =>
    <Key extends keyof T>(key: Key): PrimitiveAtom<T[Key]> =>
      focusAtom(baseAtom, optic => optic.prop(key))

const baseAtom = atom({ a: 5, b: 'string' } as const) // PrimitiveAtom<{a: number}>
const atomDeriver = dynamicFocusAtom(baseAtom)

const focusA = atomDeriver('a') //  PrimitiveAtom<5>
const focusB = atomDeriver('b') // PrimitiveAtom<'string'>

Does that work?

dai-shi commented 3 months ago

I wonder if optics-ts support such usage.

From Jotai's perspective, dynamicPropAtom should be possible and fairly easy without focusAtom.

https://github.com/jotaijs/jotai-optics/issues/15#issuecomment-2057517757 If that works, it works.

seanyboy49 commented 2 months ago

Hey @dai-shi I actually have the same use case. I'm not sure if https://github.com/jotaijs/jotai-optics/issues/15#issuecomment-2057517757 addresses it, because I'd like to pass in a prop when calling the setter, not when declaring the atom or wrapping it in useAtom.

Specifically, I'd like to call my focus atom setter from inside an Ably callback. My atom is an object of dynamic keys, for example

const atom = {
  foo: {
    a: {
      1: 1,
      2: 2,
    },
    b: {
      1: 1,
      2: 2,
    },
  },
  bar: {
    a: {
      1: 1,
      2: 2,
    },
    b: {
      1: 1,
      2: 2,
    },
  },
}

and I'd like to be able to update specific fields using a path I'm creating from inside the callback because I'm consuming ably messages that contain metadata from which I can construct my path.


  const focus = useCallback((optic: OpticFor<SuperTraits>) => optic.path(path), [path])

  const [value, setValue] = useAtom(focusAtom(atom, focus))

  const { connectionError, channelError } = useChannel(ablyChannelId, (message: Ably.Types.Message) => {
    const path = getPathFromMessage(message)

    setValue({ path, updatedValue: Math.random() })

I can't declare const [value, setValue] = useAtom(focusAtom(atom, focus)) inside my callback because that would break the rule of hooks. Is there a way to do this without focusAtom as you alluded to?

dai-shi commented 2 months ago

@seanyboy49 I'm not sure if I follow 100%, but it sounds like you can use a write-only atom or useAtomCallback.

  const setValueWithPath = useSetAtom(useMemo(() => atom(null, (get, set, { path, updatedValue }) => {
    const a = focusAtom(atom, (optic) => optic.path(path))
    set(a, updatedValue)
  }), []))

  const { connectionError, channelError } = useChannel(ablyChannelId, (message: Ably.Types.Message) => {
    const path = getPathFromMessage(message)

    setValueWithPath({ path, updatedValue: Math.random() })
seanyboy49 commented 2 months ago

@dai-shi thanks for the prompt response! Yes, that does the trick! I was also able to get it working using optics-ts in a derived atom

const largeAtom = atom({})

const useJotaiDerived = (path?: string) => {
  const read = useCallback(
    (get: Getter) => {
      const val = get(largeAtom)

      if (!path) return val

      const optic = O.optic().path(path)

      return O.get(optic)(val)
    },
    [path],
  )

  const write = useCallback((get: Getter, set: Setter, newValue: { path: string; value: string }) => {
    set(largeAtom, (prev) => {
      const { path, value } = newValue
      const optic = O.optic().path(path)

      const updated = O.set(optic)(value)(prev)

      return updated
    })
  }, [])

  const derivedTraitInstance = useAtom(useMemo(() => atom(read, write), [read, write]))

  return derivedTraitInstance
}