Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic / parameterized optics #15

Open
kevinschaich opened this issue Apr 15, 2024 · 5 comments
Open

Dynamic / parameterized optics #15

kevinschaich opened this issue Apr 15, 2024 · 5 comments

Comments

@kevinschaich
Copy link
Member

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
Copy link
Collaborator

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
Copy link
Member

dai-shi commented Apr 16, 2024

I wonder if optics-ts support such usage.

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

#15 (comment) If that works, it works.

@seanyboy49
Copy link

Hey @dai-shi I actually have the same use case. I'm not sure if #15 (comment) 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
Copy link
Member

dai-shi commented Apr 23, 2024

@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
Copy link

@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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants