Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Util RFC: derivedAtomWithCompare #2795

Closed
kevinschaich opened this issue Nov 2, 2024 · 2 comments
Closed

Util RFC: derivedAtomWithCompare #2795

kevinschaich opened this issue Nov 2, 2024 · 2 comments

Comments

@kevinschaich
Copy link
Contributor

Use case:

You have a lot of derived atoms that are selecting from large object atoms, and want to avoid the small ones publishing changes if their derived values don't change. Publishing changes every time can be undesirable for several reasons:

  • Performance can be worse if your derived atoms are all doing a non-trivial amount of work in their read functions
  • Any components which mount one of the derived atoms will re-render, even if their specific value hasn't changed
  • Async atoms re-suspend, causing your suspense boundaries to also re-suspend and make that section of your page "flicker" even if the content doesn't change

With vanilla jotai and no utils, you might have something like this

const bigAtom = atom({
    // this is demonstrative; imagine this is a very big object with many nested properties
    a: 1,
    b: 2,
    c: 3,
})

// let's say this is mounted in a component via useAtomValue
const smallAtom = atom((get) => get(bigAtom).a)

// smallAtom ideally should not re-render here, but it does because bigAtom changes
store.set(bigAtom, { ...store.get(bigAtom), b: 2 })

This is probably the right behavior for most people, but it's not the right behavior for everyone.

Existing discussions (non-exhaustive):

Many people have asked about this. If we have a snippet somewhere I'm happy to use that instead of the proposed code below.

#1158, #783, #324, #1175, #26

Proposal:

derivedAtomWithCompare: derived atom that keeps track of the previous value and deeply compares it to the next value, do not republish changes to subscribers if they are the same

  • if on read the derived value is different, updates the previous value and return the new value
  • if they are the same, don't publish changes at all (keep the existing value in all the mounted locations)
  • provide an initial static value and a read function which accepts a Getter
  • can have an async version that works with promises and a non-async version

Differences from existing utils:

  • It's similar to selectAtom, but the goal here is to provide a generic read-only atom that can do anything a normal derived atom can (needs access to a Getter)
  • It's similar to atomWithCache, but you want to check if the returned value is deeply equal to the previous returned value, rather than the dependencies of the get function. The dependency atoms will most likely change every time because they keep a lot of other things unrelated to this atom value.
  • It's similar to atomWithCompare, but your read function needs to be dynamic and read from other atoms, rather than having the returned atom be a PrimitiveAtom that gets set directly.
  • Might be similar to bunshi (formerly jotai-molecules)? I'm not entirely sure.

Current Progress:

It's not working 100% but I think I'm pretty close.

import { Atom, atom, Getter } from 'jotai'
import { isEqual } from 'lodash'

export const derivedAtomWithCompareAsync = <T>(
    read: (get: Getter) => Promise<T>,
    initialValue: T,
    areEqual?: (prev: any, next: any) => boolean,
): Atom<Promise<T>> => {
    let previousValue = initialValue
    areEqual = areEqual ?? ((prev, next) => isEqual(prev, next))

    const derivedAtom = atom(async (get) => {
        const next = await read(get)

        const arePreviousAndNextEqual = areEqual(previousValue, next)

        if (!arePreviousAndNextEqual) {
            previousValue = next

            return next
        }
        
        // this does not quite work yet, it still publishes changes to all subscribers, but I think I'm close
        return previousValue
    })

    return derivedAtom
}

Assignee:
@kevinschaich

@kevinschaich
Copy link
Contributor Author

cc: @dai-shi would love your thoughts and advice

@dmaskasky
Copy link
Collaborator

Nice RFC. I agree that compare functionality could be brought to derived atoms to make ergonomics better.

I would love to see this added as a utility to jotai-history. Feel free to submit a PR.

A few thoughts below:

  • previousValue should be in an atom so that the atom doesn't share states between stores.
const prevAtom = atom(() => { previousValue })
  • You can use withHistory from jotai-history to assist with prev/curr values.

  • I think it would be great if it could be something like withCompare and accepted both derived and primitive atoms. Then we could deprecate atomWithCompare.

@pmndrs pmndrs locked and limited conversation to collaborators Nov 3, 2024
@dai-shi dai-shi converted this issue into discussion #2796 Nov 3, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants