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

perf: avoid extra rerender #2558

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/react/useAtomValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export function useAtomValue<Value>(atom: Atom<Value>, options?: Options) {
}
rerender()
})
rerender()
if (!import.meta.env?.USE_STORE2) {
rerender()
}
return unsub
}, [store, atom, delay])

Expand Down
55 changes: 44 additions & 11 deletions src/vanilla/store2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ type AtomState<Value = AnyValue> = {
* The map value is the epoch number of the dependency.
*/
readonly d: Map<AnyAtom, number>
readonly t: Array<WeakRef<AnyAtom>>
/**
* Set of atoms with pending promise that depend on the atom.
*
Expand All @@ -146,6 +147,8 @@ type AtomState<Value = AnyValue> = {
v?: Value
/** Atom error */
e?: AnyError
/** Should update */
u?: boolean // only if the atom is unmounted but has been written state, the value is true.
}

const isAtomStateInitialized = <Value>(atomState: AtomState<Value>) =>
Expand Down Expand Up @@ -235,7 +238,13 @@ const flushPending = (pending: Pending) => {
pending[1].clear()
const functions = new Set(pending[2])
pending[2].clear()
atomStates.forEach((atomState) => atomState.m?.l.forEach((l) => l()))
atomStates.forEach((atomState) => {
if (atomState.m && atomState.m.l.size) {
atomState.m.l.forEach((l) => l())
} else {
atomState.u = true
}
})
functions.forEach((fn) => fn())
}
}
Expand All @@ -259,13 +268,24 @@ type Store = PrdStore | (PrdStore & DevStoreRev4)
export type INTERNAL_DevStoreRev4 = DevStoreRev4
export type INTERNAL_PrdStore = PrdStore

const addAtomRef = (atomState: AtomState, atom: AnyAtom) => {
if (atomState.m) {
atomState.m?.t.add(atom)
} else {
const index = atomState.t.findIndex((v) => v.deref() === atom)
if (index === -1) {
atomState.t.push(new WeakRef(atom))
}
}
}

