diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 5d2acdec..65a932dc 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -575,7 +575,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { // since collection key subscribers need to be updated differently if (!isKeyToPreserve) { const oldValue = cache.get(key); - const newValue = defaultKeyStates[key] ?? undefined; + const newValue = defaultKeyStates[key] ?? null; if (newValue !== oldValue) { cache.set(key, newValue); const collectionKey = key.substring(0, key.indexOf('_') + 1); @@ -583,9 +583,9 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { if (!keyValuesToResetAsCollection[collectionKey]) { keyValuesToResetAsCollection[collectionKey] = {}; } - keyValuesToResetAsCollection[collectionKey]![key] = newValue; + keyValuesToResetAsCollection[collectionKey]![key] = newValue ?? undefined; } else { - keyValuesToResetIndividually[key] = newValue; + keyValuesToResetIndividually[key] = newValue ?? undefined; } } } diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 230cbafa..45db3d78 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -6,6 +6,7 @@ import type {CollectionKeyBase, OnyxCollection, OnyxKey, OnyxValue, Selector} fr import useLiveRef from './useLiveRef'; import usePrevious from './usePrevious'; import Onyx from './Onyx'; +import cache from './OnyxCache'; type BaseUseOnyxOptions = { /** @@ -54,7 +55,7 @@ type ResultMetadata = { type UseOnyxResult = [CachedValue, ResultMetadata]; function getCachedValue(key: TKey, selector?: Selector): CachedValue | undefined { - return OnyxUtils.tryGetCachedValue(key, {selector}) as CachedValue | undefined; + return (OnyxUtils.tryGetCachedValue(key, {selector}) ?? undefined) as CachedValue | undefined; } function useOnyx>( @@ -73,8 +74,8 @@ function useOnyx>(key: TKey const selectorRef = useLiveRef(options?.selector); // Stores the previous cached value as it's necessary to compare with the new value in `getSnapshot()`. - // We initialize it to `undefined` to simulate that we don't have any value from cache yet. - const cachedValueRef = useRef | undefined>(undefined); + // We initialize it to `null` to simulate that we don't have any value from cache yet. + const cachedValueRef = useRef | undefined | null>(null); // Stores the previously result returned by the hook, containing the data from cache and the fetch status. // We initialize it to `undefined` and `loading` fetch status to simulate the initial result when the hook is loading from the cache. @@ -117,10 +118,11 @@ function useOnyx>(key: TKey const getSnapshot = useCallback(() => { // We get the value from the cache, supplying a selector too in case it's defined. // If `newValue` is `undefined` it means that the cache doesn't have a value for that key yet. - // If `newValue` is `null` or any other value if means that the cache does have a value for that key. + // If `newValue` is `null` or any other value it means that the cache does have a value for that key. // This difference between `undefined` and other values is crucial and it's used to address the following // conditions and use cases. let newValue = getCachedValue(key, selectorRef.current); + const hasCacheForKey = cache.hasCacheForKey(key); // Since the fetch status can be different given the use cases below, we define the variable right away. let newFetchStatus: FetchStatus | undefined; @@ -133,20 +135,20 @@ function useOnyx>(key: TKey newFetchStatus = 'loading'; } - // If data is not present in cache (if it's `undefined`) and `initialValue` is set during the first connection, + // If data is not present in cache and `initialValue` is set during the first connection, // we set the new value to `initialValue` and fetch status to `loaded` since we already have some data to return to the consumer. - if (isFirstConnectionRef.current && newValue === undefined && options?.initialValue !== undefined) { - newValue = options?.initialValue as CachedValue; + if (isFirstConnectionRef.current && !hasCacheForKey && options?.initialValue !== undefined) { + newValue = (options?.initialValue ?? undefined) as CachedValue; newFetchStatus = 'loaded'; } // If the previously cached value is different from the new value, we update both cached value // and the result to be returned by the hook. - if (!deepEqual(cachedValueRef.current, newValue)) { + // If the cache was set for the first time, we also update the cached value and the result. + const isCacheSetFirstTime = cachedValueRef.current === null && hasCacheForKey; + if (isCacheSetFirstTime || !deepEqual(cachedValueRef.current ?? undefined, newValue)) { cachedValueRef.current = newValue; - - // If the new value is `null` we default it to `undefined` to ensure the consumer get a consistent result from the hook. - resultRef.current = [(cachedValueRef.current ?? undefined) as CachedValue, {status: newFetchStatus ?? 'loaded'}]; + resultRef.current = [cachedValueRef.current as CachedValue, {status: newFetchStatus ?? 'loaded'}]; } return resultRef.current; diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 04160c51..bd9a4430 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -97,7 +97,7 @@ describe('useOnyx', () => { expect(result.current[1].status).toEqual('loaded'); }); - it('should initially return null while loading non-cached key, and then return value and loaded state', async () => { + it('should initially return `undefined` while loading non-cached key, and then return value and loaded state', async () => { await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); @@ -244,8 +244,6 @@ describe('useOnyx', () => { }); it('should return initial value if selected data is undefined', async () => { - Onyx.set(ONYXKEYS.TEST_KEY, 'test_id_1'); - const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { // @ts-expect-error bypass @@ -257,6 +255,8 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual('initial value'); expect(result.current[1].status).toEqual('loaded'); + Onyx.set(ONYXKEYS.TEST_KEY, 'test_id_1'); + await act(async () => waitForPromisesToResolve()); expect(result.current[0]).toBeUndefined(); @@ -320,12 +320,6 @@ describe('useOnyx', () => { }); it('should return initial value and loaded state while we have pending merges for the key, and then return updated value and loaded state', async () => { - Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); - - Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); - Onyx.merge(ONYXKEYS.TEST_KEY, 'test3'); - Onyx.merge(ONYXKEYS.TEST_KEY, 'test4'); - const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { initialValue: 'initial value', @@ -335,6 +329,12 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual('initial value'); expect(result.current[1].status).toEqual('loaded'); + Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); + + Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test3'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test4'); + await act(async () => waitForPromisesToResolve()); expect(result.current[0]).toEqual('test4'); @@ -361,7 +361,7 @@ describe('useOnyx', () => { }); describe('initWithStoredValues', () => { - it('should return null and loaded state, and after merge return updated value and loaded state', async () => { + it('should return `undefined` and loaded state, and after merge return updated value and loaded state', async () => { await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initWithStoredValues: false}));