From 95d61a0f3ea008b3e57fde44207d69c162a8e0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Tue, 10 Nov 2026 16:09:27 +0100 Subject: [PATCH 1/2] Share a stable selector's result between useOnyxState hooks --- lib/Onyx.ts | 2 + lib/sharedResults.ts | 47 ++++++++++++++++ lib/useOnyxState.ts | 27 ++++++--- tests/unit/useOnyxStateTest.tsx | 99 +++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 lib/sharedResults.ts diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 3a05db78a..9bca0b8f6 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -34,6 +34,7 @@ import PendingWrites from './PendingWrites'; import logMessages from './logMessages'; import onyxSubscriptionManager from './OnyxSubscriptionManager'; import OnyxMerge from './OnyxMerge'; +import {clearSharedResults} from './sharedResults'; /** Initialize the store with actions and listening for storage events */ function init({ @@ -439,6 +440,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { const clearPromise = PendingWrites.trackPendingGlobalWrite( OnyxUtils.afterInit(() => { + clearSharedResults(); const pendingMergeEntries = OnyxUtils.getPendingMergeEntries(keysToPreserve); const defaultKeyStates = OnyxUtils.getDefaultKeyStates(); const initialKeys = Object.keys(defaultKeyStates); diff --git a/lib/sharedResults.ts b/lib/sharedResults.ts new file mode 100644 index 000000000..14a306ab5 --- /dev/null +++ b/lib/sharedResults.ts @@ -0,0 +1,47 @@ +import type {OnyxKey, OnyxValue} from './types'; + +type SharedResult = { + depsKey: string; + depValues: Array>; + value: unknown; + readPreviousState: boolean; + hadPreviousView: boolean; +}; + +// Results shared between hooks live in module state, so Onyx.clear drops them with the data they came from. +let sharedResults = new WeakMap(); +const derivedValueResets = new Set<() => void>(); + +function getSharedResult(selector: object): SharedResult | undefined { + return sharedResults.get(selector); +} + +function setSharedResult(selector: object, result: SharedResult) { + sharedResults.set(selector, result); +} + +function registerDerivedValueReset(reset: () => void) { + derivedValueResets.add(reset); +} + +function clearSharedResults() { + sharedResults = new WeakMap(); + for (const reset of derivedValueResets) { + reset(); + } +} + +function areSameValues(a: Array>, b: Array>): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!Object.is(a[i], b[i])) { + return false; + } + } + return true; +} + +export {areSameValues, clearSharedResults, getSharedResult, registerDerivedValueReset, setSharedResult}; +export type {SharedResult}; diff --git a/lib/useOnyxState.ts b/lib/useOnyxState.ts index 604198775..546259e2d 100644 --- a/lib/useOnyxState.ts +++ b/lib/useOnyxState.ts @@ -4,6 +4,7 @@ import {useCallback, useEffect, useMemo, useRef, useSyncExternalStore} from 'rea import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxKey, OnyxValue} from './types'; import onyxSubscriptionManager from './OnyxSubscriptionManager'; +import {areSameValues, getSharedResult, setSharedResult} from './sharedResults'; import OnyxUtils from './OnyxUtils'; /** @@ -215,18 +216,30 @@ function useOnyxState(selecto } const depValues = depsArray.map((dep) => onyxSubscriptionManager.getState(dep)); - previousStateReadRef.current = false; - // The views carry every key at runtime; the narrowing is a constraint on what a selector may - // read, so hand them over as the declared shape. - const output = selector(liveStateView as OnyxStateViewFor, previousView as OnyxStateViewFor | undefined); - const readPreviousState = previousStateReadRef.current; + + // Shared by stable selectors only. A run that read `previousState`, or saw it missing, depends on its own instance. + const hadPreviousView = previousView !== undefined; + const shared = getSharedResult(selector); + let output: T; + let readPreviousState: boolean; + if (shared && !shared.readPreviousState && shared.hadPreviousView === hadPreviousView && shared.depsKey === depsKey && areSameValues(shared.depValues, depValues)) { + output = shared.value as T; + readPreviousState = false; + } else { + previousStateReadRef.current = false; + // The views carry every key at runtime; the narrowing is a constraint on what a selector may + // read, so hand them over as the declared shape. + output = selector(liveStateView as OnyxStateViewFor, previousView as OnyxStateViewFor | undefined); + readPreviousState = previousStateReadRef.current; + setSharedResult(selector, {depsKey, depValues, value: output, readPreviousState, hadPreviousView}); + } // Hand back the previous reference when the output is equal, so React bails out via `===` // and an unchanged store keeps returning the same value even as `getSnapshot`'s identity moves. const value = lastSnapshot !== null && selectorEquality(lastSnapshot.value, output) ? lastSnapshot.value : output; lastSnapshotRef.current = {value, selector, previousView, readPreviousState, depValues}; return value; - }, [selector, selectorEquality, depsArray]); + }, [selector, selectorEquality, depsArray, depsKey]); const value = useSyncExternalStore(subscribe, getSnapshot); @@ -250,4 +263,4 @@ function useOnyxState(selecto } export default useOnyxState; -export type {OnyxStateView, UseOnyxStateOptions, UseOnyxStateSelector}; +export type {OnyxStateView, OnyxStateViewFor, UseOnyxStateOptions, UseOnyxStateSelector}; diff --git a/tests/unit/useOnyxStateTest.tsx b/tests/unit/useOnyxStateTest.tsx index 38f54bfaf..51f99f10d 100644 --- a/tests/unit/useOnyxStateTest.tsx +++ b/tests/unit/useOnyxStateTest.tsx @@ -303,6 +303,105 @@ describe('useOnyxState', () => { }); }); + describe('sharing results between instances', () => { + it('should run a stable selector once per change for every instance that mounts it', async () => { + // Given three components that mount the same module-level selector + const selectTestValue = jest.fn((state: OnyxStateView) => `${state[ONYXKEYS.TEST_KEY] as string}!`); + await Onyx.set(ONYXKEYS.TEST_KEY, 'first'); + const hooks = [1, 2, 3].map(() => renderHook(() => useOnyxState(selectTestValue, {dependencies: [ONYXKEYS.TEST_KEY]}))); + await act(async () => waitForPromisesToResolve()); + selectTestValue.mockClear(); + + // When the dependency changes + await act(async () => Onyx.set(ONYXKEYS.TEST_KEY, 'second')); + + // Then the selector runs once and every instance gets its result + expect(selectTestValue).toHaveBeenCalledTimes(1); + for (const {result} of hooks) { + expect(result.current).toEqual('second!'); + } + }); + + it('should still run an inline selector in every instance', async () => { + // Given three components that each pass their own inline selector + const runs = jest.fn(); + await Onyx.set(ONYXKEYS.TEST_KEY, 'first'); + for (let index = 0; index < 3; index++) { + renderHook(() => + useOnyxState( + (state) => { + runs(); + return state[ONYXKEYS.TEST_KEY]; + }, + {dependencies: [ONYXKEYS.TEST_KEY]}, + ), + ); + } + await act(async () => waitForPromisesToResolve()); + runs.mockClear(); + + // When the dependency changes + await act(async () => Onyx.set(ONYXKEYS.TEST_KEY, 'second')); + + // Then nothing identifies the three selectors as the same, so each instance runs its own + expect(runs.mock.calls.length).toBeGreaterThanOrEqual(3); + }); + + it('should not share a stable selector between different dependency lists', async () => { + // Given the same selector mounted over two different dependency lists + const selectBoth = jest.fn((state: OnyxStateView) => `${state[ONYXKEYS.TEST_KEY] as string}|${state[ONYXKEYS.OTHER_TEST] as string}`); + await Onyx.set(ONYXKEYS.TEST_KEY, 'a'); + await Onyx.set(ONYXKEYS.OTHER_TEST, 'x'); + const narrow = renderHook(() => useOnyxState(selectBoth, {dependencies: [ONYXKEYS.TEST_KEY]})); + const wide = renderHook(() => useOnyxState(selectBoth, {dependencies: [ONYXKEYS.TEST_KEY, ONYXKEYS.OTHER_TEST]})); + await act(async () => waitForPromisesToResolve()); + + // When only the key the wider list declares changes + await act(async () => Onyx.set(ONYXKEYS.OTHER_TEST, 'y')); + + // Then the wider one sees the change, and the narrower one keeps what its own deps produced + expect(wide.result.current).toEqual('a|y'); + expect(narrow.result.current).toEqual('a|x'); + }); + + it('should not share the result of a run that read previousState', async () => { + // Given two components that mount the same selector, which reads previousState + const selectChange = jest.fn( + (state: OnyxStateView, previousState: OnyxStateView | undefined) => `${previousState?.[ONYXKEYS.TEST_KEY] as string}->${state[ONYXKEYS.TEST_KEY] as string}`, + ); + await Onyx.set(ONYXKEYS.TEST_KEY, 'first'); + renderHook(() => useOnyxState(selectChange, {dependencies: [ONYXKEYS.TEST_KEY]})); + renderHook(() => useOnyxState(selectChange, {dependencies: [ONYXKEYS.TEST_KEY]})); + await act(async () => waitForPromisesToResolve()); + selectChange.mockClear(); + + // When the dependency changes + await act(async () => Onyx.set(ONYXKEYS.TEST_KEY, 'second')); + + // Then each instance runs it against its own previous state + expect(selectChange).toHaveBeenCalledTimes(2); + }); + + it('should not hand a run made without previousState to an instance that has one', async () => { + // Given a selector that reads previousState, mounted in A, which has seen a change + const selectChange = (state: OnyxStateView, previousState: OnyxStateView | undefined) => `${previousState?.[ONYXKEYS.TEST_KEY] as string}->${state[ONYXKEYS.TEST_KEY] as string}`; + await Onyx.set(ONYXKEYS.TEST_KEY, 'first'); + const a = renderHook(() => useOnyxState(selectChange, {dependencies: [ONYXKEYS.TEST_KEY]})); + await act(async () => waitForPromisesToResolve()); + await act(async () => Onyx.set(ONYXKEYS.TEST_KEY, 'second')); + + // When B mounts with no previousState yet, and A re-renders + const b = renderHook(() => useOnyxState(selectChange, {dependencies: [ONYXKEYS.TEST_KEY]})); + await act(async () => waitForPromisesToResolve()); + a.rerender({}); + await act(async () => waitForPromisesToResolve()); + + // Then A still diffs against its own previous state instead of taking B's result + expect(b.result.current).toEqual('undefined->second'); + expect(a.result.current).toEqual('second->second'); + }); + }); + describe('before Onyx.init has finished', () => { // Providers above the app's migration gate subscribe during a cold start, before the cache has // been hydrated, so put Onyx back to "not initialised" for these. From 3447ecca2822286890eb3ba41f2d2f05811e4385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Tue, 10 Nov 2026 16:09:27 +0100 Subject: [PATCH 2/2] Add createDerivedValue for values computed once for every useOnyxState hook --- lib/createDerivedValue.ts | 93 +++++++++++ lib/index.ts | 7 +- tests/unit/createDerivedValueTest.tsx | 219 ++++++++++++++++++++++++++ 3 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 lib/createDerivedValue.ts create mode 100644 tests/unit/createDerivedValueTest.tsx diff --git a/lib/createDerivedValue.ts b/lib/createDerivedValue.ts new file mode 100644 index 000000000..fd89ee607 --- /dev/null +++ b/lib/createDerivedValue.ts @@ -0,0 +1,93 @@ +import type {OnyxKey, OnyxValue} from './types'; +import type {OnyxStateViewFor} from './useOnyxState'; + +import {areSameValues, registerDerivedValueReset} from './sharedResults'; + +type DerivedValuePrevious = { + /** The dependencies behind `result`, so `compute` can diff them against the current ones. */ + state: OnyxStateViewFor; + + result: T; + + /** Returns `result` and changes nothing, so a compute that waits or fails runs again on the next `get`. */ + keep: () => T; +}; + +type CreateDerivedValueOptions = { + /** Keys the value is computed from. Every `useOnyxState` hook that calls `get` must declare at least these. */ + dependencies: TDependencies; + + /** Annotate the return type when reading `previous`, or `T` is inferred as `unknown`. `previous` is undefined on the first compute. */ + compute: (state: OnyxStateViewFor, previous: DerivedValuePrevious | undefined) => T; +}; + +type DerivedValue = { + dependencies: TDependencies; + + /** Pass the `state` a `useOnyxState` selector gets, never `previousState`. */ + get: (state: OnyxStateViewFor) => T; + + /** Makes the next `get` compute from scratch. Onyx.clear calls it too. */ + reset: () => void; +}; + +type Computed = { + depValues: Array>; + result: T; +}; + +/** A value derived from several keys, computed once per change for every `useOnyxState` selector that calls `get`, inline ones included. */ +function createDerivedValue({dependencies, compute}: CreateDerivedValueOptions): DerivedValue { + let lastRead: Computed | undefined; + // What `compute` diffs against, which is the last compute that did not call `keep`. + let baseline: Computed | undefined; + // Bumped by `reset`, so a compute that called it does not write its result back as the next baseline. + let generation = 0; + + function get(state: OnyxStateViewFor): T { + const view = state as Record>; + const depValues = dependencies.map((key) => view[key]); + if (lastRead && areSameValues(depValues, lastRead.depValues)) { + return lastRead.result; + } + + let isKept = false; + let previous: DerivedValuePrevious | undefined; + if (baseline) { + const kept = baseline; + const previousState: Record> = {}; + for (let index = 0; index < dependencies.length; index++) { + previousState[dependencies[index]] = kept.depValues[index]; + } + previous = { + state: Object.freeze(previousState) as OnyxStateViewFor, + result: kept.result, + keep: () => { + isKept = true; + return kept.result; + }, + }; + } + + const computeGeneration = generation; + const result = compute(state, previous); + if (!isKept && computeGeneration === generation) { + lastRead = {depValues, result}; + baseline = {depValues, result}; + } + return result; + } + + function reset() { + generation++; + lastRead = undefined; + baseline = undefined; + } + + registerDerivedValueReset(reset); + + return {dependencies, get, reset}; +} + +export default createDerivedValue; +export type {CreateDerivedValueOptions, DerivedValue, DerivedValuePrevious}; diff --git a/lib/index.ts b/lib/index.ts index 2c7e60a3b..fb3aee001 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -22,10 +22,12 @@ import type {FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions} from '. import useOnyx from './useOnyx'; import type {OnyxStateView, UseOnyxStateOptions, UseOnyxStateSelector} from './useOnyxState'; import useOnyxState from './useOnyxState'; +import type {CreateDerivedValueOptions, DerivedValue, DerivedValuePrevious} from './createDerivedValue'; +import createDerivedValue from './createDerivedValue'; import type {OnyxSQLiteKeyValuePair} from './storage/providers/SQLiteProvider'; export default Onyx; -export {useOnyx, useOnyxState}; +export {useOnyx, useOnyxState, createDerivedValue}; export type { ConnectOptions, CustomTypeOptions, @@ -53,5 +55,8 @@ export type { OnyxStateView, UseOnyxStateOptions, UseOnyxStateSelector, + CreateDerivedValueOptions, + DerivedValue, + DerivedValuePrevious, OnyxSQLiteKeyValuePair, }; diff --git a/tests/unit/createDerivedValueTest.tsx b/tests/unit/createDerivedValueTest.tsx new file mode 100644 index 000000000..a4679e4cd --- /dev/null +++ b/tests/unit/createDerivedValueTest.tsx @@ -0,0 +1,219 @@ +import {act, renderHook} from '@testing-library/react-native'; + +import type {OnyxStateView} from '../../lib/useOnyxState'; +import type GenericCollection from '../utils/GenericCollection'; + +import Onyx from '../../lib'; +import createDerivedValue from '../../lib/createDerivedValue'; +import useOnyxState from '../../lib/useOnyxState'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; + +const ONYXKEYS = { + TEST_KEY: 'test', + OTHER_TEST: 'otherTest', + COLLECTION: { + TEST_KEY: 'test_', + }, +}; + +const COLLECTION = ONYXKEYS.COLLECTION.TEST_KEY; + +Onyx.init({ + keys: ONYXKEYS, +}); + +beforeEach(() => Onyx.clear()); + +/** A state view built by hand, for calling `get` without a hook. */ +function stateOf(values: Record): OnyxStateView { + return values as OnyxStateView; +} + +describe('createDerivedValue', () => { + it('should compute once per change for every component that reads it, even through inline selectors', async () => { + // Given a derived value and three components that each pick a different member from it inline + const compute = jest.fn((state: OnyxStateView) => { + const items = (state[COLLECTION] ?? {}) as Record; + const names: Record = {}; + for (const [key, item] of Object.entries(items)) { + names[key] = item.name.toUpperCase(); + } + return names; + }); + const names = createDerivedValue({dependencies: [COLLECTION], compute}); + await Onyx.mergeCollection(COLLECTION, {[`${COLLECTION}1`]: {name: 'a'}, [`${COLLECTION}2`]: {name: 'b'}, [`${COLLECTION}3`]: {name: 'c'}} as GenericCollection); + const hooks = [1, 2, 3].map((id) => renderHook(() => useOnyxState((state) => names.get(state)[`${COLLECTION}${id}`], {dependencies: names.dependencies}))); + await act(async () => waitForPromisesToResolve()); + compute.mockClear(); + + // When one member changes + await act(async () => Onyx.merge(`${COLLECTION}2`, {name: 'bb'})); + + // Then the value is computed once and every component reads its own part of it + expect(compute).toHaveBeenCalledTimes(1); + expect(hooks.map(({result}) => result.current)).toEqual(['A', 'BB', 'C']); + }); + + it('should hand compute the previous dependencies and result, so it can update only what changed', async () => { + // Given a derived value that copies the previous result and recomputes only the changed members + const recomputed: string[][] = []; + const names = createDerivedValue({ + dependencies: [COLLECTION], + compute: (state, previous): Record => { + const items = (state[COLLECTION] ?? {}) as Record; + const previousItems = (previous?.state[COLLECTION] ?? {}) as Record; + const result = previous ? {...previous.result} : {}; + const changed: string[] = []; + for (const [key, item] of Object.entries(items)) { + if (!previous || item !== previousItems[key]) { + result[key] = item?.name.toUpperCase() ?? ''; + changed.push(key); + } + } + recomputed.push(changed); + return result; + }, + }); + await Onyx.mergeCollection(COLLECTION, {[`${COLLECTION}1`]: {name: 'a'}, [`${COLLECTION}2`]: {name: 'b'}} as GenericCollection); + const {result} = renderHook(() => useOnyxState((state) => names.get(state), {dependencies: names.dependencies})); + await act(async () => waitForPromisesToResolve()); + + // When one member changes + await act(async () => Onyx.merge(`${COLLECTION}2`, {name: 'bb'})); + + // Then the first compute covers everything and the second only the changed member + expect(recomputed).toEqual([[`${COLLECTION}1`, `${COLLECTION}2`], [`${COLLECTION}2`]]); + expect(result.current).toEqual({[`${COLLECTION}1`]: 'A', [`${COLLECTION}2`]: 'BB'}); + }); + + it('should not compute again while its dependencies keep their references', () => { + // Given a derived value of one key + const compute = jest.fn((state: OnyxStateView) => state[ONYXKEYS.TEST_KEY]); + const value = createDerivedValue({dependencies: [ONYXKEYS.TEST_KEY], compute}); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'a', [ONYXKEYS.OTHER_TEST]: 'x'})); + + // When it is read again after only a key outside its dependencies changed + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'a', [ONYXKEYS.OTHER_TEST]: 'y'})); + + // Then the first result is reused + expect(compute).toHaveBeenCalledTimes(1); + }); + + it('should move the previous dependencies forward after a compute that returned the same result', () => { + // Given a derived value whose output ignores one of its dependencies + const seenPrevious: unknown[] = []; + const value = createDerivedValue({ + dependencies: [ONYXKEYS.TEST_KEY, ONYXKEYS.OTHER_TEST], + compute: (state, previous): string => { + seenPrevious.push(previous?.state[ONYXKEYS.OTHER_TEST]); + return previous?.result ?? 'constant'; + }, + }); + value.get(stateOf({[ONYXKEYS.OTHER_TEST]: 'w'})); + + // When the ignored dependency changes twice + value.get(stateOf({[ONYXKEYS.OTHER_TEST]: 'x'})); + value.get(stateOf({[ONYXKEYS.OTHER_TEST]: 'y'})); + + // Then each compute diffs against the one before it, not against the first + expect(seenPrevious).toEqual([undefined, 'w', 'x']); + }); + + it('should diff against the last real compute and run again after compute calls keep', () => { + // Given a derived value that waits while a flag is set + const seenPrevious: unknown[] = []; + const value = createDerivedValue({ + dependencies: [ONYXKEYS.TEST_KEY, ONYXKEYS.OTHER_TEST], + compute: (state, previous): string => { + seenPrevious.push(previous?.state[ONYXKEYS.TEST_KEY]); + if (previous && state[ONYXKEYS.OTHER_TEST] === 'wait') { + return previous.keep(); + } + return state[ONYXKEYS.TEST_KEY] as string; + }, + }); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'a'})); + + // When the value changes while it waits, is read twice, and then the wait ends + const waiting = stateOf({[ONYXKEYS.TEST_KEY]: 'b', [ONYXKEYS.OTHER_TEST]: 'wait'}); + value.get(waiting); + value.get(waiting); + const result = value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'b', [ONYXKEYS.OTHER_TEST]: 'go'})); + + // Then every read while waiting runs it again, and the compute after the wait still diffs against 'a' + expect(seenPrevious).toEqual([undefined, 'a', 'a', 'a']); + expect(result).toEqual('b'); + }); + + it('should run again after compute throws, from the last good result', () => { + // Given a derived value whose compute fails once + const seenPrevious: unknown[] = []; + let shouldThrow = false; + const value = createDerivedValue({ + dependencies: [ONYXKEYS.TEST_KEY], + compute: (state, previous): string => { + seenPrevious.push(previous?.result); + if (shouldThrow) { + shouldThrow = false; + throw new Error('compute failed'); + } + return state[ONYXKEYS.TEST_KEY] as string; + }, + }); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'a'})); + + // When the next compute throws and the same state is read again + const changed = stateOf({[ONYXKEYS.TEST_KEY]: 'b'}); + shouldThrow = true; + expect(() => value.get(changed)).toThrow('compute failed'); + const result = value.get(changed); + + // Then the retry computes again from 'a' and succeeds + expect(seenPrevious).toEqual([undefined, 'a', 'a']); + expect(result).toEqual('b'); + }); + + it('should compute from scratch after reset, also when compute called it', () => { + // Given a derived value that resets itself when it sees an empty value + const seenPrevious: boolean[] = []; + const value = createDerivedValue({ + dependencies: [ONYXKEYS.TEST_KEY], + compute: (state, previous): string => { + seenPrevious.push(previous !== undefined); + if (!state[ONYXKEYS.TEST_KEY]) { + value.reset(); + return ''; + } + return state[ONYXKEYS.TEST_KEY] as string; + }, + }); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'a'})); + + // When it resets itself and then gets a value again + value.get(stateOf({[ONYXKEYS.TEST_KEY]: ''})); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'b'})); + + // Then the compute after the reset gets no previous result + expect(seenPrevious).toEqual([false, true, false]); + }); + + it('should compute from scratch after Onyx.clear', async () => { + // Given a derived value that was already computed + const seenPrevious: boolean[] = []; + const value = createDerivedValue({ + dependencies: [ONYXKEYS.TEST_KEY], + compute: (state, previous): unknown => { + seenPrevious.push(previous !== undefined); + return state[ONYXKEYS.TEST_KEY]; + }, + }); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'a'})); + + // When Onyx is cleared, say on sign-out, and the value is read again + await Onyx.clear(); + value.get(stateOf({[ONYXKEYS.TEST_KEY]: 'b'})); + + // Then nothing from before the clear is carried over + expect(seenPrevious).toEqual([false, false]); + }); +});