export const createStore = (): Store => {
const atomStateMap = new WeakMap<AnyAtom, AtomState>()

const getAtomState = <Value>(atom: Atom<Value>) => {
let atomState = atomStateMap.get(atom) as AtomState<Value> | undefined
if (!atomState) {
atomState = { d: new Map(), p: new Set(), n: 0 }
atomState = { d: new Map(), p: new Set(), n: 0, t: [] }
atomStateMap.set(atom, atomState)
}
return atomState
Expand Down Expand Up @@ -335,7 +355,7 @@ export const createStore = (): Store => {
if (continuablePromise) {
addPendingContinuablePromiseToDependency(atom, continuablePromise, aState)
}
aState.m?.t.add(atom)
addAtomRef(aState, atom)
if (pending) {
addPendingDependent(pending, a, atom)
}
Expand Down Expand Up @@ -456,7 +476,11 @@ export const createStore = (): Store => {
const recomputeDependents = (pending: Pending, atom: AnyAtom) => {
const getDependents = (a: AnyAtom): Set<AnyAtom> => {
const aState = getAtomState(a)
const dependents = new Set(aState.m?.t)
const dependents: Set<AnyAtom> = new Set(
aState.m?.t.size
? aState.m.t
: aState.t.map((v) => v.deref()!).filter(Boolean),
)
for (const atomWithPendingContinuablePromise of aState.p) {
dependents.add(atomWithPendingContinuablePromise)
}
Expand Down Expand Up @@ -575,8 +599,9 @@ export const createStore = (): Store => {
if (atomState.m && !getPendingContinuablePromise(atomState)) {
for (const a of atomState.d.keys()) {
if (!atomState.m.d.has(a)) {
const aMounted = mountAtom(pending, a)
aMounted.t.add(atom)
mountAtom(pending, a)
const aState = getAtomState(a)
addAtomRef(aState, atom)
atomState.m.d.add(a)
}
}
Expand All @@ -597,8 +622,9 @@ export const createStore = (): Store => {
readAtomState(pending, atom)
// mount dependencies first
for (const a of atomState.d.keys()) {
const aMounted = mountAtom(pending, a)
aMounted.t.add(atom)
mountAtom(pending, a)
const aState = getAtomState(a)
addAtomRef(aState, atom)
}
// mount self
atomState.m = {
Expand All @@ -610,9 +636,10 @@ export const createStore = (): Store => {
const mounted = atomState.m
const { onMount } = atom
addPendingFunction(pending, () => {
const onUnmount = onMount((...args) =>
writeAtomState(pending, atom, ...args),
)
const onUnmount = onMount((...args) => {
atomState.u = true
return writeAtomState(pending, atom, ...args)
})
if (onUnmount) {
mounted.u = onUnmount
}
Expand All @@ -627,6 +654,7 @@ export const createStore = (): Store => {
atom: AnyAtom,
): Mounted | undefined => {
const atomState = getAtomState(atom)
atomState.t.length = 0
if (
atomState.m &&
!atomState.m.l.size &&
Expand Down Expand Up @@ -660,6 +688,11 @@ export const createStore = (): Store => {
flushPending(pending)
const listeners = mounted.l
listeners.add(listener)
const atomState = getAtomState(atom)
if (atomState.u) {
listener()
atomState.u = false
}
return () => {
listeners.delete(listener)
const pending = createPending()
Expand Down
23 changes: 23 additions & 0 deletions tests/react/optimization.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,26 @@ it('no extra rerenders after commit with derived atoms (#1213)', async () => {
})
expect(renderCount1).toBe(renderCount1AfterCommit)
})

it.skipIf(!import.meta.env?.USE_STORE2)(
'no extra rerenders after mounted (#2558)',
async () => {
const baseAtom = atom(0)

let renderCount = 0

const App = () => {
const [count] = useAtom(baseAtom)
++renderCount
return <div>count: {count}</div>
}

render(
<>
<App />
</>,
)

expect(renderCount).toBe(1)
},
)
9 changes: 9 additions & 0 deletions tests/vanilla/memoryleaks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ describe('test memory leaks (get & set only)', () => {
const detector2 = new LeakDetector(store.get(derivedAtom))
objAtom = undefined
derivedAtom = undefined
// WeapRef is different from WeakMap, we should call isLeaking twice to make garbage-collected success.
await detector1.isLeaking()
await detector2.isLeaking()
expect(await detector1.isLeaking()).toBe(false)
expect(await detector2.isLeaking()).toBe(false)
})
Expand All @@ -35,6 +38,7 @@ describe('test memory leaks (get & set only)', () => {
const detector = new LeakDetector(depAtom)
store.get(depAtom)
depAtom = undefined
await detector.isLeaking()
await expect(detector.isLeaking()).resolves.toBe(false)
},
)
Expand All @@ -49,6 +53,7 @@ describe('test memory leaks (get & set only)', () => {
}))
const detector = new LeakDetector(store.get(derivedAtom))
derivedAtom = undefined
await detector.isLeaking()
expect(await detector.isLeaking()).toBe(false)
},
)
Expand All @@ -63,6 +68,7 @@ describe('test memory leaks (with subscribe)', () => {
unsub()
unsub = undefined
objAtom = undefined
await detector.isLeaking()
expect(await detector.isLeaking()).toBe(false)
})

Expand All @@ -79,6 +85,8 @@ describe('test memory leaks (with subscribe)', () => {
unsub = undefined
objAtom = undefined
derivedAtom = undefined
await detector1.isLeaking()
await detector2.isLeaking()
expect(await detector1.isLeaking()).toBe(false)
expect(await detector2.isLeaking()).toBe(false)
})
Expand All @@ -94,6 +102,7 @@ describe('test memory leaks (with subscribe)', () => {
unsub()
unsub = undefined
derivedAtom = undefined
await detector.isLeaking()
expect(await detector.isLeaking()).toBe(false)
})
})