pmndrs / jotai

👻 Primitive and flexible state management for React
https://jotai.org
MIT License
18.63k stars 607 forks source link

Util RFC: `derivedAtomWithCompare` #2795

Closed kevinschaich closed 2 days ago

kevinschaich commented 2 days ago

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:

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.

https://github.com/pmndrs/jotai/issues/1158, https://github.com/pmndrs/jotai/issues/783, https://github.com/pmndrs/jotai/issues/324, https://github.com/pmndrs/jotai/issues/1175, https://github.com/pmndrs/jotai/issues/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

Differences from existing utils:

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 commented 2 days ago

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

dmaskasky commented 2 days ago

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:

const prevAtom = atom(() => { previousValue })