diff --git a/README.md b/README.md index 9220e9eb1..167d85d07 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,33 @@ API.Authenticate(params) The data will then be cached and stored via [`AsyncStorage`](https://github.com/react-native-async-storage/async-storage). +### Performance Options for Large Objects + +For performance-critical scenarios with large objects, `Onyx.set()` accepts optional flags to skip expensive operations: + +```javascript +Onyx.set(ONYXKEYS.LARGE_DATA, computedValue, { + skipCacheCheck: true, // Skip deep equality check + skipNullRemoval: true, // Skip null value pruning +}); +``` + +**Options:** +- `skipCacheCheck`: Skips the deep equality comparison with the cached value. By default, Onyx compares new values against cached ones to avoid unnecessary updates. For large objects, this comparison can be expensive. +- `skipNullRemoval`: Skips the removal of `null` values from nested objects. By default, Onyx removes `null` values before storage. Use this when `null` values are meaningful in your data structure. + +#### When to Use SetOptions +- **Use `skipCacheCheck: true`** for: + - Large objects where deep equality checking is expensive + - Values that you know have changed + +- **Use `skipNullRemoval: true`** for: + - Computed values where `null` represents a legitimate result + - Data structures where `null` has semantic meaning + - Values that should preserve their exact structure + +**Note**: These options are recommended only for large objects where performance is critical. Most use cases should use the standard `Onyx.set(key, value)` syntax. + ## Merging data We can also use `Onyx.merge()` to merge new `Object` or `Array` data in with existing data. diff --git a/lib/Onyx.ts b/lib/Onyx.ts index a656f4fad..39215bfa9 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -24,6 +24,7 @@ import type { OnyxValue, OnyxInput, OnyxMethodMap, + SetOptions, } from './types'; import OnyxUtils from './OnyxUtils'; import logMessages from './logMessages'; @@ -130,8 +131,9 @@ function disconnect(connection: Connection): void { * * @param key ONYXKEY to set * @param value value to store + * @param options optional configuration object */ -function set(key: TKey, value: OnyxSetInput): Promise { +function set(key: TKey, value: OnyxSetInput, options?: SetOptions): Promise { // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. if (OnyxUtils.hasPendingMergeForKey(key)) { @@ -178,8 +180,8 @@ function set(key: TKey, value: OnyxSetInput): Promis return Promise.resolve(); } - const valueWithoutNestedNullValues = utils.removeNestedNullValues(value) as OnyxValue; - const hasChanged = cache.hasValueChanged(key, valueWithoutNestedNullValues); + const valueWithoutNestedNullValues = (options?.skipNullRemoval ? value : utils.removeNestedNullValues(value)) as OnyxValue; + const hasChanged = options?.skipCacheCheck ? true : cache.hasValueChanged(key, valueWithoutNestedNullValues); OnyxUtils.logKeyChanged(OnyxUtils.METHOD.SET, key, value, hasChanged); @@ -732,4 +734,4 @@ function applyDecorators() { } export default Onyx; -export type {OnyxUpdate, Mapping, ConnectOptions}; +export type {OnyxUpdate, Mapping, ConnectOptions, SetOptions}; diff --git a/lib/types.ts b/lib/types.ts index 5af416974..aede5207b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -438,6 +438,17 @@ type OnyxUpdate = { } & OnyxMethodValueMap[Method]; }[OnyxMethod]; +/** + * Represents the options used in `Onyx.set()` method. + */ +type SetOptions = { + /** Skip the deep equality check against the cached value. Improves performance for large objects. */ + skipCacheCheck?: boolean; + + /** Skip pruning null values from the object. Improves performance for large objects. */ + skipNullRemoval?: boolean; +}; + /** * Represents the options used in `Onyx.init()` method. */ @@ -548,6 +559,7 @@ export type { OnyxUpdate, OnyxValue, Selector, + SetOptions, WithOnyxConnectOptions, MultiMergeReplaceNullPatches, MixedOperationsQueue, diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 5f4604950..53fd7fc02 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -2318,6 +2318,125 @@ describe('Onyx', () => { }); }); + describe('set', () => { + it('should work with skipCacheCheck option', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testData = {id: 1, name: 'test'}; + + return Onyx.set(ONYX_KEYS.TEST_KEY, testData) + .then(() => { + expect(testKeyValue).toEqual(testData); + + return Onyx.set(ONYX_KEYS.TEST_KEY, testData, {skipCacheCheck: true}); + }) + .then(() => { + expect(testKeyValue).toEqual(testData); + }); + }); + + it('should work with skipNullRemoval option', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testDataWithNulls = { + id: 1, + name: 'test', + nested: { + validValue: 'keep', + nullValue: null, + undefinedValue: undefined, + }, + topLevelNull: null, + }; + + return Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls, {skipNullRemoval: true}).then(() => { + // The null values should be preserved + expect(testKeyValue).toEqual(testDataWithNulls); + }); + }); + + it('should remove null values by default when skipNullRemoval is not set', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testDataWithNulls = { + id: 1, + name: 'test', + nested: { + validValue: 'keep', + nullValue: null, + undefinedValue: undefined, + }, + topLevelNull: null, + }; + + // Set value without skipNullRemoval (default behavior) + return Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls).then(() => { + // The null values should be removed + expect(testKeyValue).toEqual({ + id: 1, + name: 'test', + nested: { + validValue: 'keep', + }, + }); + }); + }); + + it('should work with both skipCacheCheck and skipNullRemoval options', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testDataWithNulls = { + id: 1, + name: 'test', + computed: null, + nested: { + result: null, + valid: 'keep', + }, + }; + + Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls, { + skipCacheCheck: true, + skipNullRemoval: true, + }).then(() => { + // The null values should be preserved + expect(testKeyValue).toEqual(testDataWithNulls); + }); + }); + }); + describe('setCollection', () => { it('should replace all existing collection members with new values and remove old ones', async () => { let result: OnyxCollection;