From 737b7c53eed739fbd30ee3deb2807e16cf2e7e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Apr 2024 11:56:49 +0100 Subject: [PATCH 01/96] Migrate file to TS --- lib/{withOnyx.js => withOnyx.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/{withOnyx.js => withOnyx.tsx} (100%) diff --git a/lib/withOnyx.js b/lib/withOnyx.tsx similarity index 100% rename from lib/withOnyx.js rename to lib/withOnyx.tsx From c784b784830dce85eeb079603919962bce7192cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Apr 2024 19:50:36 +0100 Subject: [PATCH 02/96] Start migration of withOnyx --- lib/utils.ts | 48 ++++++- lib/withOnyx.tsx | 360 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 306 insertions(+), 102 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index e29e472f..a020b415 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -117,4 +117,50 @@ function formatActionName(method: string, key?: OnyxKey): string { return key ? `${method.toUpperCase()}/${key}` : method.toUpperCase(); } -export default {isEmptyObject, fastMerge, formatActionName, removeNestedNullValues}; +function pick(obj: Record, condition: string | string[] | ((entry: [string, T]) => boolean)): Record { + const result: Record = {}; + + const entries = Object.entries(obj); + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i]; + + if (Array.isArray(condition)) { + if (condition.includes(key)) { + result[key] = value; + } + } else if (typeof condition === 'string') { + if (key === condition) { + result[key] = value; + } + } else if (condition(entries[i])) { + result[key] = value; + } + } + + return result; +} + +function omit(obj: Record, condition: string | string[] | ((entry: [string, T]) => boolean)): Record { + const result: Record = {}; + + const entries = Object.entries(obj); + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i]; + + if (Array.isArray(condition)) { + if (!condition.includes(key)) { + result[key] = value; + } + } else if (typeof condition === 'string') { + if (key !== condition) { + result[key] = value; + } + } else if (!condition(entries[i])) { + result[key] = value; + } + } + + return result; +} + +export default {isEmptyObject, fastMerge, formatActionName, removeNestedNullValues, pick, omit}; diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index 2ea6dbb5..01189800 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -5,11 +5,147 @@ */ import PropTypes from 'prop-types'; import React from 'react'; +import type {IsEqual} from 'type-fest'; import _ from 'underscore'; import Onyx from './Onyx'; +import OnyxUtils from './OnyxUtils'; import * as Str from './Str'; +import type {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector, WithOnyxConnectOptions} from './types'; import utils from './utils'; -import OnyxUtils from './OnyxUtils'; + +/** + * Represents the base mapping options between an Onyx key and the component's prop. + */ +type BaseMapping = { + canEvict?: boolean | ((props: Omit) => boolean); + initWithStoredValues?: boolean; + allowStaleData?: boolean; +}; + +type CollectionBaseMapping = { + initialValue?: OnyxCollection; +}; + +type EntryBaseMapping = { + initialValue?: OnyxEntry; +}; + +/** + * Represents the string / function `key` mapping option between an Onyx key and the component's prop. + * + * If `key` is `string`, the type of the Onyx value that is associated with `key` must match with the type of the component's prop, + * otherwise an error will be thrown. + * + * If `key` is `function`, the return type of `key` function must be a valid Onyx key and the type of the Onyx value associated + * with `key` must match with the type of the component's prop, otherwise an error will be thrown. + * + * @example + * ```ts + * // Onyx prop with `string` key + * onyxProp: { + * key: ONYXKEYS.ACCOUNT, + * }, + * + * // Onyx prop with `function` key + * onyxProp: { + * key: ({reportId}) => ONYXKEYS.ACCOUNT, + * }, + * ``` + */ +type BaseMappingKey = IsEqual extends true + ? { + key: TOnyxKey | ((props: Omit & Partial) => TOnyxKey); + } + : never; + +/** + * Represents the string `key` and `selector` mapping options between an Onyx key and the component's prop. + * + * The function signature and return type of `selector` must match with the type of the component's prop, + * otherwise an error will be thrown. + * + * @example + * ```ts + * // Onyx prop with `string` key and selector + * onyxProp: { + * key: ONYXKEYS.ACCOUNT, + * selector: (value: Account | null): string => value?.id ?? '', + * }, + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type BaseMappingStringKeyAndSelector = { + key: TOnyxKey; + selector: Selector; +}; + +/** + * Represents the function `key` and `selector` mapping options between an Onyx key and the component's prop. + * + * The function signature and return type of `selector` must match with the type of the component's prop, + * otherwise an error will be thrown. + * + * @example + * ```ts + * // Onyx prop with `function` key and selector + * onyxProp: { + * key: ({reportId}) => ONYXKEYS.ACCOUNT, + * selector: (value: Account | null) => value?.id ?? '', + * }, + * ``` + */ +type BaseMappingFunctionKeyAndSelector = { + key: (props: Omit & Partial) => TOnyxKey; + selector: Selector; +}; + +/** + * Represents the mapping options between an Onyx key and the component's prop with all its possibilities. + */ +type Mapping = BaseMapping & + EntryBaseMapping & + ( + | BaseMappingKey> + | BaseMappingStringKeyAndSelector + | BaseMappingFunctionKeyAndSelector + ); + +/** + * Represents the mapping options between an Onyx collection key without suffix and the component's prop with all its possibilities. + */ +type CollectionMapping = BaseMapping & + CollectionBaseMapping & + ( + | BaseMappingKey> + | BaseMappingStringKeyAndSelector, TOnyxKey> + | BaseMappingFunctionKeyAndSelector, TOnyxKey> + ); + +/** + * Represents an union type of all the possible Onyx key mappings. + * Each `OnyxPropMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. + */ +type OnyxPropMapping = { + [TOnyxKey in OnyxKey]: Mapping; +}[OnyxKey]; + +/** + * Represents an union type of all the possible Onyx collection keys without suffix mappings. + * Each `OnyxPropCollectionMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. + */ +type OnyxPropCollectionMapping = { + [TOnyxKey in CollectionKeyBase]: CollectionMapping; +}[CollectionKeyBase]; + +type MapOnyxToState = { + [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping; +}; + +type WithOnyxProps = Omit; + +type WithOnyxState = TOnyxProps & { + loading: boolean; +}; // This is a list of keys that can exist on a `mapping`, but are not directly related to loading data from Onyx. When the keys of a mapping are looped over to check // if a key has changed, it's a good idea to skip looking at these properties since they would have unexpected results. @@ -17,36 +153,49 @@ const mappingPropertiesToIgnoreChangesTo = ['initialValue', 'allowStaleData']; /** * Returns the display name of a component - * - * @param {object} component - * @returns {string} */ -function getDisplayName(component) { +function getDisplayName(component: React.ComponentType): string { return component.displayName || component.name || 'Component'; } /** * Removes all the keys from state that are unrelated to the onyx data being mapped to the component. * - * @param {Object} state of the component - * @param {Object} onyxToStateMapping the object holding all of the mapping configuration for the component - * @returns {Object} + * @param state of the component + * @param onyxToStateMapping the object holding all of the mapping configuration for the component */ -const getOnyxDataFromState = (state, onyxToStateMapping) => _.pick(state, _.keys(onyxToStateMapping)); +const getOnyxDataFromState = (state: WithOnyxState, onyxToStateMapping: MapOnyxToState) => + utils.pick(state, Object.keys(onyxToStateMapping)) as WithOnyxState; -export default function (mapOnyxToState, shouldDelayUpdates = false) { +export default function ( + mapOnyxToState: MapOnyxToState, + shouldDelayUpdates = false, +): React.ComponentType> { // A list of keys that must be present in tempState before we can render the WrappedComponent - const requiredKeysForInit = _.chain(mapOnyxToState) - .omit((config) => config.initWithStoredValues === false) + const requiredKeysForInit = utils + .omit(mapOnyxToState, (entry) => entry[1].initWithStoredValues === false) .keys() .value(); - return (WrappedComponent) => { + // const requiredKeysForInit = _.chain(mapOnyxToState) + // .omit((config) => config.initWithStoredValues === false) + // .keys() + // .value(); + + return (WrappedComponent: React.ComponentType): React.ComponentType> => { const displayName = getDisplayName(WrappedComponent); - class withOnyx extends React.Component { - pendingSetStates = []; - constructor(props) { + class withOnyx extends React.Component, WithOnyxState> { + pendingSetStates: Array>> = []; + + shouldDelayUpdates: boolean; + + activeConnectionIDs: Record; + + tempState: WithOnyxState; + + constructor(props: WithOnyxProps) { super(props); + this.shouldDelayUpdates = shouldDelayUpdates; this.setWithOnyxState = this.setWithOnyxState.bind(this); this.flushPendingSetStates = this.flushPendingSetStates.bind(this); @@ -55,35 +204,33 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // disconnected. It is a key value store with the format {[mapping.key]: connectionID}. this.activeConnectionIDs = {}; - const cachedState = _.reduce( - mapOnyxToState, - (resultObj, mapping, propertyName) => { - const key = Str.result(mapping.key, props); - let value = OnyxUtils.tryGetCachedValue(key, mapping); - if (!value && mapping.initialValue) { - value = mapping.initialValue; - } + const cachedState = Object.keys(mapOnyxToState).reduce>((resultObj, propName) => { + const mapping = mapOnyxToState[propName as keyof TOnyxProps]; - /** - * If we have a pending merge for a key it could mean that data is being set via Onyx.merge() and someone expects a component to have this data immediately. - * - * @example - * - * Onyx.merge('report_123', value); - * Navigation.navigate(route); // Where "route" expects the "value" to be available immediately once rendered. - * - * In reality, Onyx.merge() will only update the subscriber after all merges have been batched and the previous value is retrieved via a get() (returns a promise). - * So, we won't use the cache optimization here as it will lead us to arbitrarily defer various actions in the application code. - */ - if ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData) { - // eslint-disable-next-line no-param-reassign - resultObj[propertyName] = value; - } + const key = Str.result(mapping.key, props) as OnyxKey; + let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>) as OnyxValue; + if (!value && mapping.initialValue) { + value = mapping.initialValue; + } - return resultObj; - }, - {}, - ); + /** + * If we have a pending merge for a key it could mean that data is being set via Onyx.merge() and someone expects a component to have this data immediately. + * + * @example + * + * Onyx.merge('report_123', value); + * Navigation.navigate(route); // Where "route" expects the "value" to be available immediately once rendered. + * + * In reality, Onyx.merge() will only update the subscriber after all merges have been batched and the previous value is retrieved via a get() (returns a promise). + * So, we won't use the cache optimization here as it will lead us to arbitrarily defer various actions in the application code. + */ + if ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData) { + // eslint-disable-next-line no-param-reassign + resultObj[propName as keyof TOnyxProps] = value; + } + + return resultObj; + }, {} as WithOnyxState); // If we have all the data we need, then we can render the component immediately cachedState.loading = _.size(cachedState) < requiredKeysForInit.length; @@ -98,17 +245,21 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState); // Subscribe each of the state properties to the proper Onyx key - _.each(mapOnyxToState, (mapping, propertyName) => { - if (_.includes(mappingPropertiesToIgnoreChangesTo, propertyName)) { + Object.keys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + + if (_.includes(mappingPropertiesToIgnoreChangesTo, propName)) { return; } - const key = Str.result(mapping.key, {...this.props, ...onyxDataFromState}); - this.connectMappingToOnyx(mapping, propertyName, key); + + const key = Str.result(mapping.key, {...this.props, ...onyxDataFromState}) as OnyxKey; + this.connectMappingToOnyx(mapping, propName as keyof TOnyxProps, key); }); + this.checkEvictableKeys(); } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: WithOnyxProps, prevState: WithOnyxState) { // The whole purpose of this method is to check to see if a key that is subscribed to Onyx has changed, and then Onyx needs to be disconnected from the old // key and connected to the new key. // For example, a key could change if KeyB depends on data loading from Onyx for KeyA. @@ -116,9 +267,11 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState); const prevOnyxDataFromState = getOnyxDataFromState(prevState, mapOnyxToState); - _.each(mapOnyxToState, (mapping, propName) => { + Object.keys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + // Some properties can be ignored because they aren't related to onyx keys and they will never change - if (_.includes(mappingPropertiesToIgnoreChangesTo, propName)) { + if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { return; } @@ -129,30 +282,33 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // (eg. if a user switches chats really quickly). In this case, it's much more stable to always look at the changes to prevProp and prevState to derive the key. // The second case cannot be used all the time because the onyx data doesn't change the first time that `componentDidUpdate()` runs after loading. In this case, // the `mapping.previousKey` must be used for the comparison or else this logic never detects that onyx data could have changed during the loading process. - const previousKey = isFirstTimeUpdatingAfterLoading ? mapping.previousKey : Str.result(mapping.key, {...prevProps, ...prevOnyxDataFromState}); - const newKey = Str.result(mapping.key, {...this.props, ...onyxDataFromState}); + const previousKey = (isFirstTimeUpdatingAfterLoading ? mapping.previousKey : Str.result(mapping.key, {...prevProps, ...prevOnyxDataFromState})) as OnyxKey; + const newKey = Str.result(mapping.key, {...this.props, ...onyxDataFromState}) as OnyxKey; if (previousKey !== newKey) { Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey); delete this.activeConnectionIDs[previousKey]; - this.connectMappingToOnyx(mapping, propName, newKey); + this.connectMappingToOnyx(mapping, propName as keyof TOnyxProps, newKey); } }); + this.checkEvictableKeys(); } componentWillUnmount() { // Disconnect everything from Onyx - _.each(mapOnyxToState, (mapping) => { - const key = Str.result(mapping.key, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}); + Object.keys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + + const key = Str.result(mapping.key, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}) as OnyxKey; Onyx.disconnect(this.activeConnectionIDs[key], key); }); } - setStateProxy(modifier) { + setStateProxy(modifier: Partial>) { if (this.shouldDelayUpdates) { this.pendingSetStates.push(modifier); } else { - this.setState(modifier); + this.setState(modifier as WithOnyxState); } } @@ -174,7 +330,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { * @param {String} statePropertyName * @param {*} val */ - setWithOnyxState(statePropertyName, val) { + setWithOnyxState(statePropertyName: T, val: WithOnyxState[T]) { const prevValue = this.state[statePropertyName]; // If the component is not loading (read "mounting"), then we can just update the state @@ -190,14 +346,14 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { return; } - this.setStateProxy({[statePropertyName]: val}); + this.setStateProxy({[statePropertyName]: val as WithOnyxState[T]}); return; } this.tempState[statePropertyName] = val; // If some key does not have a value yet, do not update the state yet - const tempStateIsMissingKey = _.some(requiredKeysForInit, (key) => _.isUndefined(this.tempState[key])); + const tempStateIsMissingKey = requiredKeysForInit.some((key) => this.tempState[key as keyof TOnyxProps] === undefined); if (tempStateIsMissingKey) { return; } @@ -207,35 +363,35 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // Full of hacky workarounds to prevent the race condition described above. this.setState((prevState) => { - const finalState = _.reduce( - stateUpdate, - (result, value, key) => { - if (key === 'loading') { - return result; - } - - const initialValue = mapOnyxToState[key].initialValue; - - // If initialValue is there and the state contains something different it means - // an update has already been received and we can discard the value we are trying to hydrate - if (!_.isUndefined(initialValue) && !_.isUndefined(prevState[key]) && prevState[key] !== initialValue) { - // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - - // if value is already there (without initial value) then we can discard the value we are trying to hydrate - } else if (!_.isUndefined(prevState[key])) { - // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - } else { - // eslint-disable-next-line no-param-reassign - result[key] = value; - } + const finalState = Object.keys(stateUpdate).reduce>((result, _key) => { + const key = _key as keyof TOnyxProps; + + if (key === 'loading') { return result; - }, - {}, - ); + } + + const initialValue = mapOnyxToState[key].initialValue; + + // If initialValue is there and the state contains something different it means + // an update has already been received and we can discard the value we are trying to hydrate + if (initialValue !== undefined && prevState[key] !== undefined && prevState[key] !== initialValue) { + // eslint-disable-next-line no-param-reassign + result[key] = prevState[key]; + + // if value is already there (without initial value) then we can discard the value we are trying to hydrate + } else if (!_.isUndefined(prevState[key])) { + // eslint-disable-next-line no-param-reassign + result[key] = prevState[key]; + } else { + // eslint-disable-next-line no-param-reassign + result[key] = stateUpdate[key]; + } + + return result; + }, {} as WithOnyxState); finalState.loading = false; + return finalState; }); } @@ -249,13 +405,15 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // We will add this key to our list of recently accessed keys // if the canEvict function returns true. This is necessary criteria // we MUST use to specify if a key can be removed or not. - _.each(mapOnyxToState, (mapping) => { - if (_.isUndefined(mapping.canEvict)) { + Object.keys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + + if (mapping.canEvict === undefined) { return; } - const canEvict = Str.result(mapping.canEvict, this.props); - const key = Str.result(mapping.key, this.props); + const canEvict = Str.result(mapping.canEvict, this.props) as boolean; + const key = Str.result(mapping.key, this.props) as OnyxKey; if (!OnyxUtils.isSafeEvictionKey(key)) { throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); @@ -272,15 +430,14 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { /** * Takes a single mapping and binds the state of the component to the store * - * @param {object} mapping - * @param {string|function} mapping.key key to connect to. can be a string or a + * @param mapping.key key to connect to. can be a string or a * function that takes this.props as an argument and returns a string - * @param {string} statePropertyName the name of the state property that Onyx will add the data to - * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the + * @param statePropertyName the name of the state property that Onyx will add the data to + * @param [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the * component - * @param {string} key to connect to Onyx with + * @param key to connect to Onyx with */ - connectMappingToOnyx(mapping, statePropertyName, key) { + connectMappingToOnyx(mapping: MapOnyxToState[keyof TOnyxProps], statePropertyName: keyof TOnyxProps, key: OnyxKey) { // Remember what the previous key was so that key changes can be detected when data is being loaded from Onyx. This will allow // dependent keys to finish loading their data. // eslint-disable-next-line no-param-reassign @@ -290,7 +447,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { this.activeConnectionIDs[key] = Onyx.connect({ ...mapping, key, - statePropertyName, + statePropertyName: statePropertyName as string, withOnyxInstance: this, displayName, }); @@ -304,14 +461,15 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { this.shouldDelayUpdates = false; this.pendingSetStates.forEach((modifier) => { - this.setState(modifier); + this.setState(modifier as WithOnyxState); }); + this.pendingSetStates = []; } render() { // Remove any null values so that React replaces them with default props - const propsToPass = _.omit(this.props, _.isNull); + const propsToPass = utils.omit(this.props, (entry) => entry[1] === null); if (this.state.loading) { return null; @@ -319,8 +477,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // Remove any internal state properties used by withOnyx // that should not be passed to a wrapped component - let stateToPass = _.omit(this.state, 'loading'); - stateToPass = _.omit(stateToPass, _.isNull); + const stateToPass = utils.omit(this.state as WithOnyxState, (entry) => entry[0] === 'loading' || entry[1] === null); const stateToPassWithoutNestedNulls = utils.removeNestedNullValues(stateToPass); // Spreading props and state is necessary in an HOC where the data cannot be predicted @@ -348,6 +505,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { forwardedRef: undefined, }; withOnyx.displayName = `withOnyx(${displayName})`; + return React.forwardRef((props, ref) => { const Component = withOnyx; return ( From 708f4c8bb28e09eb3601a493879279c083897a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 10 May 2024 14:58:54 +0100 Subject: [PATCH 03/96] Fix more types --- lib/types.ts | 4 +++ lib/withOnyx.tsx | 65 +++++++++++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 62a4ec1c..ccc47774 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -390,6 +390,9 @@ type InitOptions = { debugSetState?: boolean; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GenericFunction = (...args: any[]) => any; + export type { CollectionKey, CollectionKeyBase, @@ -418,4 +421,5 @@ export type { Mapping, OnyxUpdate, InitOptions, + GenericFunction, }; diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index 01189800..ac785fee 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -10,7 +10,19 @@ import _ from 'underscore'; import Onyx from './Onyx'; import OnyxUtils from './OnyxUtils'; import * as Str from './Str'; -import type {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector, WithOnyxConnectOptions} from './types'; +import type { + CollectionKeyBase, + ExtractOnyxCollectionValue, + GenericFunction, + KeyValueMapping, + OnyxCollection, + OnyxEntry, + OnyxKey, + OnyxValue, + Selector, + WithOnyxConnectOptions, + WithOnyxInstance, +} from './types'; import utils from './utils'; /** @@ -110,6 +122,11 @@ type Mapping ); +type WithOnyxMapping = Mapping & { + connectionID: number; + previousKey?: OnyxKey; +}; + /** * Represents the mapping options between an Onyx collection key without suffix and the component's prop with all its possibilities. */ @@ -172,14 +189,12 @@ export default function ( shouldDelayUpdates = false, ): React.ComponentType> { // A list of keys that must be present in tempState before we can render the WrappedComponent - const requiredKeysForInit = utils - .omit(mapOnyxToState, (entry) => entry[1].initWithStoredValues === false) - .keys() - .value(); - // const requiredKeysForInit = _.chain(mapOnyxToState) - // .omit((config) => config.initWithStoredValues === false) - // .keys() - // .value(); + const requiredKeysForInit = Object.keys( + utils.omit( + mapOnyxToState as Record[keyof TOnyxProps]>, + (entry: [string, MapOnyxToState[keyof TOnyxProps]]) => entry[1].initWithStoredValues === false, + ), + ); return (WrappedComponent: React.ComponentType): React.ComponentType> => { const displayName = getDisplayName(WrappedComponent); @@ -205,9 +220,9 @@ export default function ( this.activeConnectionIDs = {}; const cachedState = Object.keys(mapOnyxToState).reduce>((resultObj, propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; - const key = Str.result(mapping.key, props) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, props) as OnyxKey; let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>) as OnyxValue; if (!value && mapping.initialValue) { value = mapping.initialValue; @@ -246,13 +261,13 @@ export default function ( // Subscribe each of the state properties to the proper Onyx key Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; if (_.includes(mappingPropertiesToIgnoreChangesTo, propName)) { return; } - const key = Str.result(mapping.key, {...this.props, ...onyxDataFromState}) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}) as OnyxKey; this.connectMappingToOnyx(mapping, propName as keyof TOnyxProps, key); }); @@ -268,7 +283,7 @@ export default function ( const prevOnyxDataFromState = getOnyxDataFromState(prevState, mapOnyxToState); Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; // Some properties can be ignored because they aren't related to onyx keys and they will never change if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { @@ -282,8 +297,10 @@ export default function ( // (eg. if a user switches chats really quickly). In this case, it's much more stable to always look at the changes to prevProp and prevState to derive the key. // The second case cannot be used all the time because the onyx data doesn't change the first time that `componentDidUpdate()` runs after loading. In this case, // the `mapping.previousKey` must be used for the comparison or else this logic never detects that onyx data could have changed during the loading process. - const previousKey = (isFirstTimeUpdatingAfterLoading ? mapping.previousKey : Str.result(mapping.key, {...prevProps, ...prevOnyxDataFromState})) as OnyxKey; - const newKey = Str.result(mapping.key, {...this.props, ...onyxDataFromState}) as OnyxKey; + const previousKey = ( + isFirstTimeUpdatingAfterLoading ? mapping.previousKey : Str.result(mapping.key as GenericFunction, {...prevProps, ...prevOnyxDataFromState}) + ) as OnyxKey; + const newKey = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}) as OnyxKey; if (previousKey !== newKey) { Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey); delete this.activeConnectionIDs[previousKey]; @@ -297,9 +314,9 @@ export default function ( componentWillUnmount() { // Disconnect everything from Onyx Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; - const key = Str.result(mapping.key, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}) as OnyxKey; Onyx.disconnect(this.activeConnectionIDs[key], key); }); } @@ -406,14 +423,14 @@ export default function ( // if the canEvict function returns true. This is necessary criteria // we MUST use to specify if a key can be removed or not. Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps]; + const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; if (mapping.canEvict === undefined) { return; } - const canEvict = Str.result(mapping.canEvict, this.props) as boolean; - const key = Str.result(mapping.key, this.props) as OnyxKey; + const canEvict = Str.result(mapping.canEvict as GenericFunction, this.props) as boolean; + const key = Str.result(mapping.key as GenericFunction, this.props) as OnyxKey; if (!OnyxUtils.isSafeEvictionKey(key)) { throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); @@ -438,17 +455,19 @@ export default function ( * @param key to connect to Onyx with */ connectMappingToOnyx(mapping: MapOnyxToState[keyof TOnyxProps], statePropertyName: keyof TOnyxProps, key: OnyxKey) { + const onyxMapping = mapOnyxToState[statePropertyName as keyof TOnyxProps] as WithOnyxMapping; + // Remember what the previous key was so that key changes can be detected when data is being loaded from Onyx. This will allow // dependent keys to finish loading their data. // eslint-disable-next-line no-param-reassign - mapOnyxToState[statePropertyName].previousKey = key; + onyxMapping.previousKey = key; // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs this.activeConnectionIDs[key] = Onyx.connect({ ...mapping, key, statePropertyName: statePropertyName as string, - withOnyxInstance: this, + withOnyxInstance: this as unknown as WithOnyxInstance, displayName, }); } From ccc8dd5f2917c772e9800c10fbb8188a25b8fbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 10 May 2024 16:04:23 +0100 Subject: [PATCH 04/96] Fix more types --- lib/withOnyx.tsx | 27 ++++++++++----------------- types/modules/react.d.ts | 6 ++++++ 2 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 types/modules/react.d.ts diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index ac785fee..88a3381c 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -3,7 +3,7 @@ * something in Onyx (a key/value store). That way, as soon as data in Onyx changes, the state will be set and the view * will automatically change to reflect the new data. */ -import PropTypes from 'prop-types'; +import type {ForwardedRef} from 'react'; import React from 'react'; import type {IsEqual} from 'type-fest'; import _ from 'underscore'; @@ -158,7 +158,7 @@ type MapOnyxToState = { [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping; }; -type WithOnyxProps = Omit; +type WithOnyxProps = Omit & {forwardedRef?: ForwardedRef}; type WithOnyxState = TOnyxProps & { loading: boolean; @@ -200,13 +200,16 @@ export default function ( const displayName = getDisplayName(WrappedComponent); class withOnyx extends React.Component, WithOnyxState> { + // eslint-disable-next-line react/static-property-placement + static displayName: string; + pendingSetStates: Array>> = []; shouldDelayUpdates: boolean; activeConnectionIDs: Record; - tempState: WithOnyxState; + tempState: WithOnyxState | undefined; constructor(props: WithOnyxProps) { super(props); @@ -241,7 +244,7 @@ export default function ( */ if ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData) { // eslint-disable-next-line no-param-reassign - resultObj[propName as keyof TOnyxProps] = value; + resultObj[propName as keyof TOnyxProps] = value as WithOnyxState[keyof TOnyxProps]; } return resultObj; @@ -363,14 +366,14 @@ export default function ( return; } - this.setStateProxy({[statePropertyName]: val as WithOnyxState[T]}); + this.setStateProxy({[statePropertyName]: val} as WithOnyxState); return; } this.tempState[statePropertyName] = val; // If some key does not have a value yet, do not update the state yet - const tempStateIsMissingKey = requiredKeysForInit.some((key) => this.tempState[key as keyof TOnyxProps] === undefined); + const tempStateIsMissingKey = requiredKeysForInit.some((key) => this.tempState?.[key as keyof TOnyxProps] === undefined); if (tempStateIsMissingKey) { return; } @@ -488,7 +491,7 @@ export default function ( render() { // Remove any null values so that React replaces them with default props - const propsToPass = utils.omit(this.props, (entry) => entry[1] === null); + const propsToPass = utils.omit(this.props as Omit, (entry) => entry[1] === null); if (this.state.loading) { return null; @@ -513,16 +516,6 @@ export default function ( } } - withOnyx.propTypes = { - forwardedRef: PropTypes.oneOfType([ - PropTypes.func, - // eslint-disable-next-line react/forbid-prop-types - PropTypes.shape({current: PropTypes.object}), - ]), - }; - withOnyx.defaultProps = { - forwardedRef: undefined, - }; withOnyx.displayName = `withOnyx(${displayName})`; return React.forwardRef((props, ref) => { diff --git a/types/modules/react.d.ts b/types/modules/react.d.ts new file mode 100644 index 00000000..3e2e8fb3 --- /dev/null +++ b/types/modules/react.d.ts @@ -0,0 +1,6 @@ +import type React from 'react'; + +declare module 'react' { + // eslint-disable-next-line @typescript-eslint/ban-types + function forwardRef(render: (props: P, ref: React.ForwardedRef) => React.ReactElement | null): (props: P & React.RefAttributes) => React.ReactElement | null; +} From d55adb6eeab75f02e1eeb9d2f4655450008d3e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 13 May 2024 14:36:41 +0100 Subject: [PATCH 05/96] Remove remaining underscore usage from withOnyx --- lib/withOnyx.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index da3e7e55..bd141268 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -6,7 +6,6 @@ import type {ForwardedRef} from 'react'; import React from 'react'; import type {IsEqual} from 'type-fest'; -import _ from 'underscore'; import Onyx from './Onyx'; import OnyxUtils from './OnyxUtils'; import * as Str from './Str'; @@ -251,7 +250,7 @@ export default function ( }, {} as WithOnyxState); // If we have all the data we need, then we can render the component immediately - cachedState.loading = _.size(cachedState) < requiredKeysForInit.length; + cachedState.loading = Object.keys(cachedState).length < requiredKeysForInit.length; // Object holding the temporary initial state for the component while we load the various Onyx keys this.tempState = cachedState; @@ -266,7 +265,7 @@ export default function ( Object.keys(mapOnyxToState).forEach((propName) => { const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; - if (_.includes(mappingPropertiesToIgnoreChangesTo, propName)) { + if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { return; } @@ -399,7 +398,7 @@ export default function ( result[key] = prevState[key]; // if value is already there (without initial value) then we can discard the value we are trying to hydrate - } else if (!_.isUndefined(prevState[key])) { + } else if (prevState[key] !== undefined) { // eslint-disable-next-line no-param-reassign result[key] = prevState[key]; } else { From 81d30441c0ea88f90c8bf20844453f14ac497e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 15 May 2024 18:13:48 +0100 Subject: [PATCH 06/96] Address review comments and reduce assertions --- lib/utils.ts | 68 +++++++++++++++++++++++------------------- lib/withOnyx.tsx | 77 ++++++++++++++++++++++++++++++------------------ 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 54700fd7..fb64cea7 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -138,22 +138,31 @@ function checkCompatibilityWithExistingValue(value: unknown, existingValue: unkn }; } -function pick(obj: Record, condition: string | string[] | ((entry: [string, T]) => boolean)): Record { - const result: Record = {}; - +/** + * Filters an object based on a condition and an inclusion flag. + * + * @param obj - The object to filter. + * @param condition - The condition to apply. + * @param include - If true, include entries that match the condition; otherwise, exclude them. + * @returns The filtered object. + */ +function filterObject(obj: Record, condition: string | string[] | ((entry: [string, TValue]) => boolean), include: boolean): Record { + const result: Record = {}; const entries = Object.entries(obj); + for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i]; + let shouldInclude: boolean; if (Array.isArray(condition)) { - if (condition.includes(key)) { - result[key] = value; - } + shouldInclude = condition.includes(key); } else if (typeof condition === 'string') { - if (key === condition) { - result[key] = value; - } - } else if (condition(entries[i])) { + shouldInclude = key === condition; + } else { + shouldInclude = condition(entries[i]); + } + + if (include ? shouldInclude : !shouldInclude) { result[key] = value; } } @@ -161,27 +170,26 @@ function pick(obj: Record, condition: string | string[] | ((entry: return result; } -function omit(obj: Record, condition: string | string[] | ((entry: [string, T]) => boolean)): Record { - const result: Record = {}; - - const entries = Object.entries(obj); - for (let i = 0; i < entries.length; i++) { - const [key, value] = entries[i]; - - if (Array.isArray(condition)) { - if (!condition.includes(key)) { - result[key] = value; - } - } else if (typeof condition === 'string') { - if (key !== condition) { - result[key] = value; - } - } else if (!condition(entries[i])) { - result[key] = value; - } - } +/** + * Picks entries from an object based on a condition. + * + * @param obj - The object to pick entries from. + * @param condition - The condition to determine which entries to pick. + * @returns The object containing only the picked entries. + */ +function pick(obj: Record, condition: string | string[] | ((entry: [string, TValue]) => boolean)): Record { + return filterObject(obj, condition, true); +} - return result; +/** + * Omits entries from an object based on a condition. + * + * @param obj - The object to omit entries from. + * @param condition - The condition to determine which entries to omit. + * @returns The object containing only the remaining entries after omission. + */ +function omit(obj: Record, condition: string | string[] | ((entry: [string, TValue]) => boolean)): Record { + return filterObject(obj, condition, false); } export default {isEmptyObject, fastMerge, formatActionName, removeNestedNullValues, checkCompatibilityWithExistingValue, pick, omit}; diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index bd141268..d34c3367 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -17,7 +17,6 @@ import type { OnyxCollection, OnyxEntry, OnyxKey, - OnyxValue, Selector, WithOnyxConnectOptions, WithOnyxInstance, @@ -33,10 +32,16 @@ type BaseMapping = { allowStaleData?: boolean; }; +/** + * Represents the base mapping options when an Onyx collection key is supplied. + */ type CollectionBaseMapping = { initialValue?: OnyxCollection; }; +/** + * Represents the base mapping options when an Onyx non-collection key is supplied. + */ type EntryBaseMapping = { initialValue?: OnyxEntry; }; @@ -121,6 +126,9 @@ type Mapping ); +/** + * Represents a superset of `Mapping` type with internal properties included. + */ type WithOnyxMapping = Mapping & { connectionID: number; previousKey?: OnyxKey; @@ -153,12 +161,21 @@ type OnyxPropCollectionMapping; }[CollectionKeyBase]; +/** + * Represents an Onyx mapping object that connects Onyx keys to component's props. + */ type MapOnyxToState = { [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping; }; +/** + * Represents the `withOnyx` internal component props. + */ type WithOnyxProps = Omit & {forwardedRef?: ForwardedRef}; +/** + * Represents the `withOnyx` internal component state. + */ type WithOnyxState = TOnyxProps & { loading: boolean; }; @@ -180,19 +197,24 @@ function getDisplayName(component: React.ComponentType(state: WithOnyxState, onyxToStateMapping: MapOnyxToState) => - utils.pick(state, Object.keys(onyxToStateMapping)) as WithOnyxState; +function getOnyxDataFromState(state: WithOnyxState, onyxToStateMapping: MapOnyxToState) { + return utils.pick(state, Object.keys(onyxToStateMapping)); +} + +/** + * Utility function to return the keys properly typed of the `withOnyx` mapping object. + */ +function mapOnyxToStateKeys(mapOnyxToState: MapOnyxToState) { + return Object.keys(mapOnyxToState) as Array; +} export default function ( mapOnyxToState: MapOnyxToState, shouldDelayUpdates = false, -): React.ComponentType> { +): (component: React.ComponentType) => React.ComponentType> { // A list of keys that must be present in tempState before we can render the WrappedComponent const requiredKeysForInit = Object.keys( - utils.omit( - mapOnyxToState as Record[keyof TOnyxProps]>, - (entry: [string, MapOnyxToState[keyof TOnyxProps]]) => entry[1].initWithStoredValues === false, - ), + utils.omit(mapOnyxToState, ([, options]) => (options as MapOnyxToState[keyof TOnyxProps]).initWithStoredValues === false), ); return (WrappedComponent: React.ComponentType): React.ComponentType> => { @@ -221,11 +243,11 @@ export default function ( // disconnected. It is a key value store with the format {[mapping.key]: connectionID}. this.activeConnectionIDs = {}; - const cachedState = Object.keys(mapOnyxToState).reduce>((resultObj, propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; + const cachedState = mapOnyxToStateKeys(mapOnyxToState).reduce>((resultObj, propName) => { + const mapping = mapOnyxToState[propName]; const key = Str.result(mapping.key as GenericFunction, props) as OnyxKey; - let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>) as OnyxValue; + let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>); if (!value && mapping.initialValue) { value = mapping.initialValue; } @@ -243,7 +265,7 @@ export default function ( */ if (mapping.initWithStoredValues !== false && ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData)) { // eslint-disable-next-line no-param-reassign - resultObj[propName as keyof TOnyxProps] = value as WithOnyxState[keyof TOnyxProps]; + resultObj[propName] = value as WithOnyxState[keyof TOnyxProps]; } return resultObj; @@ -262,15 +284,15 @@ export default function ( const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState); // Subscribe each of the state properties to the proper Onyx key - Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; + mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName]; - if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { + if (mappingPropertiesToIgnoreChangesTo.some((prop) => prop === propName)) { return; } const key = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}) as OnyxKey; - this.connectMappingToOnyx(mapping, propName as keyof TOnyxProps, key); + this.connectMappingToOnyx(mapping, propName, key); }); this.checkEvictableKeys(); @@ -284,11 +306,11 @@ export default function ( const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState); const prevOnyxDataFromState = getOnyxDataFromState(prevState, mapOnyxToState); - Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; + mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName] as WithOnyxMapping; // Some properties can be ignored because they aren't related to onyx keys and they will never change - if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { + if (mappingPropertiesToIgnoreChangesTo.some((prop) => prop === propName)) { return; } @@ -306,7 +328,7 @@ export default function ( if (previousKey !== newKey) { Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey); delete this.activeConnectionIDs[previousKey]; - this.connectMappingToOnyx(mapping, propName as keyof TOnyxProps, newKey); + this.connectMappingToOnyx(mapping, propName, newKey); } }); @@ -315,8 +337,8 @@ export default function ( componentWillUnmount() { // Disconnect everything from Onyx - Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; + mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName]; const key = Str.result(mapping.key as GenericFunction, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}) as OnyxKey; Onyx.disconnect(this.activeConnectionIDs[key], key); @@ -345,9 +367,6 @@ export default function ( * We however need to workaround this issue in the HOC. The addition of initialValue makes things even more complex, * since you cannot be really sure if the component has been updated before or after the initial hydration. Therefore if * initialValue is there, we just check if the update is different than that and then try to handle it as best as we can. - * - * @param {String} statePropertyName - * @param {*} val */ setWithOnyxState(statePropertyName: T, val: WithOnyxState[T]) { const prevValue = this.state[statePropertyName]; @@ -424,8 +443,8 @@ export default function ( // We will add this key to our list of recently accessed keys // if the canEvict function returns true. This is necessary criteria // we MUST use to specify if a key can be removed or not. - Object.keys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName as keyof TOnyxProps] as WithOnyxMapping; + mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { + const mapping = mapOnyxToState[propName] as WithOnyxMapping; if (mapping.canEvict === undefined) { return; @@ -457,7 +476,7 @@ export default function ( * @param key to connect to Onyx with */ connectMappingToOnyx(mapping: MapOnyxToState[keyof TOnyxProps], statePropertyName: keyof TOnyxProps, key: OnyxKey) { - const onyxMapping = mapOnyxToState[statePropertyName as keyof TOnyxProps] as WithOnyxMapping; + const onyxMapping = mapOnyxToState[statePropertyName] as WithOnyxMapping; // Remember what the previous key was so that key changes can be detected when data is being loaded from Onyx. This will allow // dependent keys to finish loading their data. @@ -506,7 +525,7 @@ export default function ( Date: Thu, 16 May 2024 09:41:12 +0100 Subject: [PATCH 07/96] Refactor Object.keys() to Object.entries() and improve types --- lib/withOnyx.tsx | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index d34c3367..1a0bf47f 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -182,7 +182,7 @@ type WithOnyxState = TOnyxProps & { // This is a list of keys that can exist on a `mapping`, but are not directly related to loading data from Onyx. When the keys of a mapping are looped over to check // if a key has changed, it's a good idea to skip looking at these properties since they would have unexpected results. -const mappingPropertiesToIgnoreChangesTo = ['initialValue', 'allowStaleData']; +const mappingPropertiesToIgnoreChangesTo: Array = ['initialValue', 'allowStaleData']; /** * Returns the display name of a component @@ -198,14 +198,14 @@ function getDisplayName(component: React.ComponentType(state: WithOnyxState, onyxToStateMapping: MapOnyxToState) { - return utils.pick(state, Object.keys(onyxToStateMapping)); + return utils.pick(state, Object.keys(onyxToStateMapping)) as Partial>; } /** - * Utility function to return the keys properly typed of the `withOnyx` mapping object. + * Utility function to return the properly typed entries of the `withOnyx` mapping object. */ -function mapOnyxToStateKeys(mapOnyxToState: MapOnyxToState) { - return Object.keys(mapOnyxToState) as Array; +function mapOnyxToStateEntries(mapOnyxToState: MapOnyxToState) { + return Object.entries(mapOnyxToState) as Array<[keyof TOnyxProps, WithOnyxMapping]>; } export default function ( @@ -243,9 +243,7 @@ export default function ( // disconnected. It is a key value store with the format {[mapping.key]: connectionID}. this.activeConnectionIDs = {}; - const cachedState = mapOnyxToStateKeys(mapOnyxToState).reduce>((resultObj, propName) => { - const mapping = mapOnyxToState[propName]; - + const cachedState = mapOnyxToStateEntries(mapOnyxToState).reduce>((resultObj, [propName, mapping]) => { const key = Str.result(mapping.key as GenericFunction, props) as OnyxKey; let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>); if (!value && mapping.initialValue) { @@ -284,10 +282,8 @@ export default function ( const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState); // Subscribe each of the state properties to the proper Onyx key - mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName]; - - if (mappingPropertiesToIgnoreChangesTo.some((prop) => prop === propName)) { + mapOnyxToStateEntries(mapOnyxToState).forEach(([propName, mapping]) => { + if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { return; } @@ -306,11 +302,9 @@ export default function ( const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState); const prevOnyxDataFromState = getOnyxDataFromState(prevState, mapOnyxToState); - mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName] as WithOnyxMapping; - + mapOnyxToStateEntries(mapOnyxToState).forEach(([propName, mapping]) => { // Some properties can be ignored because they aren't related to onyx keys and they will never change - if (mappingPropertiesToIgnoreChangesTo.some((prop) => prop === propName)) { + if (mappingPropertiesToIgnoreChangesTo.includes(propName)) { return; } @@ -337,9 +331,7 @@ export default function ( componentWillUnmount() { // Disconnect everything from Onyx - mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName]; - + mapOnyxToStateEntries(mapOnyxToState).forEach(([, mapping]) => { const key = Str.result(mapping.key as GenericFunction, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}) as OnyxKey; Onyx.disconnect(this.activeConnectionIDs[key], key); }); @@ -443,9 +435,7 @@ export default function ( // We will add this key to our list of recently accessed keys // if the canEvict function returns true. This is necessary criteria // we MUST use to specify if a key can be removed or not. - mapOnyxToStateKeys(mapOnyxToState).forEach((propName) => { - const mapping = mapOnyxToState[propName] as WithOnyxMapping; - + mapOnyxToStateEntries(mapOnyxToState).forEach(([, mapping]) => { if (mapping.canEvict === undefined) { return; } From 68954bdb0306f1b072639fad0a47dbc4a00d9f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 May 2024 09:48:36 +0100 Subject: [PATCH 08/96] Remove some assertions --- lib/withOnyx.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index 1a0bf47f..043dc357 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -244,7 +244,7 @@ export default function ( this.activeConnectionIDs = {}; const cachedState = mapOnyxToStateEntries(mapOnyxToState).reduce>((resultObj, [propName, mapping]) => { - const key = Str.result(mapping.key as GenericFunction, props) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, props); let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>); if (!value && mapping.initialValue) { value = mapping.initialValue; @@ -287,7 +287,7 @@ export default function ( return; } - const key = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}); this.connectMappingToOnyx(mapping, propName, key); }); @@ -315,10 +315,8 @@ export default function ( // (eg. if a user switches chats really quickly). In this case, it's much more stable to always look at the changes to prevProp and prevState to derive the key. // The second case cannot be used all the time because the onyx data doesn't change the first time that `componentDidUpdate()` runs after loading. In this case, // the `mapping.previousKey` must be used for the comparison or else this logic never detects that onyx data could have changed during the loading process. - const previousKey = ( - isFirstTimeUpdatingAfterLoading ? mapping.previousKey : Str.result(mapping.key as GenericFunction, {...prevProps, ...prevOnyxDataFromState}) - ) as OnyxKey; - const newKey = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}) as OnyxKey; + const previousKey = isFirstTimeUpdatingAfterLoading ? mapping.previousKey : Str.result(mapping.key as GenericFunction, {...prevProps, ...prevOnyxDataFromState}); + const newKey = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState}); if (previousKey !== newKey) { Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey); delete this.activeConnectionIDs[previousKey]; @@ -332,16 +330,16 @@ export default function ( componentWillUnmount() { // Disconnect everything from Onyx mapOnyxToStateEntries(mapOnyxToState).forEach(([, mapping]) => { - const key = Str.result(mapping.key as GenericFunction, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)}); Onyx.disconnect(this.activeConnectionIDs[key], key); }); } - setStateProxy(modifier: Partial>) { + setStateProxy(modifier: WithOnyxState) { if (this.shouldDelayUpdates) { this.pendingSetStates.push(modifier); } else { - this.setState(modifier as WithOnyxState); + this.setState(modifier); } } @@ -441,7 +439,7 @@ export default function ( } const canEvict = Str.result(mapping.canEvict as GenericFunction, this.props) as boolean; - const key = Str.result(mapping.key as GenericFunction, this.props) as OnyxKey; + const key = Str.result(mapping.key as GenericFunction, this.props); if (!OnyxUtils.isSafeEvictionKey(key)) { throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); From 4aafd8a25f662d36ae2366159aa986afff541e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 May 2024 16:54:45 +0100 Subject: [PATCH 09/96] Improve WithOnyxInstance types --- lib/OnyxUtils.ts | 28 ++++++++++++++-------------- lib/index.ts | 25 +++++++++++++------------ lib/types.ts | 16 ++-------------- lib/withOnyx.tsx | 17 ++++++++++++++--- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index e0649685..d544eb51 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -3,32 +3,32 @@ import {deepEqual} from 'fast-equals'; import lodashClone from 'lodash/clone'; import type {ValueOf} from 'type-fest'; +import DevTools from './DevTools'; import * as Logger from './Logger'; +import type Onyx from './Onyx'; import cache from './OnyxCache'; -import * as Str from './Str'; import * as PerformanceUtils from './PerformanceUtils'; -import Storage from './storage'; -import utils from './utils'; +import * as Str from './Str'; import unstable_batchedUpdates from './batch'; -import DevTools from './DevTools'; +import Storage from './storage'; import type { - DeepRecord, - Mapping, CollectionKey, CollectionKeyBase, + DeepRecord, + DefaultConnectCallback, + DefaultConnectOptions, + KeyValueMapping, + Mapping, NullableKeyValueMapping, + OnyxCollection, + OnyxEntry, OnyxKey, OnyxValue, Selector, - WithOnyxInstanceState, - OnyxCollection, WithOnyxConnectOptions, - DefaultConnectOptions, - OnyxEntry, - KeyValueMapping, - DefaultConnectCallback, } from './types'; -import type Onyx from './Onyx'; +import utils from './utils'; +import type {WithOnyxState} from './withOnyx'; // Method constants const METHOD = { @@ -195,7 +195,7 @@ function batchUpdates(updates: () => void): Promise { function reduceCollectionWithSelector( collection: OnyxCollection, selector: Selector, - withOnyxInstanceState: WithOnyxInstanceState | undefined, + withOnyxInstanceState: WithOnyxState | undefined, ): Record { return Object.entries(collection ?? {}).reduce((finalCollection: Record, [key, item]) => { // eslint-disable-next-line no-param-reassign diff --git a/lib/index.ts b/lib/index.ts index e7e81889..63a727ee 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,25 +1,26 @@ +import type {ConnectOptions, OnyxUpdate} from './Onyx'; import Onyx from './Onyx'; -import type {OnyxUpdate, ConnectOptions} from './Onyx'; -import type {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, OnyxValue} from './types'; -import type {UseOnyxResult, FetchStatus, ResultMetadata} from './useOnyx'; +import type {CustomTypeOptions, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector} from './types'; +import type {FetchStatus, ResultMetadata, UseOnyxResult} from './useOnyx'; import useOnyx from './useOnyx'; +import type {WithOnyxState} from './withOnyx'; import withOnyx from './withOnyx'; export default Onyx; -export {withOnyx, useOnyx}; +export {useOnyx, withOnyx}; export type { + ConnectOptions, CustomTypeOptions, + FetchStatus, + KeyValueMapping, + NullishDeep, OnyxCollection, OnyxEntry, - OnyxUpdate, - ConnectOptions, - NullishDeep, - KeyValueMapping, OnyxKey, - Selector, - WithOnyxInstanceState, - UseOnyxResult, + OnyxUpdate, OnyxValue, - FetchStatus, ResultMetadata, + Selector, + UseOnyxResult, + WithOnyxState, }; diff --git a/lib/types.ts b/lib/types.ts index d5921cbe..db0eaea7 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,7 @@ -import type {Component} from 'react'; import type {Merge} from 'type-fest'; import type {BuiltIns} from 'type-fest/source/internal'; import type OnyxUtils from './OnyxUtils'; +import type {WithOnyxInstance, WithOnyxState} from './withOnyx'; /** * Utility type that excludes `null` from the type `TValue`. @@ -148,7 +148,7 @@ type NullableKeyValueMapping = { * The type `TKey` extends `OnyxKey` and it is the key used to access a value in `KeyValueMapping`. * `TReturnType` is the type of the returned value from the selector function. */ -type Selector = (value: OnyxEntry, state: WithOnyxInstanceState) => TReturnType; +type Selector = (value: OnyxEntry, state?: WithOnyxState) => TReturnType; /** * Represents a single Onyx entry, that can be either `TOnyxValue` or `null` / `undefined` if it doesn't exist. @@ -252,11 +252,6 @@ type NullishObjectDeep = { [KeyType in keyof ObjectType]?: NullishDeep | null; }; -/** - * Represents withOnyx's internal state, containing the Onyx props and a `loading` flag. - */ -type WithOnyxInstanceState = (TOnyxProps & {loading: boolean}) | undefined; - /** * Represents a mapping between Onyx collection keys and their respective values. * @@ -274,11 +269,6 @@ type Collection = { : never; }; -type WithOnyxInstance = Component> & { - setStateProxy: (cb: (state: Record>) => OnyxValue) => void; - setWithOnyxState: (statePropertyName: OnyxKey, value: OnyxValue) => void; -}; - /** Represents the base options used in `Onyx.connect()` method. */ type BaseConnectOptions = { initWithStoredValues?: boolean; @@ -432,6 +422,4 @@ export type { OnyxValue, Selector, WithOnyxConnectOptions, - WithOnyxInstance, - WithOnyxInstanceState, }; diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index 043dc357..50d22f03 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -14,12 +14,13 @@ import type { ExtractOnyxCollectionValue, GenericFunction, KeyValueMapping, + NullableKeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, + OnyxValue, Selector, WithOnyxConnectOptions, - WithOnyxInstance, } from './types'; import utils from './utils'; @@ -180,6 +181,14 @@ type WithOnyxState = TOnyxProps & { loading: boolean; }; +/** + * Represents the `withOnyx` internal component instance. + */ +type WithOnyxInstance = React.Component> & { + setStateProxy: (modifier: Record> | ((state: Record>) => OnyxValue)) => void; + setWithOnyxState: (statePropertyName: OnyxKey, value: OnyxValue) => void; +}; + // This is a list of keys that can exist on a `mapping`, but are not directly related to loading data from Onyx. When the keys of a mapping are looped over to check // if a key has changed, it's a good idea to skip looking at these properties since they would have unexpected results. const mappingPropertiesToIgnoreChangesTo: Array = ['initialValue', 'allowStaleData']; @@ -224,7 +233,7 @@ export default function ( // eslint-disable-next-line react/static-property-placement static displayName: string; - pendingSetStates: Array>> = []; + pendingSetStates: Array | ((state: WithOnyxState) => WithOnyxState | null)> = []; shouldDelayUpdates: boolean; @@ -335,7 +344,7 @@ export default function ( }); } - setStateProxy(modifier: WithOnyxState) { + setStateProxy(modifier: WithOnyxState | ((state: WithOnyxState) => WithOnyxState | null)) { if (this.shouldDelayUpdates) { this.pendingSetStates.push(modifier); } else { @@ -536,3 +545,5 @@ export default function ( }); }; } + +export type {WithOnyxState, WithOnyxInstance}; From a6f3e05e113984cbf76409dd3ca9761697bae0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 May 2024 18:55:10 +0100 Subject: [PATCH 10/96] Moves react.d.ts inside lib and excludes it from dist --- {types => lib/types}/modules/react.d.ts | 0 lib/withOnyx.d.ts | 141 ------------------------ lib/withOnyx.tsx | 7 ++ package.json | 2 +- 4 files changed, 8 insertions(+), 142 deletions(-) rename {types => lib/types}/modules/react.d.ts (100%) delete mode 100644 lib/withOnyx.d.ts diff --git a/types/modules/react.d.ts b/lib/types/modules/react.d.ts similarity index 100% rename from types/modules/react.d.ts rename to lib/types/modules/react.d.ts diff --git a/lib/withOnyx.d.ts b/lib/withOnyx.d.ts deleted file mode 100644 index f665cea9..00000000 --- a/lib/withOnyx.d.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {IsEqual} from 'type-fest'; -import {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; - -/** - * Represents the base mapping options between an Onyx key and the component's prop. - */ -type BaseMapping = { - canEvict?: boolean | ((props: Omit) => boolean); - initWithStoredValues?: boolean; - allowStaleData?: boolean; -}; - -type CollectionBaseMapping = { - initialValue?: OnyxCollection; -}; - -type EntryBaseMapping = { - initialValue?: OnyxEntry; -}; - -/** - * Represents the string / function `key` mapping option between an Onyx key and the component's prop. - * - * If `key` is `string`, the type of the Onyx value that is associated with `key` must match with the type of the component's prop, - * otherwise an error will be thrown. - * - * If `key` is `function`, the return type of `key` function must be a valid Onyx key and the type of the Onyx value associated - * with `key` must match with the type of the component's prop, otherwise an error will be thrown. - * - * @example - * ```ts - * // Onyx prop with `string` key - * onyxProp: { - * key: ONYXKEYS.ACCOUNT, - * }, - * - * // Onyx prop with `function` key - * onyxProp: { - * key: ({reportId}) => ONYXKEYS.ACCOUNT, - * }, - * ``` - */ -type BaseMappingKey = IsEqual extends true - ? { - key: TOnyxKey | ((props: Omit & Partial) => TOnyxKey); - } - : never; - -/** - * Represents the string `key` and `selector` mapping options between an Onyx key and the component's prop. - * - * The function signature and return type of `selector` must match with the type of the component's prop, - * otherwise an error will be thrown. - * - * @example - * ```ts - * // Onyx prop with `string` key and selector - * onyxProp: { - * key: ONYXKEYS.ACCOUNT, - * selector: (value: Account | null): string => value?.id ?? '', - * }, - * ``` - */ -type BaseMappingStringKeyAndSelector = { - key: TOnyxKey; - selector: Selector; -}; - -/** - * Represents the function `key` and `selector` mapping options between an Onyx key and the component's prop. - * - * The function signature and return type of `selector` must match with the type of the component's prop, - * otherwise an error will be thrown. - * - * @example - * ```ts - * // Onyx prop with `function` key and selector - * onyxProp: { - * key: ({reportId}) => ONYXKEYS.ACCOUNT, - * selector: (value: Account | null) => value?.id ?? '', - * }, - * ``` - */ -type BaseMappingFunctionKeyAndSelector = { - key: (props: Omit & Partial) => TOnyxKey; - selector: Selector; -}; - -/** - * Represents the mapping options between an Onyx key and the component's prop with all its possibilities. - */ -type Mapping = BaseMapping & - EntryBaseMapping & - ( - | BaseMappingKey> - | BaseMappingStringKeyAndSelector - | BaseMappingFunctionKeyAndSelector - ); - -/** - * Represents the mapping options between an Onyx collection key without suffix and the component's prop with all its possibilities. - */ -type CollectionMapping = BaseMapping & - CollectionBaseMapping & - ( - | BaseMappingKey> - | BaseMappingStringKeyAndSelector, TOnyxKey> - | BaseMappingFunctionKeyAndSelector, TOnyxKey> - ); - -/** - * Represents an union type of all the possible Onyx key mappings. - * Each `OnyxPropMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. - */ -type OnyxPropMapping = { - [TOnyxKey in OnyxKey]: Mapping; -}[OnyxKey]; - -/** - * Represents an union type of all the possible Onyx collection keys without suffix mappings. - * Each `OnyxPropCollectionMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. - */ -type OnyxPropCollectionMapping = { - [TOnyxKey in CollectionKeyBase]: CollectionMapping; -}[CollectionKeyBase]; - -/** - * @deprecated Use `useOnyx` instead of `withOnyx` whenever possible. - * - * This is a higher order component that provides the ability to map a state property directly to - * something in Onyx (a key/value store). That way, as soon as data in Onyx changes, the state will be set and the view - * will automatically change to reflect the new data. - */ -declare function withOnyx( - mapping: { - [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping; - }, - shouldDelayUpdates?: boolean, -): (component: React.ComponentType) => React.ComponentType>; - -export default withOnyx; diff --git a/lib/withOnyx.tsx b/lib/withOnyx.tsx index 50d22f03..789bcd1e 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx.tsx @@ -217,6 +217,13 @@ function mapOnyxToStateEntries(mapOnyxToState: MapO return Object.entries(mapOnyxToState) as Array<[keyof TOnyxProps, WithOnyxMapping]>; } +/** + * @deprecated Use `useOnyx` instead of `withOnyx` whenever possible. + * + * This is a higher order component that provides the ability to map a state property directly to + * something in Onyx (a key/value store). That way, as soon as data in Onyx changes, the state will be set and the view + * will automatically change to reflect the new data. + */ export default function ( mapOnyxToState: MapOnyxToState, shouldDelayUpdates = false, diff --git a/package.json b/package.json index 9e520e4a..44ce969f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "jest", - "build": "tsc -p tsconfig.build.json && cp ./lib/*.d.ts ./dist", + "build": "tsc -p tsconfig.build.json", "build:watch": "nodemon --watch lib --ext js,json,ts,tsx --exec \"npm run build && npm pack\"", "prebuild:docs": "npm run build", "build:docs": "ts-node buildDocs.ts", From 3238b5dd9b014723ee3dc2a789ec34e3327f35b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 May 2024 19:09:20 +0100 Subject: [PATCH 11/96] Extract withOnyx types to a new file --- lib/OnyxUtils.ts | 2 +- lib/index.ts | 2 +- lib/types.ts | 2 +- lib/{withOnyx.tsx => withOnyx/index.tsx} | 192 +---------------------- lib/withOnyx/types.ts | 170 ++++++++++++++++++++ 5 files changed, 179 insertions(+), 189 deletions(-) rename lib/{withOnyx.tsx => withOnyx/index.tsx} (73%) create mode 100644 lib/withOnyx/types.ts diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index d544eb51..68054766 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -28,7 +28,7 @@ import type { WithOnyxConnectOptions, } from './types'; import utils from './utils'; -import type {WithOnyxState} from './withOnyx'; +import type {WithOnyxState} from './withOnyx/types'; // Method constants const METHOD = { diff --git a/lib/index.ts b/lib/index.ts index 63a727ee..79bcf856 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,8 +3,8 @@ import Onyx from './Onyx'; import type {CustomTypeOptions, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector} from './types'; import type {FetchStatus, ResultMetadata, UseOnyxResult} from './useOnyx'; import useOnyx from './useOnyx'; -import type {WithOnyxState} from './withOnyx'; import withOnyx from './withOnyx'; +import type {WithOnyxState} from './withOnyx/types'; export default Onyx; export {useOnyx, withOnyx}; diff --git a/lib/types.ts b/lib/types.ts index db0eaea7..7ea33e52 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,7 @@ import type {Merge} from 'type-fest'; import type {BuiltIns} from 'type-fest/source/internal'; import type OnyxUtils from './OnyxUtils'; -import type {WithOnyxInstance, WithOnyxState} from './withOnyx'; +import type {WithOnyxInstance, WithOnyxState} from './withOnyx/types'; /** * Utility type that excludes `null` from the type `TValue`. diff --git a/lib/withOnyx.tsx b/lib/withOnyx/index.tsx similarity index 73% rename from lib/withOnyx.tsx rename to lib/withOnyx/index.tsx index 789bcd1e..b6c86079 100644 --- a/lib/withOnyx.tsx +++ b/lib/withOnyx/index.tsx @@ -3,191 +3,13 @@ * something in Onyx (a key/value store). That way, as soon as data in Onyx changes, the state will be set and the view * will automatically change to reflect the new data. */ -import type {ForwardedRef} from 'react'; import React from 'react'; -import type {IsEqual} from 'type-fest'; -import Onyx from './Onyx'; -import OnyxUtils from './OnyxUtils'; -import * as Str from './Str'; -import type { - CollectionKeyBase, - ExtractOnyxCollectionValue, - GenericFunction, - KeyValueMapping, - NullableKeyValueMapping, - OnyxCollection, - OnyxEntry, - OnyxKey, - OnyxValue, - Selector, - WithOnyxConnectOptions, -} from './types'; -import utils from './utils'; - -/** - * Represents the base mapping options between an Onyx key and the component's prop. - */ -type BaseMapping = { - canEvict?: boolean | ((props: Omit) => boolean); - initWithStoredValues?: boolean; - allowStaleData?: boolean; -}; - -/** - * Represents the base mapping options when an Onyx collection key is supplied. - */ -type CollectionBaseMapping = { - initialValue?: OnyxCollection; -}; - -/** - * Represents the base mapping options when an Onyx non-collection key is supplied. - */ -type EntryBaseMapping = { - initialValue?: OnyxEntry; -}; - -/** - * Represents the string / function `key` mapping option between an Onyx key and the component's prop. - * - * If `key` is `string`, the type of the Onyx value that is associated with `key` must match with the type of the component's prop, - * otherwise an error will be thrown. - * - * If `key` is `function`, the return type of `key` function must be a valid Onyx key and the type of the Onyx value associated - * with `key` must match with the type of the component's prop, otherwise an error will be thrown. - * - * @example - * ```ts - * // Onyx prop with `string` key - * onyxProp: { - * key: ONYXKEYS.ACCOUNT, - * }, - * - * // Onyx prop with `function` key - * onyxProp: { - * key: ({reportId}) => ONYXKEYS.ACCOUNT, - * }, - * ``` - */ -type BaseMappingKey = IsEqual extends true - ? { - key: TOnyxKey | ((props: Omit & Partial) => TOnyxKey); - } - : never; - -/** - * Represents the string `key` and `selector` mapping options between an Onyx key and the component's prop. - * - * The function signature and return type of `selector` must match with the type of the component's prop, - * otherwise an error will be thrown. - * - * @example - * ```ts - * // Onyx prop with `string` key and selector - * onyxProp: { - * key: ONYXKEYS.ACCOUNT, - * selector: (value: Account | null): string => value?.id ?? '', - * }, - * ``` - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type BaseMappingStringKeyAndSelector = { - key: TOnyxKey; - selector: Selector; -}; - -/** - * Represents the function `key` and `selector` mapping options between an Onyx key and the component's prop. - * - * The function signature and return type of `selector` must match with the type of the component's prop, - * otherwise an error will be thrown. - * - * @example - * ```ts - * // Onyx prop with `function` key and selector - * onyxProp: { - * key: ({reportId}) => ONYXKEYS.ACCOUNT, - * selector: (value: Account | null) => value?.id ?? '', - * }, - * ``` - */ -type BaseMappingFunctionKeyAndSelector = { - key: (props: Omit & Partial) => TOnyxKey; - selector: Selector; -}; - -/** - * Represents the mapping options between an Onyx key and the component's prop with all its possibilities. - */ -type Mapping = BaseMapping & - EntryBaseMapping & - ( - | BaseMappingKey> - | BaseMappingStringKeyAndSelector - | BaseMappingFunctionKeyAndSelector - ); - -/** - * Represents a superset of `Mapping` type with internal properties included. - */ -type WithOnyxMapping = Mapping & { - connectionID: number; - previousKey?: OnyxKey; -}; - -/** - * Represents the mapping options between an Onyx collection key without suffix and the component's prop with all its possibilities. - */ -type CollectionMapping = BaseMapping & - CollectionBaseMapping & - ( - | BaseMappingKey> - | BaseMappingStringKeyAndSelector, TOnyxKey> - | BaseMappingFunctionKeyAndSelector, TOnyxKey> - ); - -/** - * Represents an union type of all the possible Onyx key mappings. - * Each `OnyxPropMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. - */ -type OnyxPropMapping = { - [TOnyxKey in OnyxKey]: Mapping; -}[OnyxKey]; - -/** - * Represents an union type of all the possible Onyx collection keys without suffix mappings. - * Each `OnyxPropCollectionMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. - */ -type OnyxPropCollectionMapping = { - [TOnyxKey in CollectionKeyBase]: CollectionMapping; -}[CollectionKeyBase]; - -/** - * Represents an Onyx mapping object that connects Onyx keys to component's props. - */ -type MapOnyxToState = { - [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping; -}; - -/** - * Represents the `withOnyx` internal component props. - */ -type WithOnyxProps = Omit & {forwardedRef?: ForwardedRef}; - -/** - * Represents the `withOnyx` internal component state. - */ -type WithOnyxState = TOnyxProps & { - loading: boolean; -}; - -/** - * Represents the `withOnyx` internal component instance. - */ -type WithOnyxInstance = React.Component> & { - setStateProxy: (modifier: Record> | ((state: Record>) => OnyxValue)) => void; - setWithOnyxState: (statePropertyName: OnyxKey, value: OnyxValue) => void; -}; +import Onyx from '../Onyx'; +import OnyxUtils from '../OnyxUtils'; +import * as Str from '../Str'; +import type {GenericFunction, OnyxKey, WithOnyxConnectOptions} from '../types'; +import utils from '../utils'; +import type {MapOnyxToState, WithOnyxInstance, WithOnyxMapping, WithOnyxProps, WithOnyxState} from './types'; // This is a list of keys that can exist on a `mapping`, but are not directly related to loading data from Onyx. When the keys of a mapping are looped over to check // if a key has changed, it's a good idea to skip looking at these properties since they would have unexpected results. @@ -552,5 +374,3 @@ export default function ( }); }; } - -export type {WithOnyxState, WithOnyxInstance}; diff --git a/lib/withOnyx/types.ts b/lib/withOnyx/types.ts new file mode 100644 index 00000000..06145551 --- /dev/null +++ b/lib/withOnyx/types.ts @@ -0,0 +1,170 @@ +import type {ForwardedRef} from 'react'; +import type {IsEqual} from 'type-fest'; +import type {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, NullableKeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector} from '../types'; + +/** + * Represents the base mapping options between an Onyx key and the component's prop. + */ +type BaseMapping = { + canEvict?: boolean | ((props: Omit) => boolean); + initWithStoredValues?: boolean; + allowStaleData?: boolean; +}; + +/** + * Represents the base mapping options when an Onyx collection key is supplied. + */ +type CollectionBaseMapping = { + initialValue?: OnyxCollection; +}; + +/** + * Represents the base mapping options when an Onyx non-collection key is supplied. + */ +type EntryBaseMapping = { + initialValue?: OnyxEntry; +}; + +/** + * Represents the string / function `key` mapping option between an Onyx key and the component's prop. + * + * If `key` is `string`, the type of the Onyx value that is associated with `key` must match with the type of the component's prop, + * otherwise an error will be thrown. + * + * If `key` is `function`, the return type of `key` function must be a valid Onyx key and the type of the Onyx value associated + * with `key` must match with the type of the component's prop, otherwise an error will be thrown. + * + * @example + * ```ts + * // Onyx prop with `string` key + * onyxProp: { + * key: ONYXKEYS.ACCOUNT, + * }, + * + * // Onyx prop with `function` key + * onyxProp: { + * key: ({reportId}) => ONYXKEYS.ACCOUNT, + * }, + * ``` + */ +type BaseMappingKey = IsEqual extends true + ? { + key: TOnyxKey | ((props: Omit & Partial) => TOnyxKey); + } + : never; + +/** + * Represents the string `key` and `selector` mapping options between an Onyx key and the component's prop. + * + * The function signature and return type of `selector` must match with the type of the component's prop, + * otherwise an error will be thrown. + * + * @example + * ```ts + * // Onyx prop with `string` key and selector + * onyxProp: { + * key: ONYXKEYS.ACCOUNT, + * selector: (value: Account | null): string => value?.id ?? '', + * }, + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type BaseMappingStringKeyAndSelector = { + key: TOnyxKey; + selector: Selector; +}; + +/** + * Represents the function `key` and `selector` mapping options between an Onyx key and the component's prop. + * + * The function signature and return type of `selector` must match with the type of the component's prop, + * otherwise an error will be thrown. + * + * @example + * ```ts + * // Onyx prop with `function` key and selector + * onyxProp: { + * key: ({reportId}) => ONYXKEYS.ACCOUNT, + * selector: (value: Account | null) => value?.id ?? '', + * }, + * ``` + */ +type BaseMappingFunctionKeyAndSelector = { + key: (props: Omit & Partial) => TOnyxKey; + selector: Selector; +}; + +/** + * Represents the mapping options between an Onyx key and the component's prop with all its possibilities. + */ +type Mapping = BaseMapping & + EntryBaseMapping & + ( + | BaseMappingKey> + | BaseMappingStringKeyAndSelector + | BaseMappingFunctionKeyAndSelector + ); + +/** + * Represents a superset of `Mapping` type with internal properties included. + */ +type WithOnyxMapping = Mapping & { + connectionID: number; + previousKey?: OnyxKey; +}; + +/** + * Represents the mapping options between an Onyx collection key without suffix and the component's prop with all its possibilities. + */ +type CollectionMapping = BaseMapping & + CollectionBaseMapping & + ( + | BaseMappingKey> + | BaseMappingStringKeyAndSelector, TOnyxKey> + | BaseMappingFunctionKeyAndSelector, TOnyxKey> + ); + +/** + * Represents an union type of all the possible Onyx key mappings. + * Each `OnyxPropMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. + */ +type OnyxPropMapping = { + [TOnyxKey in OnyxKey]: Mapping; +}[OnyxKey]; + +/** + * Represents an union type of all the possible Onyx collection keys without suffix mappings. + * Each `OnyxPropCollectionMapping` will be associated with its respective Onyx key, ensuring different type-safety for each object. + */ +type OnyxPropCollectionMapping = { + [TOnyxKey in CollectionKeyBase]: CollectionMapping; +}[CollectionKeyBase]; + +/** + * Represents an Onyx mapping object that connects Onyx keys to component's props. + */ +type MapOnyxToState = { + [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping; +}; + +/** + * Represents the `withOnyx` internal component props. + */ +type WithOnyxProps = Omit & {forwardedRef?: ForwardedRef}; + +/** + * Represents the `withOnyx` internal component state. + */ +type WithOnyxState = TOnyxProps & { + loading: boolean; +}; + +/** + * Represents the `withOnyx` internal component instance. + */ +type WithOnyxInstance = React.Component> & { + setStateProxy: (modifier: Record> | ((state: Record>) => OnyxValue)) => void; + setWithOnyxState: (statePropertyName: OnyxKey, value: OnyxValue) => void; +}; + +export type {WithOnyxMapping, MapOnyxToState, WithOnyxProps, WithOnyxInstance, WithOnyxState}; From f59ea2ee777f6f9ceb107ce7b80d2511b4fd681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 May 2024 19:09:25 +0100 Subject: [PATCH 12/96] Update docs --- API-INTERNAL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/API-INTERNAL.md b/API-INTERNAL.md index aae82ccc..db3e45f5 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -115,7 +115,8 @@ whatever it is we attempted to do.

removeNullValues()

Removes a key from storage if the value is null. -Otherwise removes all nested null values in objects and returns the object

+Otherwise removes all nested null values in objects, +if shouldRemoveNestedNulls is true and returns the object.

prepareKeyValuePairsForStorage()

Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]] @@ -367,7 +368,8 @@ Notifies subscribers and writes current value to cache ## removeNullValues() ⇒ Removes a key from storage if the value is null. -Otherwise removes all nested null values in objects and returns the object +Otherwise removes all nested null values in objects, +if shouldRemoveNestedNulls is true and returns the object. **Kind**: global function **Returns**: The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely From bb3c09d49673372e6ce5679484ece1a688e0e432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 May 2024 19:15:40 +0100 Subject: [PATCH 13/96] Type improvement --- lib/withOnyx/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/withOnyx/index.tsx b/lib/withOnyx/index.tsx index b6c86079..77f51705 100644 --- a/lib/withOnyx/index.tsx +++ b/lib/withOnyx/index.tsx @@ -335,7 +335,7 @@ export default function ( render() { // Remove any null values so that React replaces them with default props - const propsToPass = utils.omit(this.props as Omit, (entry) => entry[1] === null); + const propsToPass = utils.omit(this.props as Omit, ([, propValue]) => propValue === null); if (this.state.loading) { return null; @@ -343,7 +343,7 @@ export default function ( // Remove any internal state properties used by withOnyx // that should not be passed to a wrapped component - const stateToPass = utils.omit(this.state as WithOnyxState, (entry) => entry[0] === 'loading' || entry[1] === null); + const stateToPass = utils.omit(this.state as WithOnyxState, ([stateKey, stateValue]) => stateKey === 'loading' || stateValue === null); const stateToPassWithoutNestedNulls = utils.removeNestedNullValues(stateToPass); // Spreading props and state is necessary in an HOC where the data cannot be predicted From dbdb6753d21ac9b1a18dd95297728cb37d2de94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 17 May 2024 14:08:35 +0100 Subject: [PATCH 14/96] Address review comment --- lib/withOnyx/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/withOnyx/index.tsx b/lib/withOnyx/index.tsx index 77f51705..3245bc8d 100644 --- a/lib/withOnyx/index.tsx +++ b/lib/withOnyx/index.tsx @@ -276,7 +276,7 @@ export default function ( return; } - const canEvict = Str.result(mapping.canEvict as GenericFunction, this.props) as boolean; + const canEvict = !!Str.result(mapping.canEvict as GenericFunction, this.props); const key = Str.result(mapping.key as GenericFunction, this.props); if (!OnyxUtils.isSafeEvictionKey(key)) { From f08585a3bb2a33dbabe5df3ef769136cfe6d82d0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 24 May 2024 12:54:55 +0200 Subject: [PATCH 15/96] remove null values from cache --- lib/OnyxCache.ts | 9 +++++++-- lib/utils.ts | 7 ++++--- tests/unit/onyxCacheTest.tsx | 8 +++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index f139b041..c5e9120f 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -86,8 +86,13 @@ class OnyxCache { set(key: OnyxKey, value: OnyxValue): OnyxValue { this.addKey(key); this.addToAccessedKeys(key); - this.storageMap[key] = value; + if (value === null) { + delete this.storageMap[key]; + return undefined; + } + + this.storageMap[key] = utils.removeNestedNullValues(value); return value; } @@ -107,7 +112,7 @@ class OnyxCache { throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs'); } - this.storageMap = {...utils.fastMerge(this.storageMap, data, false)}; + this.storageMap = {...utils.fastMerge(this.storageMap, data)}; const storageKeys = this.getAllKeys(); const mergedKeys = Object.keys(data); diff --git a/lib/utils.ts b/lib/utils.ts index 64c56c0d..15766f7e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -104,12 +104,13 @@ function fastMerge>(target: TObject | nu } /** Deep removes the nested null values from the given value. */ -function removeNestedNullValues>(value: unknown | unknown[] | TObject): Record | unknown[] | null { +function removeNestedNullValues>(value: TObject | unknown | unknown[]): TObject | unknown | unknown[] { if (typeof value === 'object' && !Array.isArray(value)) { - return fastMerge(value as Record | null, value as Record | null); + const objectValue = value as Record | null; + return fastMerge(objectValue, objectValue); } - return value as Record | null; + return value; } /** Formats the action name by uppercasing and adding the key if provided. */ diff --git a/tests/unit/onyxCacheTest.tsx b/tests/unit/onyxCacheTest.tsx index 28ca1fa7..badc3f7c 100644 --- a/tests/unit/onyxCacheTest.tsx +++ b/tests/unit/onyxCacheTest.tsx @@ -357,16 +357,14 @@ describe('Onyx', () => { expect(() => cache.merge({})).not.toThrow(); }); - it('Should merge `null` values', () => { - // `null` values can override existing values and should also - // be preserved during merges. + it('Should remove `null` values when merging', () => { cache.set('mockKey', {ID: 5}); cache.set('mockNullKey', null); cache.merge({mockKey: null}); - expect(cache.getValue('mockKey')).toEqual(null); - expect(cache.getValue('mockNullKey')).toEqual(null); + expect(cache.getValue('mockKey')).toEqual(undefined); + expect(cache.getValue('mockNullKey')).toEqual(undefined); }); }); From 46db8eb86ab447a10cc83e2fba88fd9b2a2d75be Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 24 May 2024 13:00:46 +0200 Subject: [PATCH 16/96] fix: don't remove (nested) null values on read --- lib/OnyxUtils.ts | 41 ++++++++++++++++++----------------------- lib/utils.ts | 2 +- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index e0649685..9cf4b724 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -426,19 +426,17 @@ function getCachedCollection(collectionKey: TKey function keysChanged( collectionKey: TKey, partialCollection: OnyxCollection, - previousPartialCollection: OnyxCollection | undefined, + previousPartialCollection: OnyxCollection, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true, ): void { - const previousCollectionWithoutNestedNulls = previousPartialCollection === undefined ? {} : (utils.removeNestedNullValues(previousPartialCollection) as Record); - // We prepare the "cached collection" which is the entire collection + the new partial data that // was merged in via mergeCollection(). const cachedCollection = getCachedCollection(collectionKey); - const cachedCollectionWithoutNestedNulls = utils.removeNestedNullValues(cachedCollection) as Record; + const previousCollection = previousPartialCollection ?? {}; // If the previous collection equals the new collection then we do not need to notify any subscribers. - if (previousPartialCollection !== undefined && deepEqual(cachedCollectionWithoutNestedNulls, previousCollectionWithoutNestedNulls)) { + if (previousPartialCollection !== undefined && deepEqual(cachedCollection, previousPartialCollection)) { return; } @@ -477,7 +475,7 @@ function keysChanged( // send the whole cached collection. if (isSubscribedToCollectionKey) { if (subscriber.waitForCollectionCallback) { - subscriber.callback(cachedCollectionWithoutNestedNulls); + subscriber.callback(cachedCollection); continue; } @@ -487,11 +485,11 @@ function keysChanged( for (let j = 0; j < dataKeys.length; j++) { const dataKey = dataKeys[j]; - if (deepEqual(cachedCollectionWithoutNestedNulls[dataKey], previousCollectionWithoutNestedNulls[dataKey])) { + if (deepEqual(cachedCollection[dataKey], previousCollection[dataKey])) { continue; } - subscriber.callback(cachedCollectionWithoutNestedNulls[dataKey], dataKey); + subscriber.callback(cachedCollection[dataKey], dataKey); } continue; } @@ -499,12 +497,12 @@ function keysChanged( // And if the subscriber is specifically only tracking a particular collection member key then we will // notify them with the cached data for that key only. if (isSubscribedToCollectionMemberKey) { - if (deepEqual(cachedCollectionWithoutNestedNulls[subscriber.key], previousCollectionWithoutNestedNulls[subscriber.key])) { + if (deepEqual(cachedCollection[subscriber.key], previousCollection[subscriber.key])) { continue; } const subscriberCallback = subscriber.callback as DefaultConnectCallback; - subscriberCallback(cachedCollectionWithoutNestedNulls[subscriber.key], subscriber.key as TKey); + subscriberCallback(cachedCollection[subscriber.key], subscriber.key as TKey); continue; } @@ -556,7 +554,7 @@ function keysChanged( // If a React component is only interested in a single key then we can set the cached value directly to the state name. if (isSubscribedToCollectionMemberKey) { - if (deepEqual(cachedCollectionWithoutNestedNulls[subscriber.key], previousCollectionWithoutNestedNulls[subscriber.key])) { + if (deepEqual(cachedCollection[subscriber.key], previousCollection[subscriber.key])) { continue; } @@ -647,16 +645,14 @@ function keyChanged( } if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) { const cachedCollection = getCachedCollection(subscriber.key); - const cachedCollectionWithoutNestedNulls = utils.removeNestedNullValues(cachedCollection) as Record; - cachedCollectionWithoutNestedNulls[key] = value; - subscriber.callback(cachedCollectionWithoutNestedNulls); + cachedCollection[key] = value; + subscriber.callback(cachedCollection); continue; } - const valueWithoutNestedNulls = utils.removeNestedNullValues(value); const subscriberCallback = subscriber.callback as DefaultConnectCallback; - subscriberCallback(valueWithoutNestedNulls, key); + subscriberCallback(value, key); continue; } @@ -752,7 +748,7 @@ function keyChanged( * - sets state on the withOnyxInstances * - triggers the callback function */ -function sendDataToConnection(mapping: Mapping, val: OnyxValue, matchedKey: TKey | undefined, isBatched: boolean): void { +function sendDataToConnection(mapping: Mapping, value: OnyxValue, matchedKey: TKey | undefined, isBatched: boolean): void { // If the mapping no longer exists then we should not send any data. // This means our subscriber disconnected or withOnyx wrapped component unmounted. if (!callbackToStateMapping[mapping.connectionID]) { @@ -760,15 +756,15 @@ function sendDataToConnection(mapping: Mapping, val: } if ('withOnyxInstance' in mapping && mapping.withOnyxInstance) { - let newData: OnyxValue = val; + let newData: OnyxValue = value; // If the mapping has a selector, then the component's state must only be updated with the data // returned by the selector. if (mapping.selector) { if (isCollectionKey(mapping.key)) { - newData = reduceCollectionWithSelector(val as OnyxCollection, mapping.selector, mapping.withOnyxInstance.state); + newData = reduceCollectionWithSelector(value as OnyxCollection, mapping.selector, mapping.withOnyxInstance.state); } else { - newData = mapping.selector(val, mapping.withOnyxInstance.state); + newData = mapping.selector(value, mapping.withOnyxInstance.state); } } @@ -781,8 +777,7 @@ function sendDataToConnection(mapping: Mapping, val: return; } - const valuesWithoutNestedNulls = utils.removeNestedNullValues(val); - (mapping as DefaultConnectOptions).callback?.(valuesWithoutNestedNulls, matchedKey as TKey); + (mapping as DefaultConnectOptions).callback?.(value, matchedKey as TKey); } /** @@ -991,7 +986,7 @@ function hasPendingMergeForKey(key: OnyxKey): boolean { } type RemoveNullValuesOutput = { - value: Record | unknown[] | null; + value: Record | unknown | unknown[] | null; wasRemoved: boolean; }; diff --git a/lib/utils.ts b/lib/utils.ts index 15766f7e..9fd39747 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -104,7 +104,7 @@ function fastMerge>(target: TObject | nu } /** Deep removes the nested null values from the given value. */ -function removeNestedNullValues>(value: TObject | unknown | unknown[]): TObject | unknown | unknown[] { +function removeNestedNullValues>(value: TObject | unknown | unknown[] | null): TObject | unknown | unknown[] | null { if (typeof value === 'object' && !Array.isArray(value)) { const objectValue = value as Record | null; return fastMerge(objectValue, objectValue); From b76cfa2d599dd78620c3032bd9d832dc217a90fc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 24 May 2024 15:47:38 +0200 Subject: [PATCH 17/96] improve how we write (set, merge) and read (get) cache values --- lib/Onyx.ts | 56 ++++++++++++++++++++++++++++++++++++------------ lib/OnyxCache.ts | 2 +- lib/OnyxUtils.ts | 17 ++++++++------- lib/utils.ts | 6 +++--- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 1474e33d..ed785271 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -128,12 +128,12 @@ function connect(connectOptions: ConnectOptions): nu // component. This null value will be filtered out so that the connected component can utilize defaultProps. if (matchingKeys.length === 0) { if (mapping.key && !OnyxUtils.isCollectionKey(mapping.key)) { - cache.set(mapping.key, null); + cache.addKey(mapping.key); } // Here we cannot use batching because the null value is expected to be set immediately for default props // or they will be undefined. - OnyxUtils.sendDataToConnection(mapping, null as OnyxValue, undefined, false); + OnyxUtils.sendDataToConnection(mapping, undefined as OnyxValue, undefined, false); return; } @@ -210,8 +210,18 @@ function disconnect(connectionID: number, keyToRemoveFromEvictionBlocklist?: Ony * @param value value to store */ function set(key: TKey, value: NonUndefined>): Promise { - // check if the value is compatible with the existing value in the storage + if (OnyxUtils.hasPendingMergeForKey(key)) { + delete OnyxUtils.getMergeQueue()[key]; + } + const existingValue = cache.getValue(key, false); + + // If the existing value as well as the new value are null, we can return early. + if (value === null && existingValue === null) { + return Promise.resolve(); + } + + // Check if the value is compatible with the existing value in the storage const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(value, existingValue); if (!isCompatible) { Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType)); @@ -220,22 +230,29 @@ function set(key: TKey, value: NonUndefined; - if (OnyxUtils.hasPendingMergeForKey(key)) { - delete OnyxUtils.getMergeQueue()[key]; + const logSetCall = (hasChanged = true) => { + // Logging properties only since values could be sensitive things we don't want to log + Logger.logInfo(`set called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`); + }; + + // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber. + // Therefore, we don't need to further broadcast and update the value so we can return early. + if (wasRemoved) { + logSetCall(); + return Promise.resolve(); } + const valueWithoutNullValues = valueAfterRemoving as OnyxValue; const hasChanged = cache.hasValueChanged(key, valueWithoutNullValues); - // Logging properties only since values could be sensitive things we don't want to log - Logger.logInfo(`set called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`); + logSetCall(hasChanged); // This approach prioritizes fast UI changes without waiting for data to be stored in device storage. - const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNullValues, hasChanged, wasRemoved); + const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNullValues, hasChanged); // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. - if (!hasChanged || wasRemoved) { + if (!hasChanged) { return updatePromise; } @@ -339,9 +356,21 @@ function merge(key: TKey, changes: NonUndefined { + // Logging properties only since values could be sensitive things we don't want to log + Logger.logInfo(`merge called for key: ${key}${_.isObject(batchedDeltaChanges) ? ` properties: ${_.keys(batchedDeltaChanges).join(',')}` : ''} hasChanged: ${hasChanged}`); + }; + // If the batched changes equal null, we want to remove the key from storage, to reduce storage size const {wasRemoved} = OnyxUtils.removeNullValues(key, batchedDeltaChanges); + // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber. + // Therefore, we don't need to further broadcast and update the value so we can return early. + if (wasRemoved) { + logMergeCall(); + return Promise.resolve(); + } + // For providers that can't handle delta changes, we need to merge the batched changes with the existing value beforehand. // The "preMergedValue" will be directly "set" in storage instead of being merged // Therefore we merge the batched changes with the existing value to get the final merged value that will be stored. @@ -351,14 +380,13 @@ function merge(key: TKey, changes: NonUndefined, hasChanged, wasRemoved); + const updatePromise = OnyxUtils.broadcastUpdate(key, preMergedValue as OnyxValue, hasChanged); // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. - if (!hasChanged || wasRemoved) { + if (!hasChanged) { return updatePromise; } diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index c5e9120f..1ad5711f 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -69,7 +69,7 @@ class OnyxCache { /** Check whether cache has data for the given key */ hasCacheForKey(key: OnyxKey): boolean { - return this.storageMap[key] !== undefined; + return this.storageKeys.has(key); } /** Saves a key in the storage keys list diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 9cf4b724..9bf2fa7d 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -207,7 +207,8 @@ function reduceCollectionWithSelector> { - // When we already have the value in cache - resolve right away + // When we already have the value in cache (or the key has been set to null, + // and therefore no data is set in cache), resolve right away. if (cache.hasCacheForKey(key)) { return Promise.resolve(cache.getValue(key)); } @@ -967,12 +968,12 @@ function evictStorageAndRetry(key: TKey, value: OnyxValue, hasChanged?: boolean, wasRemoved = false): Promise { +function broadcastUpdate(key: TKey, value: OnyxValue, hasChanged?: boolean): Promise { const prevValue = cache.getValue(key, false) as OnyxValue; // Update subscribers if the cached value has changed, or when the subscriber specifically requires // all updates regardless of value changes (indicated by initWithStoredValues set to false). - if (hasChanged && !wasRemoved) { + if (hasChanged) { cache.set(key, value); } else { cache.addToAccessedKeys(key); @@ -985,8 +986,8 @@ function hasPendingMergeForKey(key: OnyxKey): boolean { return !!mergeQueue[key]; } -type RemoveNullValuesOutput = { - value: Record | unknown | unknown[] | null; +type RemoveNullValuesOutput | null> = { + value: Value | null; wasRemoved: boolean; }; @@ -997,16 +998,16 @@ type RemoveNullValuesOutput = { * * @returns The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely */ -function removeNullValues(key: OnyxKey, value: OnyxValue, shouldRemoveNestedNulls = true): RemoveNullValuesOutput { +function removeNullValues>(key: OnyxKey, value: Value | null, shouldRemoveNestedNulls = true): RemoveNullValuesOutput { if (value === null) { remove(key); - return {value, wasRemoved: true}; + return {value: null, wasRemoved: true}; } // We can remove all null values in an object by merging it with itself // utils.fastMerge recursively goes through the object and removes all null values // Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values - return {value: shouldRemoveNestedNulls ? utils.removeNestedNullValues(value as Record) : (value as Record), wasRemoved: false}; + return {value: shouldRemoveNestedNulls ? utils.removeNestedNullValues(value) : value, wasRemoved: false}; } /** diff --git a/lib/utils.ts b/lib/utils.ts index 9fd39747..ddfb4932 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ -import type {OnyxKey} from './types'; +import type {OnyxKey, OnyxValue} from './types'; type EmptyObject = Record; type EmptyValue = EmptyObject | null | undefined; @@ -104,10 +104,10 @@ function fastMerge>(target: TObject | nu } /** Deep removes the nested null values from the given value. */ -function removeNestedNullValues>(value: TObject | unknown | unknown[] | null): TObject | unknown | unknown[] | null { +function removeNestedNullValues | unknown | unknown[]>(value: TValue | null): TValue | null { if (typeof value === 'object' && !Array.isArray(value)) { const objectValue = value as Record | null; - return fastMerge(objectValue, objectValue); + return fastMerge(objectValue, objectValue) as TValue | null; } return value; From f098951b6c07ebd97326b4a86c85cc0a45f0f32f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 24 May 2024 16:44:19 +0200 Subject: [PATCH 18/96] fix: drop instead of manual set --- lib/OnyxCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 1ad5711f..32be6593 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -88,7 +88,7 @@ class OnyxCache { this.addToAccessedKeys(key); if (value === null) { - delete this.storageMap[key]; + this.drop(key); return undefined; } From a40cdce0613c3cfe894b6a95fb36a27b435e6136 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 24 May 2024 16:45:20 +0200 Subject: [PATCH 19/96] rename prop --- lib/OnyxUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 9bf2fa7d..093c5545 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -427,17 +427,17 @@ function getCachedCollection(collectionKey: TKey function keysChanged( collectionKey: TKey, partialCollection: OnyxCollection, - previousPartialCollection: OnyxCollection, + partialPreviousCollection: OnyxCollection, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true, ): void { // We prepare the "cached collection" which is the entire collection + the new partial data that // was merged in via mergeCollection(). const cachedCollection = getCachedCollection(collectionKey); - const previousCollection = previousPartialCollection ?? {}; + const previousCollection = partialPreviousCollection ?? {}; // If the previous collection equals the new collection then we do not need to notify any subscribers. - if (previousPartialCollection !== undefined && deepEqual(cachedCollection, previousPartialCollection)) { + if (partialPreviousCollection !== undefined && deepEqual(cachedCollection, partialPreviousCollection)) { return; } From 988d1f7ecdedda999cf22f9c82a0bcf4d9c4b17f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 26 May 2024 13:45:09 +0200 Subject: [PATCH 20/96] return resolved promises instead of undefined --- lib/Onyx.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index ed785271..b21d7752 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -328,7 +328,7 @@ function merge(key: TKey, changes: NonUndefined { // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue if (mergeQueue[key] == null) { - return undefined; + return Promise.resolve(); } try { @@ -343,7 +343,7 @@ function merge(key: TKey, changes: NonUndefined Date: Sun, 26 May 2024 14:04:11 +0200 Subject: [PATCH 21/96] remove keys from cache wen merged with null --- lib/OnyxCache.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 32be6593..9519a20e 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -115,9 +115,14 @@ class OnyxCache { this.storageMap = {...utils.fastMerge(this.storageMap, data)}; const storageKeys = this.getAllKeys(); - const mergedKeys = Object.keys(data); - this.storageKeys = new Set([...storageKeys, ...mergedKeys]); - mergedKeys.forEach((key) => this.addToAccessedKeys(key)); + const updatedKeys: string[] = []; + Object.entries(data).forEach(([key, value]) => { + this.addToAccessedKeys(key); + + if (value === null) this.storageKeys.delete(key); + else updatedKeys.push(key); + }); + this.storageKeys = new Set([...storageKeys, ...updatedKeys]); } /** From 045ed490420a9976aa2fac67a50284a4f4fed472 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 26 May 2024 20:55:30 +0200 Subject: [PATCH 22/96] improve set (don't execute removeNestedNullValues twice --- lib/OnyxCache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 9519a20e..d914c8f6 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -84,7 +84,6 @@ class OnyxCache { * Adds the key to the storage keys list as well */ set(key: OnyxKey, value: OnyxValue): OnyxValue { - this.addKey(key); this.addToAccessedKeys(key); if (value === null) { @@ -92,7 +91,8 @@ class OnyxCache { return undefined; } - this.storageMap[key] = utils.removeNestedNullValues(value); + this.addKey(key); + this.storageMap[key] = value; return value; } From 39bd6cfce35dad0ff774051298a473cbcef2e6d4 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:24:00 +0200 Subject: [PATCH 23/96] fix: set cache implementation --- lib/OnyxCache.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index d914c8f6..216fd543 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -84,15 +84,17 @@ class OnyxCache { * Adds the key to the storage keys list as well */ set(key: OnyxKey, value: OnyxValue): OnyxValue { + this.addKey(key); this.addToAccessedKeys(key); + this.nullishStorageKeys.delete(key); - if (value === null) { + if (value === null || value === undefined) { this.drop(key); return undefined; } - this.addKey(key); this.storageMap[key] = value; + return value; } From 0aa664967fa1b6f817c481e0ed6fb535c8bf0cfa Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:24:19 +0200 Subject: [PATCH 24/96] fix: merge cache implementation --- lib/OnyxCache.ts | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 216fd543..6153754d 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -117,29 +117,12 @@ class OnyxCache { this.storageMap = {...utils.fastMerge(this.storageMap, data)}; const storageKeys = this.getAllKeys(); - const updatedKeys: string[] = []; - Object.entries(data).forEach(([key, value]) => { + const mergedKeys = Object.keys(data); + this.storageKeys = new Set([...storageKeys, ...mergedKeys]); + mergedKeys.forEach((key) => { this.addToAccessedKeys(key); - - if (value === null) this.storageKeys.delete(key); - else updatedKeys.push(key); + this.nullishStorageKeys.delete(key); }); - this.storageKeys = new Set([...storageKeys, ...updatedKeys]); - } - - /** - * Allows to set all the keys at once. - * This is useful when we are getting - * all the keys from the storage provider - * and we want to keep the cache in sync. - * - * Previously, we had to call `addKey` in a loop - * to achieve the same result. - * - * @param keys - an array of keys - */ - setAllKeys(keys: OnyxKey[]) { - this.storageKeys = new Set(keys); } /** From 8a2b1663593f62d205717f01ff8b5990364417c9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:24:39 +0200 Subject: [PATCH 25/96] move functions and add nullishStorageKeys set --- lib/OnyxCache.ts | 51 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 6153754d..c1c04df2 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -9,7 +9,10 @@ import type {OnyxKey, OnyxValue} from './types'; */ class OnyxCache { /** Cache of all the storage keys available in persistent storage */ - storageKeys: Set; + private storageKeys: Set; + + /** Cache of all the storage keys that have been fetched before and were not set */ + private nullishStorageKeys: Set; /** Unique list of keys maintained in access order (most recent at the end) */ private recentKeys: Set; @@ -28,6 +31,7 @@ class OnyxCache { constructor() { this.storageKeys = new Set(); + this.nullishStorageKeys = new Set(); this.recentKeys = new Set(); this.storageMap = {}; this.pendingPromises = new Map(); @@ -57,19 +61,18 @@ class OnyxCache { } /** - * Get a cached value from storage - * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. + * Allows to set all the keys at once. + * This is useful when we are getting + * all the keys from the storage provider + * and we want to keep the cache in sync. + * + * Previously, we had to call `addKey` in a loop + * to achieve the same result. + * + * @param keys - an array of keys */ - getValue(key: OnyxKey, shouldReindexCache = true): OnyxValue { - if (shouldReindexCache) { - this.addToAccessedKeys(key); - } - return this.storageMap[key]; - } - - /** Check whether cache has data for the given key */ - hasCacheForKey(key: OnyxKey): boolean { - return this.storageKeys.has(key); + setAllKeys(keys: OnyxKey[]) { + this.storageKeys = new Set(keys); } /** Saves a key in the storage keys list @@ -79,6 +82,28 @@ class OnyxCache { this.storageKeys.add(key); } + /** Used to set keys that have been fetched before and were null + */ + addNullishStorageKey(key: OnyxKey): void { + this.nullishStorageKeys.add(key); + } + + /** Check whether cache has data for the given key */ + hasCacheForKey(key: OnyxKey): boolean { + return this.storageMap[key] !== undefined || this.nullishStorageKeys.has(key); + } + + /** + * Get a cached value from storage + * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. + */ + getValue(key: OnyxKey, shouldReindexCache = true): OnyxValue { + if (shouldReindexCache) { + this.addToAccessedKeys(key); + } + return this.storageMap[key]; + } + /** * Set's a key value in cache * Adds the key to the storage keys list as well From f0549c44b138fe76dec8fb2890dce801174307b5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:25:22 +0200 Subject: [PATCH 26/96] fix: don't use storageKeys directly from OnyxCache --- lib/Onyx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index b21d7752..983ffcd1 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -111,7 +111,7 @@ function connect(connectOptions: ConnectOptions): nu // Performance improvement // If the mapping is connected to an onyx key that is not a collection // we can skip the call to getAllKeys() and return an array with a single item - if (Boolean(mapping.key) && typeof mapping.key === 'string' && !mapping.key.endsWith('_') && cache.storageKeys.has(mapping.key)) { + if (Boolean(mapping.key) && typeof mapping.key === 'string' && !mapping.key.endsWith('_') && cache.getAllKeys().has(mapping.key)) { return new Set([mapping.key]); } return OnyxUtils.getAllKeys(); From d2db18c459ebd3211ef869a67aae32cd1630230e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:26:52 +0200 Subject: [PATCH 27/96] set nullish values in cache --- lib/Onyx.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 983ffcd1..662eca80 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -128,10 +128,10 @@ function connect(connectOptions: ConnectOptions): nu // component. This null value will be filtered out so that the connected component can utilize defaultProps. if (matchingKeys.length === 0) { if (mapping.key && !OnyxUtils.isCollectionKey(mapping.key)) { - cache.addKey(mapping.key); + cache.addNullishStorageKey(mapping.key); } - // Here we cannot use batching because the null value is expected to be set immediately for default props + // Here we cannot use batching because the nullish value is expected to be set immediately for default props // or they will be undefined. OnyxUtils.sendDataToConnection(mapping, undefined as OnyxValue, undefined, false); return; From 5aae227022a466220ca4aaa9d36ce0b7f831e87c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:28:34 +0200 Subject: [PATCH 28/96] add nullish values in get --- lib/OnyxUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 093c5545..c5c35de6 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -223,6 +223,11 @@ function get(key: OnyxKey): Promise> { // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages const promise = Storage.getItem(key) .then((val) => { + if (val === undefined) { + cache.addNullishStorageKey(key); + return val; + } + cache.set(key, val); return val; }) From e40262db5022eea787c95d8b78753e7bcc379791 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:28:48 +0200 Subject: [PATCH 29/96] rename variable --- lib/OnyxUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index c5c35de6..8b25fb7d 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -239,9 +239,9 @@ function get(key: OnyxKey): Promise> { /** Returns current key names stored in persisted storage */ function getAllKeys(): Promise> { // When we've already read stored keys, resolve right away - const storedKeys = cache.getAllKeys(); - if (storedKeys.size > 0) { - return Promise.resolve(storedKeys); + const cachedKeys = cache.getAllKeys(); + if (cachedKeys.size > 0) { + return Promise.resolve(cachedKeys); } const taskName = 'getAllKeys'; @@ -254,6 +254,7 @@ function getAllKeys(): Promise> { // Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages const promise = Storage.getAllKeys().then((keys) => { cache.setAllKeys(keys); + // return the updated set of keys return cache.getAllKeys(); }); From abf648ce17535dc3aa45a695efb15f71fab5e3b1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:33:26 +0200 Subject: [PATCH 30/96] disallow nulls from being returned from Onyx --- lib/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 72c6afbb..844edb6e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -178,7 +178,7 @@ type Selector = (value: OnyxEntry * })(Component); * ``` */ -type OnyxEntry = TOnyxValue | null | undefined; +type OnyxEntry = TOnyxValue | undefined; /** * Represents an Onyx collection of entries, that can be either a record of `TOnyxValue`s or `null` / `undefined` if it is empty or doesn't exist. @@ -208,7 +208,7 @@ type OnyxEntry = TOnyxValue | null | undefined; * })(Component); * ``` */ -type OnyxCollection = OnyxEntry>; +type OnyxCollection = OnyxEntry>; /** Utility type to extract `TOnyxValue` from `OnyxCollection` */ type ExtractOnyxCollectionValue = TOnyxCollection extends NonNullable> ? U : never; From 534a651887298da3ac9a900fe18d37c139669d28 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:53:51 +0200 Subject: [PATCH 31/96] check for null values in idb keyval provider --- lib/storage/providers/IDBKeyValProvider.ts | 32 ++++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/storage/providers/IDBKeyValProvider.ts b/lib/storage/providers/IDBKeyValProvider.ts index f3539b50..9b749ab8 100644 --- a/lib/storage/providers/IDBKeyValProvider.ts +++ b/lib/storage/providers/IDBKeyValProvider.ts @@ -23,7 +23,13 @@ const provider: StorageProvider = { idbKeyValStore = newIdbKeyValStore; }, - setItem: (key, value) => set(key, value, idbKeyValStore), + setItem: (key, value) => { + if (value === null) { + provider.removeItem(key); + } + + return set(key, value, idbKeyValStore); + }, multiGet: (keysParam) => getMany(keysParam, idbKeyValStore).then((values) => values.map((value, index) => [keysParam[index], value])), multiMerge: (pairs) => idbKeyValStore('readwrite', (store) => { @@ -32,7 +38,16 @@ const provider: StorageProvider = { const getValues = Promise.all(pairs.map(([key]) => promisifyRequest>(store.get(key)))); return getValues.then((values) => { - const upsertMany = pairs.map(([key, value], index) => { + const pairsWithoutNull = pairs.filter(([key, value]) => { + if (value === null) { + provider.removeItem(key); + return false; + } + + return true; + }); + + const upsertMany = pairsWithoutNull.map(([key, value], index) => { const prev = values[index]; const newValue = utils.fastMerge(prev as Record, value as Record); return promisifyRequest(store.put(newValue, key)); @@ -44,7 +59,18 @@ const provider: StorageProvider = { // Since Onyx also merged the existing value with the changes, we can just set the value directly return provider.setItem(key, preMergedValue); }, - multiSet: (pairs) => setMany(pairs, idbKeyValStore), + multiSet: (pairs) => { + const pairsWithoutNull = pairs.filter(([key, value]) => { + if (value === null) { + provider.removeItem(key); + return false; + } + + return true; + }); + + return setMany(pairsWithoutNull, idbKeyValStore); + }, clear: () => clear(idbKeyValStore), getAllKeys: () => keys(idbKeyValStore), getItem: (key) => From 14b551ecb7ae828d35dbe35dd12ed44aa5eb366b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 13:54:30 +0200 Subject: [PATCH 32/96] also remove null values in multiSet --- lib/Onyx.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 662eca80..73bca93a 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -272,9 +272,19 @@ function set(key: TKey, value: NonUndefined): Promise { - const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(data, true); + const allKeyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(data, true); - const updatePromises = keyValuePairs.map(([key, value]) => { + const removePromises: Array> = []; + const keyValuePairsToUpdate = allKeyValuePairs.filter(([key, value]) => { + if (value === null) { + removePromises.push(OnyxUtils.remove(key)); + return false; + } + + return true; + }); + + const updatePromises = keyValuePairsToUpdate.map(([key, value]) => { const prevValue = cache.getValue(key, false); // Update cache and optimistically inform subscribers on the next tick @@ -282,11 +292,11 @@ function multiSet(data: Partial): Promise { return OnyxUtils.scheduleSubscriberUpdate(key, value, prevValue); }); - return Storage.multiSet(keyValuePairs) + return Storage.multiSet(allKeyValuePairs) .catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, data)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, data); - return Promise.all(updatePromises); + return Promise.all([removePromises, updatePromises]); }) .then(() => undefined); } From f13ad865461491771eb2ba0795a46480c56f7594 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 14:29:34 +0200 Subject: [PATCH 33/96] never pass undefined values to withOnyx --- lib/Onyx.ts | 2 +- lib/OnyxUtils.ts | 4 ++-- lib/withOnyx.js | 31 +++++++++++++++++++------------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 73bca93a..cdd04251 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -133,7 +133,7 @@ function connect(connectOptions: ConnectOptions): nu // Here we cannot use batching because the nullish value is expected to be set immediately for default props // or they will be undefined. - OnyxUtils.sendDataToConnection(mapping, undefined as OnyxValue, undefined, false); + OnyxUtils.sendDataToConnection(mapping, null, undefined, false); return; } diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 8b25fb7d..45f23c8c 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -755,7 +755,7 @@ function keyChanged( * - sets state on the withOnyxInstances * - triggers the callback function */ -function sendDataToConnection(mapping: Mapping, value: OnyxValue, matchedKey: TKey | undefined, isBatched: boolean): void { +function sendDataToConnection(mapping: Mapping, value: OnyxValue | null, matchedKey: TKey | undefined, isBatched: boolean): void { // If the mapping no longer exists then we should not send any data. // This means our subscriber disconnected or withOnyx wrapped component unmounted. if (!callbackToStateMapping[mapping.connectionID]) { @@ -784,7 +784,7 @@ function sendDataToConnection(mapping: Mapping, valu return; } - (mapping as DefaultConnectOptions).callback?.(value, matchedKey as TKey); + (mapping as DefaultConnectOptions).callback?.(value === null ? undefined : value, matchedKey as TKey); } /** diff --git a/lib/withOnyx.js b/lib/withOnyx.js index cd862637..9c546319 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -61,7 +61,11 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { const key = Str.result(mapping.key, props); let value = OnyxUtils.tryGetCachedValue(key, mapping); if (!value && mapping.initialValue) { - value = mapping.initialValue; + if (mapping.initialValue === null) { + value = undefined; + } else { + value = mapping.initialValue; + } } /** @@ -219,17 +223,21 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // If initialValue is there and the state contains something different it means // an update has already been received and we can discard the value we are trying to hydrate if (!_.isUndefined(initialValue) && !_.isUndefined(prevState[key]) && prevState[key] !== initialValue) { - // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - - // if value is already there (without initial value) then we can discard the value we are trying to hydrate + if (prevState[key] !== null) { + // eslint-disable-next-line no-param-reassign + result[key] = prevState[key]; + } } else if (!_.isUndefined(prevState[key])) { - // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - } else { + // if value is already there (without initial value) then we can discard the value we are trying to hydrate + if (prevState[key] !== null) { + // eslint-disable-next-line no-param-reassign + result[key] = prevState[key]; + } + } else if (value !== null) { // eslint-disable-next-line no-param-reassign result[key] = value; } + return result; }, {}, @@ -319,10 +327,9 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // Remove any internal state properties used by withOnyx // that should not be passed to a wrapped component - let stateToPass = _.omit(this.state, 'loading'); - stateToPass = _.omit(stateToPass, _.isNull); - const stateToPassWithoutNestedNulls = utils.removeNestedNullValues(stateToPass); + const stateToPass = _.omit(this.state, 'loading'); + console.log({stateToPass}); // Spreading props and state is necessary in an HOC where the data cannot be predicted return ( ); From 9407b6cd1c6a1eb0fcc1b23db8a3be53368f6b9f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 14:29:59 +0200 Subject: [PATCH 34/96] remove console.log --- lib/withOnyx.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/withOnyx.js b/lib/withOnyx.js index 9c546319..39b7c848 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -329,7 +329,6 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // that should not be passed to a wrapped component const stateToPass = _.omit(this.state, 'loading'); - console.log({stateToPass}); // Spreading props and state is necessary in an HOC where the data cannot be predicted return ( Date: Mon, 27 May 2024 15:34:21 +0200 Subject: [PATCH 35/96] rename OnyxCache.get --- lib/Onyx.ts | 10 ++--- lib/OnyxCache.ts | 2 +- lib/OnyxUtils.ts | 16 ++++---- tests/unit/onyxCacheTest.tsx | 44 +++++++++++----------- tests/unit/onyxClearNativeStorageTest.ts | 8 ++-- tests/unit/onyxClearWebStorageTest.ts | 8 ++-- tests/unit/onyxMultiMergeWebStorageTest.ts | 32 ++++++++-------- 7 files changed, 60 insertions(+), 60 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index cdd04251..184a8155 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -45,7 +45,7 @@ function init({ if (shouldSyncMultipleInstances) { Storage.keepInstancesSync?.((key, value) => { - const prevValue = cache.getValue(key, false) as OnyxValue; + const prevValue = cache.get(key, false) as OnyxValue; cache.set(key, value); OnyxUtils.keyChanged(key, value as OnyxValue, prevValue); }); @@ -214,7 +214,7 @@ function set(key: TKey, value: NonUndefined): Promise { }); const updatePromises = keyValuePairsToUpdate.map(([key, value]) => { - const prevValue = cache.getValue(key, false); + const prevValue = cache.get(key, false); // Update cache and optimistically inform subscribers on the next tick cache.set(key, value); @@ -575,7 +575,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { // 2. Figure out whether it is a collection key or not, // since collection key subscribers need to be updated differently if (!isKeyToPreserve) { - const oldValue = cache.getValue(key); + const oldValue = cache.get(key); const newValue = defaultKeyStates[key] ?? null; if (newValue !== oldValue) { cache.set(key, newValue); @@ -603,7 +603,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { // Notify the subscribers for each key/value group so they can receive the new values Object.entries(keyValuesToResetIndividually).forEach(([key, value]) => { - updatePromises.push(OnyxUtils.scheduleSubscriberUpdate(key, value, cache.getValue(key, false))); + updatePromises.push(OnyxUtils.scheduleSubscriberUpdate(key, value, cache.get(key, false))); }); Object.entries(keyValuesToResetAsCollection).forEach(([key, value]) => { updatePromises.push(OnyxUtils.scheduleNotifyCollectionSubscribers(key, value)); diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index c1c04df2..4b5208e7 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -97,7 +97,7 @@ class OnyxCache { * Get a cached value from storage * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. */ - getValue(key: OnyxKey, shouldReindexCache = true): OnyxValue { + get(key: OnyxKey, shouldReindexCache = true): OnyxValue { if (shouldReindexCache) { this.addToAccessedKeys(key); } diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 45f23c8c..cc9c3250 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -210,7 +210,7 @@ function get(key: OnyxKey): Promise> { // When we already have the value in cache (or the key has been set to null, // and therefore no data is set in cache), resolve right away. if (cache.hasCacheForKey(key)) { - return Promise.resolve(cache.getValue(key)); + return Promise.resolve(cache.get(key)); } const taskName = `get:${key}`; @@ -225,7 +225,7 @@ function get(key: OnyxKey): Promise> { .then((val) => { if (val === undefined) { cache.addNullishStorageKey(key); - return val; + return undefined; } cache.set(key, val); @@ -307,7 +307,7 @@ function isSafeEvictionKey(testKey: OnyxKey): boolean { * If the requested key is a collection, it will return an object with all the collection members. */ function tryGetCachedValue(key: TKey, mapping?: Partial>): OnyxValue { - let val = cache.getValue(key); + let val = cache.get(key); if (isCollectionKey(key)) { const allCacheKeys = cache.getAllKeys(); @@ -320,7 +320,7 @@ function tryGetCachedValue(key: TKey, mapping?: Partial k.startsWith(key)); const values = matchingKeys.reduce((finalObject: NonNullable>, matchedKey) => { - const cachedValue = cache.getValue(matchedKey); + const cachedValue = cache.get(matchedKey); if (cachedValue) { // This is permissible because we're in the process of constructing the final object in a reduce function. // eslint-disable-next-line no-param-reassign @@ -421,7 +421,7 @@ function getCachedCollection(collectionKey: TKey return; } - collection[key] = cache.getValue(key); + collection[key] = cache.get(key); }); return collection; @@ -832,7 +832,7 @@ function getCollectionDataAndSendAsObject(matchingKeys: Co * These missingKeys will be later to use to multiGet the data from the storage. */ matchingKeys.forEach((key) => { - const cacheValue = cache.getValue(key) as OnyxValue; + const cacheValue = cache.get(key) as OnyxValue; if (cacheValue) { data[key] = cacheValue; return; @@ -920,7 +920,7 @@ function scheduleNotifyCollectionSubscribers( * Remove a key from Onyx and update the subscribers */ function remove(key: TKey): Promise { - const prevValue = cache.getValue(key, false) as OnyxValue; + const prevValue = cache.get(key, false) as OnyxValue; cache.drop(key); scheduleSubscriberUpdate(key, null as OnyxValue, prevValue); return Storage.removeItem(key).then(() => undefined); @@ -975,7 +975,7 @@ function evictStorageAndRetry(key: TKey, value: OnyxValue, hasChanged?: boolean): Promise { - const prevValue = cache.getValue(key, false) as OnyxValue; + const prevValue = cache.get(key, false) as OnyxValue; // Update subscribers if the cached value has changed, or when the subscriber specifically requires // all updates regardless of value changes (indicated by initWithStoredValues set to false). diff --git a/tests/unit/onyxCacheTest.tsx b/tests/unit/onyxCacheTest.tsx index badc3f7c..9487b0b4 100644 --- a/tests/unit/onyxCacheTest.tsx +++ b/tests/unit/onyxCacheTest.tsx @@ -79,7 +79,7 @@ describe('Onyx', () => { // Given empty cache // When a value is retrieved - const result = cache.getValue('mockKey'); + const result = cache.get('mockKey'); // Then it should be undefined expect(result).not.toBeDefined(); @@ -92,8 +92,8 @@ describe('Onyx', () => { // When a value is retrieved // Then it should be the correct value - expect(cache.getValue('mockKey')).toEqual({items: ['mockValue', 'mockValue2']}); - expect(cache.getValue('mockKey2')).toEqual('mockValue3'); + expect(cache.get('mockKey')).toEqual({items: ['mockValue', 'mockValue2']}); + expect(cache.get('mockKey2')).toEqual('mockValue3'); }); }); @@ -155,7 +155,7 @@ describe('Onyx', () => { cache.set('mockKey', {value: 'mockValue'}); // Then data should be cached - const data = cache.getValue('mockKey'); + const data = cache.get('mockKey'); expect(data).toEqual({value: 'mockValue'}); }); @@ -178,7 +178,7 @@ describe('Onyx', () => { cache.set('mockKey2', {value: []}); // Then the value should be overwritten - expect(cache.getValue('mockKey2')).toEqual({value: []}); + expect(cache.get('mockKey2')).toEqual({value: []}); }); }); @@ -193,7 +193,7 @@ describe('Onyx', () => { // Then a value should not be available in cache expect(cache.hasCacheForKey('mockKey')).toBe(false); - expect(cache.getValue('mockKey')).not.toBeDefined(); + expect(cache.get('mockKey')).not.toBeDefined(); expect(cache.getAllKeys().has('mockKey')).toBe(false); }); }); @@ -209,8 +209,8 @@ describe('Onyx', () => { }); // Then data should be created in cache - expect(cache.getValue('mockKey')).toEqual({value: 'mockValue'}); - expect(cache.getValue('mockKey2')).toEqual({value: 'mockValue2'}); + expect(cache.get('mockKey')).toEqual({value: 'mockValue'}); + expect(cache.get('mockKey2')).toEqual({value: 'mockValue2'}); }); it('Should merge data to existing cache value', () => { @@ -225,12 +225,12 @@ describe('Onyx', () => { }); // Then the values should be merged together in cache - expect(cache.getValue('mockKey')).toEqual({ + expect(cache.get('mockKey')).toEqual({ value: 'mockValue', mockItems: [], }); - expect(cache.getValue('mockKey2')).toEqual({ + expect(cache.get('mockKey2')).toEqual({ other: 'overwrittenMockValue', items: [1, 2], mock: 'mock', @@ -247,7 +247,7 @@ describe('Onyx', () => { }); // Then the values should be merged together in cache - expect(cache.getValue('mockKey')).toEqual({ + expect(cache.get('mockKey')).toEqual({ value: 'mockValue', mockItems: [], otherValue: 'overwritten', @@ -264,7 +264,7 @@ describe('Onyx', () => { }); // Then the arrays should be replaced as expected - expect(cache.getValue('mockKey')).toEqual([{ID: 3}, {added: 'field'}, {}, {ID: 1000}]); + expect(cache.get('mockKey')).toEqual([{ID: 3}, {added: 'field'}, {}, {ID: 1000}]); }); it('Should merge arrays inside objects correctly', () => { @@ -277,7 +277,7 @@ describe('Onyx', () => { }); // Then the first array is completely replaced by the second array - expect(cache.getValue('mockKey')).toEqual({ID: [2]}); + expect(cache.get('mockKey')).toEqual({ID: [2]}); }); it('Should work with primitive values', () => { @@ -288,31 +288,31 @@ describe('Onyx', () => { cache.merge({mockKey: false}); // Then the object should be overwritten with a bool value - expect(cache.getValue('mockKey')).toEqual(false); + expect(cache.get('mockKey')).toEqual(false); // When merge is called with number cache.merge({mockKey: 0}); // Then the value should be overwritten - expect(cache.getValue('mockKey')).toEqual(0); + expect(cache.get('mockKey')).toEqual(0); // When merge is called with string cache.merge({mockKey: '123'}); // Then the value should be overwritten - expect(cache.getValue('mockKey')).toEqual('123'); + expect(cache.get('mockKey')).toEqual('123'); // When merge is called with string again cache.merge({mockKey: '123'}); // Then strings should not have been concatenated - expect(cache.getValue('mockKey')).toEqual('123'); + expect(cache.get('mockKey')).toEqual('123'); // When merge is called with an object cache.merge({mockKey: {value: 'myMockObject'}}); // Then the old primitive value should be overwritten with the object - expect(cache.getValue('mockKey')).toEqual({value: 'myMockObject'}); + expect(cache.get('mockKey')).toEqual({value: 'myMockObject'}); }); it('Should ignore `undefined` values', () => { @@ -323,12 +323,12 @@ describe('Onyx', () => { cache.merge({mockKey: {ID: undefined}}); // Then the key should still be in cache and the value unchanged - expect(cache.getValue('mockKey')).toEqual({ID: 5}); + expect(cache.get('mockKey')).toEqual({ID: 5}); cache.merge({mockKey: undefined}); // Then the key should still be in cache and the value unchanged - expect(cache.getValue('mockKey')).toEqual({ID: 5}); + expect(cache.get('mockKey')).toEqual({ID: 5}); }); it('Should update storageKeys when new keys are created', () => { @@ -363,8 +363,8 @@ describe('Onyx', () => { cache.merge({mockKey: null}); - expect(cache.getValue('mockKey')).toEqual(undefined); - expect(cache.getValue('mockNullKey')).toEqual(undefined); + expect(cache.get('mockKey')).toEqual(undefined); + expect(cache.get('mockNullKey')).toEqual(undefined); }); }); diff --git a/tests/unit/onyxClearNativeStorageTest.ts b/tests/unit/onyxClearNativeStorageTest.ts index 090a7f78..28605adc 100644 --- a/tests/unit/onyxClearNativeStorageTest.ts +++ b/tests/unit/onyxClearNativeStorageTest.ts @@ -54,7 +54,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value in Onyx, the cache, and Storage is the default key state expect(onyxValue).toBe(DEFAULT_VALUE); - const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); + const cachedValue = cache.get(ONYX_KEYS.DEFAULT_KEY); expect(cachedValue).toBe(DEFAULT_VALUE); return StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY).then((storedValue) => expect(parseInt(storedValue as string, 10)).toBe(DEFAULT_VALUE)); }); @@ -70,7 +70,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value in Onyx, the cache, and Storage is the default key state expect(onyxValue).toBe(DEFAULT_VALUE); - const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); + const cachedValue = cache.get(ONYX_KEYS.DEFAULT_KEY); expect(cachedValue).toBe(DEFAULT_VALUE); return StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY).then((storedValue) => expect(parseInt(storedValue as string, 10)).toBe(DEFAULT_VALUE)); }); @@ -94,7 +94,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value of the preserved key is also still set in both the cache and storage expect(regularKeyOnyxValue).toBe(SET_VALUE); - expect(cache.getValue(ONYX_KEYS.REGULAR_KEY)).toBe(SET_VALUE); + expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBe(SET_VALUE); return StorageMock.getItem(ONYX_KEYS.REGULAR_KEY).then((storedValue) => expect(parseInt(storedValue as string, 10)).toBe(SET_VALUE)); }); }); @@ -111,7 +111,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value in Onyx, the cache, and Storage is the set value expect(onyxValue).toBe(SET_VALUE); - const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); + const cachedValue = cache.get(ONYX_KEYS.DEFAULT_KEY); expect(cachedValue).toBe(SET_VALUE); return StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY).then((storedValue) => expect(parseInt(storedValue as string, 10)).toBe(SET_VALUE)); }); diff --git a/tests/unit/onyxClearWebStorageTest.ts b/tests/unit/onyxClearWebStorageTest.ts index d9117612..cbf46cbd 100644 --- a/tests/unit/onyxClearWebStorageTest.ts +++ b/tests/unit/onyxClearWebStorageTest.ts @@ -56,7 +56,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value in Onyx, the cache, and Storage is the default key state expect(onyxValue).toBe(DEFAULT_VALUE); - const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); + const cachedValue = cache.get(ONYX_KEYS.DEFAULT_KEY); expect(cachedValue).toBe(DEFAULT_VALUE); const storedValue = StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY); return expect(storedValue).resolves.toBe(DEFAULT_VALUE); @@ -73,7 +73,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value in Onyx, the cache, and Storage is the default key state expect(onyxValue).toBe(DEFAULT_VALUE); - const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); + const cachedValue = cache.get(ONYX_KEYS.DEFAULT_KEY); expect(cachedValue).toBe(DEFAULT_VALUE); const storedValue = StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY); return expect(storedValue).resolves.toBe(DEFAULT_VALUE); @@ -98,7 +98,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value of the preserved key is also still set in both the cache and storage expect(regularKeyOnyxValue).toBe(SET_VALUE); - const regularKeyCachedValue = cache.getValue(ONYX_KEYS.REGULAR_KEY); + const regularKeyCachedValue = cache.get(ONYX_KEYS.REGULAR_KEY); expect(regularKeyCachedValue).toBe(SET_VALUE); const regularKeyStoredValue = StorageMock.getItem(ONYX_KEYS.REGULAR_KEY); return expect(regularKeyStoredValue).resolves.toBe(SET_VALUE); @@ -117,7 +117,7 @@ describe('Set data while storage is clearing', () => { return waitForPromisesToResolve().then(() => { // Then the value in Onyx, the cache, and Storage is the set value expect(onyxValue).toBe(SET_VALUE); - const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); + const cachedValue = cache.get(ONYX_KEYS.DEFAULT_KEY); expect(cachedValue).toBe(SET_VALUE); const storedValue = StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY); return expect(storedValue).resolves.toBe(SET_VALUE); diff --git a/tests/unit/onyxMultiMergeWebStorageTest.ts b/tests/unit/onyxMultiMergeWebStorageTest.ts index a9984caa..c3f939c7 100644 --- a/tests/unit/onyxMultiMergeWebStorageTest.ts +++ b/tests/unit/onyxMultiMergeWebStorageTest.ts @@ -45,9 +45,9 @@ describe('Onyx.mergeCollection() and WebStorage', () => { expect(StorageMock.getMockStore().test_3).toEqual(initialTestObject); // And an empty cache values for the collection keys - expect(OnyxCache.getValue('test_1')).not.toBeDefined(); - expect(OnyxCache.getValue('test_2')).not.toBeDefined(); - expect(OnyxCache.getValue('test_3')).not.toBeDefined(); + expect(OnyxCache.get('test_1')).not.toBeDefined(); + expect(OnyxCache.get('test_2')).not.toBeDefined(); + expect(OnyxCache.get('test_3')).not.toBeDefined(); // When we merge additional data const additionalDataOne = {b: 'b', c: 'c', e: [1, 2]}; @@ -75,9 +75,9 @@ describe('Onyx.mergeCollection() and WebStorage', () => { }; // Then our new data should merge with the existing data in the cache - expect(OnyxCache.getValue('test_1')).toEqual(finalObject); - expect(OnyxCache.getValue('test_2')).toEqual(finalObject); - expect(OnyxCache.getValue('test_3')).toEqual(finalObject); + expect(OnyxCache.get('test_1')).toEqual(finalObject); + expect(OnyxCache.get('test_2')).toEqual(finalObject); + expect(OnyxCache.get('test_3')).toEqual(finalObject); // And the storage should reflect the same state expect(StorageMock.getMockStore().test_1).toEqual(finalObject); @@ -93,9 +93,9 @@ describe('Onyx.mergeCollection() and WebStorage', () => { expect(StorageMock.getMockStore().test_3).toBeFalsy(); // And an empty cache values for the collection keys - expect(OnyxCache.getValue('test_1')).toBeFalsy(); - expect(OnyxCache.getValue('test_2')).toBeFalsy(); - expect(OnyxCache.getValue('test_3')).toBeFalsy(); + expect(OnyxCache.get('test_1')).toBeFalsy(); + expect(OnyxCache.get('test_2')).toBeFalsy(); + expect(OnyxCache.get('test_3')).toBeFalsy(); // When we merge additional data and wait for the change const data = {a: 'a', b: 'b'}; @@ -108,9 +108,9 @@ describe('Onyx.mergeCollection() and WebStorage', () => { return waitForPromisesToResolve() .then(() => { // Then the cache and storage should match - expect(OnyxCache.getValue('test_1')).toEqual(data); - expect(OnyxCache.getValue('test_2')).toEqual(data); - expect(OnyxCache.getValue('test_3')).toEqual(data); + expect(OnyxCache.get('test_1')).toEqual(data); + expect(OnyxCache.get('test_2')).toEqual(data); + expect(OnyxCache.get('test_3')).toEqual(data); expect(StorageMock.getMockStore().test_1).toEqual(data); expect(StorageMock.getMockStore().test_2).toEqual(data); expect(StorageMock.getMockStore().test_3).toEqual(data); @@ -137,9 +137,9 @@ describe('Onyx.mergeCollection() and WebStorage', () => { }; // Then our new data should merge with the existing data in the cache - expect(OnyxCache.getValue('test_1')).toEqual(finalObject); - expect(OnyxCache.getValue('test_2')).toEqual(finalObject); - expect(OnyxCache.getValue('test_3')).toEqual(finalObject); + expect(OnyxCache.get('test_1')).toEqual(finalObject); + expect(OnyxCache.get('test_2')).toEqual(finalObject); + expect(OnyxCache.get('test_3')).toEqual(finalObject); // And the storage should reflect the same state expect(StorageMock.getMockStore().test_1).toEqual(finalObject); @@ -178,7 +178,7 @@ describe('Onyx.mergeCollection() and WebStorage', () => { f: 'f', }; - expect(OnyxCache.getValue('test_1')).toEqual(finalObject); + expect(OnyxCache.get('test_1')).toEqual(finalObject); expect(StorageMock.getMockStore().test_1).toEqual(finalObject); }); }); From d6e24a94bad561b372c77149473e091f3b155b6e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 27 May 2024 15:40:38 +0200 Subject: [PATCH 36/96] fix: rename function --- lib/OnyxCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 4b5208e7..c6cd8878 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -40,7 +40,7 @@ class OnyxCache { bindAll( this, 'getAllKeys', - 'getValue', + 'get', 'hasCacheForKey', 'addKey', 'set', From 861733d691ec4f508a4828ba696a6ad2052ef0c3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 13:51:20 +0200 Subject: [PATCH 37/96] simplify withOnyx conditions --- lib/withOnyx.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/withOnyx.js b/lib/withOnyx.js index 39b7c848..38e5a8bf 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -222,17 +222,13 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // If initialValue is there and the state contains something different it means // an update has already been received and we can discard the value we are trying to hydrate - if (!_.isUndefined(initialValue) && !_.isUndefined(prevState[key]) && prevState[key] !== initialValue) { - if (prevState[key] !== null) { - // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - } - } else if (!_.isUndefined(prevState[key])) { + if (initialValue !== undefined && prevState[key] !== undefined && prevState[key] !== initialValue && prevState[key] !== null) { + // eslint-disable-next-line no-param-reassign + result[key] = prevState[key]; + } else if (prevState[key] !== undefined && prevState[key] !== null) { // if value is already there (without initial value) then we can discard the value we are trying to hydrate - if (prevState[key] !== null) { - // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - } + // eslint-disable-next-line no-param-reassign + result[key] = prevState[key]; } else if (value !== null) { // eslint-disable-next-line no-param-reassign result[key] = value; @@ -327,7 +323,8 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // Remove any internal state properties used by withOnyx // that should not be passed to a wrapped component - const stateToPass = _.omit(this.state, 'loading'); + let stateToPass = _.omit(this.state, 'loading'); + stateToPass = _.omit(stateToPass, _.isNull); // Spreading props and state is necessary in an HOC where the data cannot be predicted return ( From d11555bdc976c378eb1e62ecceaf6e2f0c7328dd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 14:21:52 +0200 Subject: [PATCH 38/96] update cache --- lib/OnyxCache.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index c6cd8878..c5eb5cdf 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -43,6 +43,7 @@ class OnyxCache { 'get', 'hasCacheForKey', 'addKey', + 'addNullishStorageKey', 'set', 'drop', 'merge', @@ -82,7 +83,7 @@ class OnyxCache { this.storageKeys.add(key); } - /** Used to set keys that have been fetched before and were null + /** Used to set keys that are null/undefined in storage without addding null to the storage map */ addNullishStorageKey(key: OnyxKey): void { this.nullishStorageKeys.add(key); From 7879c9f1e5f878e9e309345d84c2fd33dacbd429 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 14:21:59 +0200 Subject: [PATCH 39/96] update comment --- lib/OnyxUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index cc9c3250..d42cc5aa 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -207,8 +207,7 @@ function reduceCollectionWithSelector> { - // When we already have the value in cache (or the key has been set to null, - // and therefore no data is set in cache), resolve right away. + // When we already have the value in cache - resolve right away if (cache.hasCacheForKey(key)) { return Promise.resolve(cache.get(key)); } From 018726bf8cb667d866712725091e7c42b3652994 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 15:08:21 +0200 Subject: [PATCH 40/96] don't remove keys from cache when deleted --- lib/OnyxCache.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index c5eb5cdf..271863aa 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -115,7 +115,7 @@ class OnyxCache { this.nullishStorageKeys.delete(key); if (value === null || value === undefined) { - this.drop(key); + delete this.storageMap[key]; return undefined; } @@ -142,12 +142,15 @@ class OnyxCache { this.storageMap = {...utils.fastMerge(this.storageMap, data)}; - const storageKeys = this.getAllKeys(); - const mergedKeys = Object.keys(data); - this.storageKeys = new Set([...storageKeys, ...mergedKeys]); - mergedKeys.forEach((key) => { + Object.entries(data).forEach(([key, value]) => { + this.addKey(key); this.addToAccessedKeys(key); - this.nullishStorageKeys.delete(key); + + if (value === null || value === undefined) { + this.nullishStorageKeys.add(key); + } else { + this.nullishStorageKeys.delete(key); + } }); } From 34c77c9da3ff1d3839e0c83a129e8d9cbb8440be Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 15:09:30 +0200 Subject: [PATCH 41/96] fix: tests expecting null instead of undefined --- tests/unit/onyxClearWebStorageTest.ts | 2 +- tests/unit/onyxTest.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/onyxClearWebStorageTest.ts b/tests/unit/onyxClearWebStorageTest.ts index cbf46cbd..6fb52fbc 100644 --- a/tests/unit/onyxClearWebStorageTest.ts +++ b/tests/unit/onyxClearWebStorageTest.ts @@ -158,7 +158,7 @@ describe('Set data while storage is clearing', () => { expect(collectionCallback).toHaveBeenCalledTimes(3); // And it should be called with the expected parameters each time - expect(collectionCallback).toHaveBeenNthCalledWith(1, null, undefined); + expect(collectionCallback).toHaveBeenNthCalledWith(1, undefined, undefined); expect(collectionCallback).toHaveBeenNthCalledWith(2, { test_1: 1, test_2: 2, diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 0f4e6784..95c189be 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -816,7 +816,7 @@ describe('Onyx', () => { expect(mockCallback).toHaveBeenCalledTimes(2); // AND the value for the first call should be null since the collection was not initialized at that point - expect(mockCallback).toHaveBeenNthCalledWith(1, null, undefined); + expect(mockCallback).toHaveBeenNthCalledWith(1, undefined, undefined); // AND the value for the second call should be collectionUpdate since the collection was updated expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate); @@ -845,7 +845,7 @@ describe('Onyx', () => { expect(mockCallback).toHaveBeenCalledTimes(2); // AND the value for the first call should be null since the collection was not initialized at that point - expect(mockCallback).toHaveBeenNthCalledWith(1, null, undefined); + expect(mockCallback).toHaveBeenNthCalledWith(1, undefined, undefined); // AND the value for the second call should be collectionUpdate since the collection was updated expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate.testPolicy_1, 'testPolicy_1'); @@ -987,11 +987,11 @@ describe('Onyx', () => { {onyxMethod: Onyx.METHOD.MERGE_COLLECTION, key: ONYX_KEYS.COLLECTION.TEST_UPDATE, value: {[itemKey]: {a: 'a'}}}, ]).then(() => { expect(collectionCallback).toHaveBeenCalledTimes(2); - expect(collectionCallback).toHaveBeenNthCalledWith(1, null, undefined); + expect(collectionCallback).toHaveBeenNthCalledWith(1, undefined, undefined); expect(collectionCallback).toHaveBeenNthCalledWith(2, {[itemKey]: {a: 'a'}}); expect(testCallback).toHaveBeenCalledTimes(2); - expect(testCallback).toHaveBeenNthCalledWith(1, null, undefined); + expect(testCallback).toHaveBeenNthCalledWith(1, undefined, undefined); expect(testCallback).toHaveBeenNthCalledWith(2, 'taco', ONYX_KEYS.TEST_KEY); expect(otherTestCallback).toHaveBeenCalledTimes(2); From 6f2e453a4eef177efc9d5eb620186a33ddf74031 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 15:43:58 +0200 Subject: [PATCH 42/96] fix: withOnyx loading state --- lib/withOnyx.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/withOnyx.js b/lib/withOnyx.js index 38e5a8bf..ca9f4455 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -10,6 +10,7 @@ import Onyx from './Onyx'; import * as Str from './Str'; import utils from './utils'; import OnyxUtils from './OnyxUtils'; +import cache from './OnyxCache'; // This is a list of keys that can exist on a `mapping`, but are not directly related to loading data from Onyx. When the keys of a mapping are looped over to check // if a key has changed, it's a good idea to skip looking at these properties since they would have unexpected results. @@ -60,12 +61,10 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { (resultObj, mapping, propertyName) => { const key = Str.result(mapping.key, props); let value = OnyxUtils.tryGetCachedValue(key, mapping); - if (!value && mapping.initialValue) { - if (mapping.initialValue === null) { - value = undefined; - } else { - value = mapping.initialValue; - } + const hasCacheForKey = cache.hasCacheForKey(key); + + if (!hasCacheForKey && !value && mapping.initialValue) { + value = mapping.initialValue === null ? undefined : mapping.initialValue; } /** @@ -79,7 +78,11 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { * In reality, Onyx.merge() will only update the subscriber after all merges have been batched and the previous value is retrieved via a get() (returns a promise). * So, we won't use the cache optimization here as it will lead us to arbitrarily defer various actions in the application code. */ - if (mapping.initWithStoredValues !== false && ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData)) { + const hasPendingMergeForKey = OnyxUtils.hasPendingMergeForKey(key); + const hasValueInCache = hasCacheForKey || value !== undefined; + const shouldSetState = mapping.initWithStoredValues !== false && ((hasValueInCache && !hasPendingMergeForKey) || !!mapping.allowStaleData); + + if (shouldSetState) { // eslint-disable-next-line no-param-reassign resultObj[propertyName] = value; } From a0abbb79458a39c452f69137718e846412c30def Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 17:21:10 +0200 Subject: [PATCH 43/96] fix: clear nullish storage keys on clear --- lib/Onyx.ts | 2 ++ lib/OnyxCache.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 184a8155..620151a3 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -556,6 +556,8 @@ function mergeCollection(collectionKey: TK function clear(keysToPreserve: OnyxKey[] = []): Promise { return OnyxUtils.getAllKeys() .then((keys) => { + cache.clearNullishStorageKeys(); + const keysToBeClearedFromStorage: OnyxKey[] = []; const keyValuesToResetAsCollection: Record> = {}; const keyValuesToResetIndividually: NullableKeyValueMapping = {}; diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 271863aa..d86bb477 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -44,6 +44,7 @@ class OnyxCache { 'hasCacheForKey', 'addKey', 'addNullishStorageKey', + 'clearNullishStorageKeys', 'set', 'drop', 'merge', @@ -89,6 +90,12 @@ class OnyxCache { this.nullishStorageKeys.add(key); } + /** Used to clear keys that are null/undefined in cache + */ + clearNullishStorageKeys(): void { + this.nullishStorageKeys = new Set(); + } + /** Check whether cache has data for the given key */ hasCacheForKey(key: OnyxKey): boolean { return this.storageMap[key] !== undefined || this.nullishStorageKeys.has(key); From 26e2af4fd6be1596254c0a81a38900f5834bd9a4 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 17:29:01 +0200 Subject: [PATCH 44/96] remove line (re-trigger tests) --- lib/OnyxCache.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index d86bb477..07ea3b9c 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -2,7 +2,6 @@ import {deepEqual} from 'fast-equals'; import bindAll from 'lodash/bindAll'; import utils from './utils'; import type {OnyxKey, OnyxValue} from './types'; - /** * In memory cache providing data by reference * Encapsulates Onyx cache related functionality From 564868c628a37bc420012a2d1290eee5725cee0d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 29 May 2024 17:29:13 +0200 Subject: [PATCH 45/96] add back line --- lib/OnyxCache.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 07ea3b9c..d86bb477 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -2,6 +2,7 @@ import {deepEqual} from 'fast-equals'; import bindAll from 'lodash/bindAll'; import utils from './utils'; import type {OnyxKey, OnyxValue} from './types'; + /** * In memory cache providing data by reference * Encapsulates Onyx cache related functionality From 81b1f4b40430869a5f4e97abd4d149aa84a56c99 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 29 May 2024 18:55:33 +0000 Subject: [PATCH 46/96] 2.0.42 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0e686bb..e5438adc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.41", + "version": "2.0.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.41", + "version": "2.0.42", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 777e3aa0..621bc74c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.41", + "version": "2.0.42", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native", From 270db80e541a48e460ad6b0ddab326aa228873fa Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 30 May 2024 09:34:50 +0200 Subject: [PATCH 47/96] Update OnyxCache.ts Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- lib/OnyxCache.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index d86bb477..51237cc9 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -90,8 +90,7 @@ class OnyxCache { this.nullishStorageKeys.add(key); } - /** Used to clear keys that are null/undefined in cache - */ + /** Used to clear keys that are null/undefined in cache */ clearNullishStorageKeys(): void { this.nullishStorageKeys = new Set(); } From bbfb198dbe94bed21f5a3f6e11efc20861a410c3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 30 May 2024 09:35:15 +0200 Subject: [PATCH 48/96] Update OnyxCache.ts Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- lib/OnyxCache.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 51237cc9..d7f5af3e 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -84,8 +84,7 @@ class OnyxCache { this.storageKeys.add(key); } - /** Used to set keys that are null/undefined in storage without addding null to the storage map - */ + /** Used to set keys that are null/undefined in storage without adding null to the storage map */ addNullishStorageKey(key: OnyxKey): void { this.nullishStorageKeys.add(key); } From ec1eea341e087e0aa9d9a856bab151ef43661ba0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 30 May 2024 16:20:47 +0200 Subject: [PATCH 49/96] improve withOnyx logic --- lib/withOnyx.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/withOnyx.js b/lib/withOnyx.js index ca9f4455..49e6b7dd 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -182,7 +182,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { * @param {*} val */ setWithOnyxState(statePropertyName, val) { - const prevValue = this.state[statePropertyName]; + const prevVal = this.state[statePropertyName]; // If the component is not loading (read "mounting"), then we can just update the state // There is a small race condition. @@ -193,7 +193,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { // This simply bypasses the loading check if the tempState is gone and the update can be safely queued with a normal setStateProxy. if (!this.state.loading || !this.tempState) { // Performance optimization, do not trigger update with same values - if (prevValue === val || (utils.isEmptyObject(prevValue) && utils.isEmptyObject(val))) { + if (prevVal === val || (utils.isEmptyObject(prevVal) && utils.isEmptyObject(val))) { return; } @@ -216,23 +216,25 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { this.setState((prevState) => { const finalState = _.reduce( stateUpdate, - (result, value, key) => { + (result, valueWithNull, key) => { if (key === 'loading') { return result; } - const initialValue = mapOnyxToState[key].initialValue; + const initialValue = mapOnyxToState[key].initialValue == null ? undefined : mapOnyxToState[key].initialValue; + const prevValue = prevState[key] === null ? undefined : prevState[key]; + const value = valueWithNull === null ? undefined : valueWithNull; // If initialValue is there and the state contains something different it means // an update has already been received and we can discard the value we are trying to hydrate - if (initialValue !== undefined && prevState[key] !== undefined && prevState[key] !== initialValue && prevState[key] !== null) { + if (initialValue !== undefined && prevValue !== undefined && prevValue !== initialValue) { // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - } else if (prevState[key] !== undefined && prevState[key] !== null) { + result[key] = prevValue; + } else if (prevValue !== undefined) { // if value is already there (without initial value) then we can discard the value we are trying to hydrate // eslint-disable-next-line no-param-reassign - result[key] = prevState[key]; - } else if (value !== null) { + result[key] = prevValue; + } else { // eslint-disable-next-line no-param-reassign result[key] = value; } From 30b0347d1f577b1b2e887ffe6697730448c3f3df Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 30 May 2024 16:20:52 +0200 Subject: [PATCH 50/96] add comments --- lib/Onyx.ts | 4 ++++ lib/OnyxCache.ts | 7 +++++-- lib/OnyxUtils.ts | 11 +++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 620151a3..45065165 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -210,6 +210,8 @@ function disconnect(connectionID: number, keyToRemoveFromEvictionBlocklist?: Ony * @param value value to store */ function set(key: TKey, value: NonUndefined>): 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)) { delete OnyxUtils.getMergeQueue()[key]; } @@ -274,6 +276,8 @@ function set(key: TKey, value: NonUndefined): Promise { const allKeyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(data, true); + // When a key is set to null, we need to remove the remove the key from storage using "OnyxUtils.remove". + // Therefore, we filter the key value pairs to exclude null values and remove those keys explicitly. const removePromises: Array> = []; const keyValuePairsToUpdate = allKeyValuePairs.filter(([key, value]) => { if (value === null) { diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index d7f5af3e..2d323369 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -11,7 +11,7 @@ class OnyxCache { /** Cache of all the storage keys available in persistent storage */ private storageKeys: Set; - /** Cache of all the storage keys that have been fetched before and were not set */ + /** A list of keys where a nullish value has been fetched from storage before, but the key still exists in cache */ private nullishStorageKeys: Set; /** Unique list of keys maintained in access order (most recent at the end) */ @@ -117,6 +117,9 @@ class OnyxCache { set(key: OnyxKey, value: OnyxValue): OnyxValue { this.addKey(key); this.addToAccessedKeys(key); + + // When a key is explicitly set in cache, we can remove it from the list of nullish keys, + // since it will either be set to a non nullish value or removed from the cache completely. this.nullishStorageKeys.delete(key); if (value === null || value === undefined) { @@ -152,7 +155,7 @@ class OnyxCache { this.addToAccessedKeys(key); if (value === null || value === undefined) { - this.nullishStorageKeys.add(key); + this.addNullishStorageKey(key); } else { this.nullishStorageKeys.delete(key); } diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index d42cc5aa..c00cfe0b 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -432,20 +432,21 @@ function getCachedCollection(collectionKey: TKey function keysChanged( collectionKey: TKey, partialCollection: OnyxCollection, - partialPreviousCollection: OnyxCollection, + partialPreviousCollection: OnyxCollection | undefined, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true, ): void { // We prepare the "cached collection" which is the entire collection + the new partial data that // was merged in via mergeCollection(). const cachedCollection = getCachedCollection(collectionKey); - const previousCollection = partialPreviousCollection ?? {}; // If the previous collection equals the new collection then we do not need to notify any subscribers. if (partialPreviousCollection !== undefined && deepEqual(cachedCollection, partialPreviousCollection)) { return; } + const previousCollection = partialPreviousCollection ?? {}; + // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or // individual collection key member for the collection that is being updated. It is important to note that the collection parameter cane be a PARTIAL collection // and does not represent all of the combined keys and values for a collection key. It is just the "new" data that was merged in via mergeCollection(). @@ -783,6 +784,12 @@ function sendDataToConnection(mapping: Mapping, valu return; } + // When there are no matching keys in "Onyx.connect", we pass null to "sendDataToConnection" explicitly, + // to allow the withOnyx instance to set the value in the state initially and therefore stop the loading state once all + // required keys have been set. + // If we would pass undefined to setWithOnyxInstance instead, withOnyx would not set the value in the state. + // withOnyx will internally replace null values with undefined and never pass null values to wrapped components. + // For regular callbacks, we never want to pass null values, but always just undefined if a value is not set in cache or storage. (mapping as DefaultConnectOptions).callback?.(value === null ? undefined : value, matchedKey as TKey); } From 9f6583a5527647adaeb0aca972695d9c73487526 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 30 May 2024 16:22:13 +0200 Subject: [PATCH 51/96] fix: unnecessary null check --- lib/withOnyx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/withOnyx.js b/lib/withOnyx.js index 49e6b7dd..e1444cfe 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -64,7 +64,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) { const hasCacheForKey = cache.hasCacheForKey(key); if (!hasCacheForKey && !value && mapping.initialValue) { - value = mapping.initialValue === null ? undefined : mapping.initialValue; + value = mapping.initialValue; } /** From a367599baa3e28ba1badfaffcce338387c034725 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 30 May 2024 21:53:39 +0000 Subject: [PATCH 52/96] 2.0.43 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5438adc..ea051853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.42", + "version": "2.0.43", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.42", + "version": "2.0.43", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 621bc74c..4a92714d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.42", + "version": "2.0.43", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native", From 3b6b49d457b2daa6b2bf7ade25608b50dcee8121 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 31 May 2024 10:35:00 +0200 Subject: [PATCH 53/96] fix: onyx input method types (deep nullish values) --- lib/Onyx.ts | 20 +++++++++--------- lib/OnyxUtils.ts | 3 +-- lib/types.ts | 48 +++++++++++++++++++++++++++++++++++-------- lib/withOnyx/types.ts | 4 ++-- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 45065165..59d89b99 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -8,18 +8,18 @@ import Storage from './storage'; import utils from './utils'; import DevTools from './DevTools'; import type { - Collection, CollectionKeyBase, ConnectOptions, InitOptions, KeyValueMapping, Mapping, - NonUndefined, NullableKeyValueMapping, - NullishDeep, OnyxCollection, - OnyxEntry, OnyxKey, + OnyxMergeCollectionInput, + OnyxMergeInput, + OnyxMultiSetInput, + OnyxSetInput, OnyxUpdate, OnyxValue, } from './types'; @@ -209,7 +209,7 @@ function disconnect(connectionID: number, keyToRemoveFromEvictionBlocklist?: Ony * @param key ONYXKEY to set * @param value value to store */ -function set(key: TKey, value: NonUndefined>): Promise { +function set(key: TKey, value: OnyxSetInput): 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)) { @@ -273,7 +273,7 @@ function set(key: TKey, value: NonUndefined): Promise { +function multiSet(data: OnyxMultiSetInput): Promise { const allKeyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(data, true); // When a key is set to null, we need to remove the remove the key from storage using "OnyxUtils.remove". @@ -321,7 +321,7 @@ function multiSet(data: Partial): Promise { * Onyx.merge(ONYXKEYS.POLICY, {id: 1}); // -> {id: 1} * Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'} */ -function merge(key: TKey, changes: NonUndefined>>): Promise { +function merge(key: TKey, changes: OnyxMergeInput): Promise { const mergeQueue = OnyxUtils.getMergeQueue(); const mergeQueuePromise = OnyxUtils.getMergeQueuePromise(); @@ -430,7 +430,7 @@ function merge(key: TKey, changes: NonUndefined(collectionKey: TKey, collection: Collection>): Promise { +function mergeCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { if (typeof collection !== 'object' || Array.isArray(collection) || utils.isEmptyObject(collection)) { Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.'); return Promise.resolve(); @@ -564,7 +564,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { const keysToBeClearedFromStorage: OnyxKey[] = []; const keyValuesToResetAsCollection: Record> = {}; - const keyValuesToResetIndividually: NullableKeyValueMapping = {}; + const keyValuesToResetIndividually: KeyValueMapping = {}; // The only keys that should not be cleared are: // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline @@ -619,7 +619,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { const defaultKeyValuePairs = Object.entries( Object.keys(defaultKeyStates) .filter((key) => !keysToPreserve.includes(key)) - .reduce((obj: NullableKeyValueMapping, key) => { + .reduce((obj: KeyValueMapping, key) => { // eslint-disable-next-line no-param-reassign obj[key] = defaultKeyStates[key]; return obj; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 7abdeeaa..8403676f 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -19,7 +19,6 @@ import type { DefaultConnectOptions, KeyValueMapping, Mapping, - NullableKeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, @@ -103,7 +102,7 @@ function getDefaultKeyStates(): Record> { * @param initialKeyStates - initial data to set when `init()` and `clear()` are called * @param safeEvictionKeys - This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal. */ -function initStoreValues(keys: DeepRecord, initialKeyStates: Partial, safeEvictionKeys: OnyxKey[]): void { +function initStoreValues(keys: DeepRecord, initialKeyStates: Partial, safeEvictionKeys: OnyxKey[]): void { // We need the value of the collection keys later for checking if a // key is a collection. We store it in a map for faster lookup. const collectionValues = Object.values(keys.COLLECTION ?? {}); diff --git a/lib/types.ts b/lib/types.ts index 28f98a2b..5f73f7c2 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -136,7 +136,7 @@ type KeyValueMapping = { * It's very similar to `KeyValueMapping` but this type accepts using `null` as well. */ type NullableKeyValueMapping = { - [TKey in OnyxKey]: OnyxValue; + [TKey in OnyxKey]: NonUndefined> | null; }; /** @@ -180,6 +180,13 @@ type Selector = (value: OnyxEntry */ type OnyxEntry = TOnyxValue | undefined; +/** + * Represents an input value that can be passed to Onyx methods, that can be either `TOnyxValue` or `null`. + * Setting a key to `null` will remove the key from the store. + * `undefined` is not allowed for setting values, because it will have no effect on the data. + */ +type OnyxInput = TOnyxValue | null; + /** * Represents an Onyx collection of entries, that can be either a record of `TOnyxValue`s or `null` / `undefined` if it is empty or doesn't exist. * @@ -261,7 +268,7 @@ type NullishObjectDeep = { * Also, the `TMap` type is inferred automatically in `mergeCollection()` method and represents * the object of collection keys/values specified in the second parameter of the method. */ -type Collection = { +type Collection = { [MapK in keyof TMap]: MapK extends `${TKey}${string}` ? MapK extends `${TKey}` ? never // forbids empty id @@ -321,6 +328,26 @@ type Mapping = ConnectOptions & { connectionID: number; }; +/** + * This represents the value that can be passed to `Onyx.set` and to `Onyx.update` with the method "SET" + */ +type OnyxSetInput = OnyxInput; + +/** + * This represents the value that can be passed to `Onyx.multiSet` and to `Onyx.update` with the method "MULTI_SET" + */ +type OnyxMultiSetInput = Partial; + +/** + * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" + */ +type OnyxMergeInput = OnyxInput>; + +/** + * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" + */ +type OnyxMergeCollectionInput = Collection, TMap>; + /** * Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of * different update methods (`SET`, `MERGE`, `MERGE_COLLECTION`), each with their own key and value structure. @@ -331,17 +358,17 @@ type OnyxUpdate = | { onyxMethod: typeof OnyxUtils.METHOD.SET; key: TKey; - value: NonUndefined>; + value: OnyxSetInput; } | { - onyxMethod: typeof OnyxUtils.METHOD.MERGE; + onyxMethod: typeof OnyxUtils.METHOD.MULTI_SET; key: TKey; - value: NonUndefined>>; + value: OnyxMultiSetInput; } | { - onyxMethod: typeof OnyxUtils.METHOD.MULTI_SET; + onyxMethod: typeof OnyxUtils.METHOD.MERGE; key: TKey; - value: Partial; + value: OnyxMergeInput; } | { onyxMethod: typeof OnyxUtils.METHOD.CLEAR; @@ -353,7 +380,7 @@ type OnyxUpdate = [TKey in CollectionKeyBase]: { onyxMethod: typeof OnyxUtils.METHOD.MERGE_COLLECTION; key: TKey; - value: Record<`${TKey}${string}`, NullishDeep>; + value: OnyxMergeCollectionInput; }; }[CollectionKeyBase]; @@ -417,7 +444,12 @@ export type { NullishDeep, OnyxCollection, OnyxEntry, + OnyxInput, OnyxKey, + OnyxSetInput, + OnyxMultiSetInput, + OnyxMergeInput, + OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector, diff --git a/lib/withOnyx/types.ts b/lib/withOnyx/types.ts index 06145551..4b8b85ec 100644 --- a/lib/withOnyx/types.ts +++ b/lib/withOnyx/types.ts @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import type {IsEqual} from 'type-fest'; -import type {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, NullableKeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector} from '../types'; +import type {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector} from '../types'; /** * Represents the base mapping options between an Onyx key and the component's prop. @@ -162,7 +162,7 @@ type WithOnyxState = TOnyxProps & { /** * Represents the `withOnyx` internal component instance. */ -type WithOnyxInstance = React.Component> & { +type WithOnyxInstance = React.Component> & { setStateProxy: (modifier: Record> | ((state: Record>) => OnyxValue)) => void; setWithOnyxState: (statePropertyName: OnyxKey, value: OnyxValue) => void; }; From ebbf0e182f7bd896f1d9c604c7202744c111f183 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 31 May 2024 10:45:31 +0200 Subject: [PATCH 54/96] fix: replace never with object --- lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types.ts b/lib/types.ts index 5f73f7c2..f1635026 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -346,7 +346,7 @@ type OnyxMergeInput = OnyxInput = Collection, TMap>; +type OnyxMergeCollectionInput = Collection, TMap>; /** * Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of From 72bea5108d637a354c3593ace5113b7c7091bb68 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 31 May 2024 11:15:32 +0200 Subject: [PATCH 55/96] export types --- lib/index.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/index.ts b/lib/index.ts index 79bcf856..3352d39e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,6 +1,20 @@ import type {ConnectOptions, OnyxUpdate} from './Onyx'; import Onyx from './Onyx'; -import type {CustomTypeOptions, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector} from './types'; +import type { + CustomTypeOptions, + KeyValueMapping, + NullishDeep, + OnyxCollection, + OnyxEntry, + OnyxInput, + OnyxKey, + OnyxValue, + Selector, + OnyxSetInput, + OnyxMultiSetInput, + OnyxMergeInput, + OnyxMergeCollectionInput, +} from './types'; import type {FetchStatus, ResultMetadata, UseOnyxResult} from './useOnyx'; import useOnyx from './useOnyx'; import withOnyx from './withOnyx'; @@ -16,7 +30,12 @@ export type { NullishDeep, OnyxCollection, OnyxEntry, + OnyxInput, OnyxKey, + OnyxSetInput, + OnyxMultiSetInput, + OnyxMergeInput, + OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, ResultMetadata, From ebc82434cd9b6c71e56ffd5dcb81a2c00dbc50f3 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 31 May 2024 14:49:43 +0000 Subject: [PATCH 56/96] 2.0.44 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea051853..fb2ce6a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.43", + "version": "2.0.44", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.43", + "version": "2.0.44", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 4a92714d..301cb3ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.43", + "version": "2.0.44", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native", From c6a93542855d5219dd5553cfba10718dbd5cea60 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Mon, 3 Jun 2024 14:06:46 -0700 Subject: [PATCH 57/96] Update README.md to promote useOnyx usage over withOnyx --- README.md | 134 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 52461ffb..4b326d1f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Awesome persistent storage solution wrapped in a Pub/Sub library. - Onyx allows other code to subscribe to changes in data, and then publishes change events whenever data is changed - Anything needing to read Onyx data needs to: 1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage) - 2. Subscribe to changes of the data for a particular key or set of keys. React components use `withOnyx()` and non-React libs use `Onyx.connect()`. + 2. Subscribe to changes of the data for a particular key or set of keys. React functional components use the `useOnyx()` hook (recommended), class components use `withOnyx()` HOC (deprecated, not-recommended) and non-React libs use `Onyx.connect()`. 3. Get initialized with the current value of that key from persistent storage (Onyx does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process) - Subscribing to Onyx keys is done using a constant defined in `ONYXKEYS`. Each Onyx key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (e.g. display a list of them in the nav menu), then it would subscribe to the key `ONYXKEYS.COLLECTION.REPORT`. @@ -116,7 +116,32 @@ To teardown the subscription call `Onyx.disconnect()` with the `connectionID` re Onyx.disconnect(connectionID); ``` -We can also access values inside React components via the `withOnyx()` [higher order component](https://reactjs.org/docs/higher-order-components.html). When the data changes the component will re-render. +We can also access values inside React functional components via the `useOnyx()` [hook](https://react.dev/reference/react/hooks) (recommended) or class components via the `withOnyx()` [higher order component](https://reactjs.org/docs/higher-order-components.html) (deprecated, not-recommended). When the data changes the component will re-render. + +```javascript +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; + +const App = () => { + const [session] = useOnyx('session'); + + return ( + + {session.token ? Logged in : Logged out} + + ); +}; + +export default App; +``` + +While `Onyx.connect()` gives you more control on how your component reacts as data is fetched from disk, `useOnyx()` will delay the rendering of the wrapped component until all keys/entities have been fetched and passed to the component, this can be convenient for simple cases. This however, can really delay your application if many entities are connected to the same component, you can pass an `initialValue` to each key to allow Onyx to eagerly render your component with this value. + +```javascript +const [session] = useOnyx('session', {initialValue: {}}); +``` + +> **Deprecated Note**: Please note, `withOnyx()` Higher Order Component (HOC) is now considered deprecated. Use `useOnyx()` hook instead. ```javascript import React from 'react'; @@ -164,27 +189,68 @@ export default withOnyx({ }, true)(App); ``` -### Dependent Onyx Keys and withOnyx() +### Dependent Onyx Keys and useOnyx() Some components need to subscribe to multiple Onyx keys at once and sometimes, one key might rely on the data from another key. This is similar to a JOIN in SQL. Example: To get the policy of a report, the `policy` key depends on the `report` key. ```javascript -export default withOnyx({ - report: { - key: ({reportID) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, - }, -})(App); +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +const ONYXKEYS = { + REPORT: 'report_1234', + POLICY: 'policy_' +}; + +const App = () => { + const [report] = useOnyx(ONYXKEYS.REPORT); + const [policy] = useOnyx(`${ONYXKEYS.POLICY}${report.policyID}`); + + return ( + + {/* Render with policy data */} + + ); +}; + +export default App; ``` Background info: - The `key` value can be a function that returns the key that Onyx subscribes to - The first argument to the `key` function is the `props` from the component -**Detailed explanation of how this is handled and rendered:** +```javascript +const App = ({reportID}) => { + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`); + + return ( + + {/* Render with policy data */} + + ); +}; + +export default App; +``` + +**Detailed explanation of how this is handled and rendered with `useOnyx()`:** + +1. The component mounts with a `reportID={1234}` prop. +2. The `useOnyx` hook evaluates the mapping and subscribes to the key `reports_1234` using the `reportID` prop. +3. The `useOnyx` hook fetches the data for the key `reports_1234` from Onyx and sets the state with the initial value (if provided). +4. Since `policyID` is not defined yet, it defaults to `undefined`. The `useOnyx` hook subscribes to the key `policies_undefined`. +5. The `useOnyx` hook reads the data and updates the state of the component: + - `report={{reportID: 1234, policyID: 1, ...rest of the object...}}` - `policy={undefined}` (since there is no policy with ID `undefined`) +6. As there is still an `undefined` key, the `useOnyx` hook again evaluates the key `policies_1` after fetching the updated `report` object which has `policyID: 1`. +7. The `useOnyx` hook reads the data and updates the state with: + - `policy={{policyID: 1, ...rest of the object...}}` +8. Now, all mappings have values that are defined (not undefined), and the component is rendered with all necessary data. + +* It is VERY important to NOT use empty string default values like `report.policyID || ''`. This results in the key returned to `useOnyx` as `policies_`, which subscribes to the ENTIRE POLICY COLLECTION and is most assuredly not what you were intending. You can use a default of `0` (as long as you are reasonably sure that there is never a policyID=0). This allows Onyx to return `undefined` as the value of the policy key, which is handled by `useOnyx` appropriately. + +**Detailed explanation of how this is handled and rendered with `withOnyx` HOC:** 1. The component mounts with a `reportID={1234}` prop 2. `withOnyx` evaluates the mapping 3. `withOnyx` connects to the key `reports_1234` because of the prop passed to the component @@ -239,15 +305,23 @@ Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { There are several ways to subscribe to these keys: ```javascript -withOnyx({ - allReports: {key: ONYXKEYS.COLLECTION.REPORT}, -})(MyComponent); +const MyComponent = () => { + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + + return ( + + {/* Render with allReports data */} + + ); +}; + +export default MyComponent; ``` This will add a prop to the component called `allReports` which is an object of collection member key/values. Changes to the individual member keys will modify the entire object and new props will be passed with each individual key update. The prop doesn't update on the initial rendering of the component until the entire collection has been read out of Onyx. ```js -Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (memberValue, memberKey) => {...}}); +Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (memberValue, memberKey) => {...}); ``` This will fire the callback once per member key depending on how many collection member keys are currently stored. Changes to those keys after the initial callbacks fire will occur when each individual key is updated. @@ -256,11 +330,11 @@ This will fire the callback once per member key depending on how many collection Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, - callback: (allReports) => {...}}, + callback: (allReports) => {...}, }); ``` -This final option forces `Onyx.connect()` to behave more like `withOnyx()` and only update the callback once with the entire collection initially and later with an updated version of the collection when individual keys update. +This final option forces `Onyx.connect()` to behave more like `useOnyx()` and only update the callback once with the entire collection initially and later with an updated version of the collection when individual keys update. ### Performance Considerations When Using Collections @@ -270,12 +344,12 @@ Remember, `mergeCollection()` will notify a subscriber only *once* with the tota ```js // Bad -_.each(reports, report => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report)); // -> A component using withOnyx() will have it's state updated with each iteration +_.each(reports, report => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report)); // -> A component using useOnyx() will have it's state updated with each iteration // Good const values = {}; _.each(reports, report => values[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = report); -Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, values); // -> A component using withOnyx() will only have it's state updated once +Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, values); // -> A component using useOnyx() will only have its state updated once ``` ## Clean up @@ -325,12 +399,20 @@ Onyx.init({ ``` ```js -export default withOnyx({ - reportActions: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, - canEvict: props => !props.isActiveReport, - }, -})(ReportActionsView); +const ReportActionsView = ({isActiveReport}) => { + const [reportActions] = useOnyx( + ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, + {canEvict: () => !isActiveReport} + ); + + return ( + + {/* Render with reportActions data */} + + ); +}; + +export default ReportActionsView; ``` # Benchmarks From 4e1cb68fa383efbbc2d953010dd491bee07ff305 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 14:16:52 +0200 Subject: [PATCH 58/96] fix: check if kez exists rather than undefined --- lib/withOnyx/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/withOnyx/index.tsx b/lib/withOnyx/index.tsx index 8ecb9545..fdc5bed5 100644 --- a/lib/withOnyx/index.tsx +++ b/lib/withOnyx/index.tsx @@ -228,7 +228,8 @@ export default function ( this.tempState[statePropertyName] = val; // If some key does not have a value yet, do not update the state yet - const tempStateIsMissingKey = requiredKeysForInit.some((key) => this.tempState?.[key as keyof TOnyxProps] === undefined); + const tempStateIsMissingKey = requiredKeysForInit.some((key) => !(key in (this.tempState ?? {}))); + if (tempStateIsMissingKey) { return; } From 138eca1a1a7897900f4240a78fac83b7b1e12e78 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 15:19:59 +0200 Subject: [PATCH 59/96] fix: only add undefined property to collection returned by "getCachedCollection" if it's set as nullish in cache --- lib/OnyxCache.ts | 8 +++++- lib/OnyxUtils.ts | 66 ++++++++++++++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 2d323369..530f8e66 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -44,6 +44,7 @@ class OnyxCache { 'hasCacheForKey', 'addKey', 'addNullishStorageKey', + 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', @@ -89,6 +90,11 @@ class OnyxCache { this.nullishStorageKeys.add(key); } + /** Used to set keys that are null/undefined in storage without adding null to the storage map */ + hasNullishStorageKey(key: OnyxKey): boolean { + return this.nullishStorageKeys.has(key); + } + /** Used to clear keys that are null/undefined in cache */ clearNullishStorageKeys(): void { this.nullishStorageKeys = new Set(); @@ -96,7 +102,7 @@ class OnyxCache { /** Check whether cache has data for the given key */ hasCacheForKey(key: OnyxKey): boolean { - return this.storageMap[key] !== undefined || this.nullishStorageKeys.has(key); + return this.storageMap[key] !== undefined || this.hasNullishStorageKey(key); } /** diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 8403676f..8cd6d475 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -419,6 +419,12 @@ function getCachedCollection(collectionKey: TKey return; } + const cachedValue = cache.get(key); + + if (cachedValue === undefined && !cache.hasNullishStorageKey(key)) { + return; + } + collection[key] = cache.get(key); }); @@ -439,11 +445,6 @@ function keysChanged( // was merged in via mergeCollection(). const cachedCollection = getCachedCollection(collectionKey); - // If the previous collection equals the new collection then we do not need to notify any subscribers. - if (partialPreviousCollection !== undefined && deepEqual(cachedCollection, partialPreviousCollection)) { - return; - } - const previousCollection = partialPreviousCollection ?? {}; // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or @@ -532,12 +533,13 @@ function keysChanged( const previousData = prevState[subscriber.statePropertyName]; const newData = reduceCollectionWithSelector(cachedCollection, collectionSelector, subscriber.withOnyxInstance.state); - if (!deepEqual(previousData, newData)) { - return { - [subscriber.statePropertyName]: newData, - }; + if (deepEqual(previousData, newData)) { + return null; } - return null; + + return { + [subscriber.statePropertyName]: newData, + }; }); continue; } @@ -550,6 +552,10 @@ function keysChanged( finalCollection[dataKey] = cachedCollection[dataKey]; } + if (!prevState.loading && deepEqual(cachedCollection, finalCollection)) { + return null; + } + PerformanceUtils.logSetStateCall(subscriber, prevState?.[subscriber.statePropertyName], finalCollection, 'keysChanged', collectionKey); return { [subscriber.statePropertyName]: finalCollection, @@ -579,14 +585,15 @@ function keysChanged( subscriber.withOnyxInstance.setStateProxy((prevState) => { const prevData = prevState[subscriber.statePropertyName]; const newData = selector(cachedCollection[subscriber.key], subscriber.withOnyxInstance.state); - if (!deepEqual(prevData, newData)) { - PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keysChanged', collectionKey); - return { - [subscriber.statePropertyName]: newData, - }; + + if (deepEqual(prevData, newData)) { + return null; } - return null; + PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keysChanged', collectionKey); + return { + [subscriber.statePropertyName]: newData, + }; }); continue; } @@ -683,13 +690,15 @@ function keyChanged( ...prevWithOnyxData, ...newWithOnyxData, }; - if (!deepEqual(prevWithOnyxData, prevDataWithNewData)) { - PerformanceUtils.logSetStateCall(subscriber, prevWithOnyxData, newWithOnyxData, 'keyChanged', key); - return { - [subscriber.statePropertyName]: prevDataWithNewData, - }; + + if (deepEqual(prevWithOnyxData, prevDataWithNewData)) { + return null; } - return null; + + PerformanceUtils.logSetStateCall(subscriber, prevWithOnyxData, newWithOnyxData, 'keyChanged', key); + return { + [subscriber.statePropertyName]: prevDataWithNewData, + }; }); continue; } @@ -711,16 +720,17 @@ function keyChanged( // If the subscriber has a selector, then the component's state must only be updated with the data // returned by the selector and only if the selected data has changed. if (selector) { - subscriber.withOnyxInstance.setStateProxy(() => { + subscriber.withOnyxInstance.setStateProxy((prevState) => { const prevValue = selector(previousValue, subscriber.withOnyxInstance.state); const newValue = selector(value, subscriber.withOnyxInstance.state); - if (!deepEqual(prevValue, newValue)) { - return { - [subscriber.statePropertyName]: newValue, - }; + if (!prevState.loading && deepEqual(prevValue, newValue)) { + return null; } - return null; + + return { + [subscriber.statePropertyName]: newValue, + }; }); continue; } From da593f163031fcc40d9b89017ffc24a222628d33 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 17:02:28 +0200 Subject: [PATCH 60/96] fix: improve types and add undefined handling --- lib/Onyx.ts | 43 +++++++++++++---------------- lib/OnyxUtils.ts | 36 +++++++++++++------------ lib/types.ts | 70 +++++++++++++++++++++++++----------------------- lib/utils.ts | 24 +++++++++-------- 4 files changed, 86 insertions(+), 87 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 59d89b99..edbf058e 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -13,7 +13,7 @@ import type { InitOptions, KeyValueMapping, Mapping, - NullableKeyValueMapping, + OnyxInputKeyValueMapping, OnyxCollection, OnyxKey, OnyxMergeCollectionInput, @@ -22,6 +22,7 @@ import type { OnyxSetInput, OnyxUpdate, OnyxValue, + OnyxInput, } from './types'; import OnyxUtils from './OnyxUtils'; import logMessages from './logMessages'; @@ -216,10 +217,14 @@ function set(key: TKey, value: OnyxSetInput): Promis delete OnyxUtils.getMergeQueue()[key]; } - const existingValue = cache.get(key, false); + // Onyx.set will ignore `undefined` values as inputs, therefore we can return early. + if (value === undefined) { + return Promise.resolve(); + } + const existingValue = cache.get(key, false); // If the existing value as well as the new value are null, we can return early. - if (value === null && existingValue === null) { + if (existingValue === undefined && value === null) { return Promise.resolve(); } @@ -274,21 +279,9 @@ function set(key: TKey, value: OnyxSetInput): Promis * @param data object keyed by ONYXKEYS and the values to set */ function multiSet(data: OnyxMultiSetInput): Promise { - const allKeyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(data, true); - - // When a key is set to null, we need to remove the remove the key from storage using "OnyxUtils.remove". - // Therefore, we filter the key value pairs to exclude null values and remove those keys explicitly. - const removePromises: Array> = []; - const keyValuePairsToUpdate = allKeyValuePairs.filter(([key, value]) => { - if (value === null) { - removePromises.push(OnyxUtils.remove(key)); - return false; - } - - return true; - }); + const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(data, true); - const updatePromises = keyValuePairsToUpdate.map(([key, value]) => { + const updatePromises = keyValuePairsToSet.map(([key, value]) => { const prevValue = cache.get(key, false); // Update cache and optimistically inform subscribers on the next tick @@ -296,11 +289,11 @@ function multiSet(data: OnyxMultiSetInput): Promise { return OnyxUtils.scheduleSubscriberUpdate(key, value, prevValue); }); - return Storage.multiSet(allKeyValuePairs) + return Storage.multiSet(keyValuePairsToSet) .catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, data)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, data); - return Promise.all([removePromises, updatePromises]); + return Promise.all(updatePromises); }) .then(() => undefined); } @@ -354,7 +347,7 @@ function merge(key: TKey, changes: OnyxMergeInput): Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType)); } return isCompatible; - }); + }) as Array>; if (!validChanges.length) { return Promise.resolve(); @@ -435,7 +428,7 @@ function mergeCollection(collectionKey: TK Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.'); return Promise.resolve(); } - const mergedCollection: NullableKeyValueMapping = collection; + const mergedCollection: OnyxInputKeyValueMapping = collection; // Confirm all the collection keys belong to the same parent let hasCollectionKeyCheckFailed = false; @@ -474,7 +467,7 @@ function mergeCollection(collectionKey: TK const newKeys = keys.filter((key) => !persistedKeys.has(key)); - const existingKeyCollection = existingKeys.reduce((obj: NullableKeyValueMapping, key) => { + const existingKeyCollection = existingKeys.reduce((obj: OnyxInputKeyValueMapping, key) => { const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(mergedCollection[key], cachedCollectionForExistingKeys[key]); if (!isCompatible) { Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType)); @@ -483,13 +476,13 @@ function mergeCollection(collectionKey: TK // eslint-disable-next-line no-param-reassign obj[key] = mergedCollection[key]; return obj; - }, {}); + }, {}) as Record>; - const newCollection = newKeys.reduce((obj: NullableKeyValueMapping, key) => { + const newCollection = newKeys.reduce((obj: OnyxInputKeyValueMapping, key) => { // eslint-disable-next-line no-param-reassign obj[key] = mergedCollection[key]; return obj; - }, {}); + }, {}) as Record>; // When (multi-)merging the values with the existing values in storage, // we don't want to remove nested null values from the data that we pass to the storage layer, diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 8cd6d475..a1cf51fb 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -21,6 +21,7 @@ import type { Mapping, OnyxCollection, OnyxEntry, + OnyxInput, OnyxKey, OnyxValue, Selector, @@ -205,17 +206,17 @@ function reduceCollectionWithSelector> { +function get>(key: TKey): Promise { // When we already have the value in cache - resolve right away if (cache.hasCacheForKey(key)) { - return Promise.resolve(cache.get(key)); + return Promise.resolve(cache.get(key) as TValue); } const taskName = `get:${key}`; // When a value retrieving task for this key is still running hook to it if (cache.hasPendingTask(taskName)) { - return cache.getTaskPromise(taskName) as Promise>; + return cache.getTaskPromise(taskName) as Promise; } // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages @@ -231,7 +232,7 @@ function get(key: OnyxKey): Promise> { }) .catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`)); - return cache.captureTask(taskName, promise); + return cache.captureTask(taskName, promise) as Promise; } /** Returns current key names stored in persisted storage */ @@ -1007,8 +1008,8 @@ function hasPendingMergeForKey(key: OnyxKey): boolean { return !!mergeQueue[key]; } -type RemoveNullValuesOutput | null> = { - value: Value | null; +type RemoveNullValuesOutput | undefined> = { + value: Value; wasRemoved: boolean; }; @@ -1019,10 +1020,14 @@ type RemoveNullValuesOutput | null> = { * * @returns The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely */ -function removeNullValues>(key: OnyxKey, value: Value | null, shouldRemoveNestedNulls = true): RemoveNullValuesOutput { +function removeNullValues | undefined>(key: OnyxKey, value: Value, shouldRemoveNestedNulls = true): RemoveNullValuesOutput { if (value === null) { remove(key); - return {value: null, wasRemoved: true}; + return {value, wasRemoved: true}; + } + + if (value === undefined) { + return {value, wasRemoved: false}; } // We can remove all null values in an object by merging it with itself @@ -1038,11 +1043,11 @@ function removeNullValues>(key: OnyxKey, value: * @return an array of key - value pairs <[key, value]> */ -function prepareKeyValuePairsForStorage(data: Record>, shouldRemoveNestedNulls: boolean): Array<[OnyxKey, OnyxValue]> { - return Object.entries(data).reduce]>>((pairs, [key, value]) => { +function prepareKeyValuePairsForStorage(data: Record>, shouldRemoveNestedNulls: boolean): Array<[OnyxKey, OnyxInput]> { + return Object.entries(data).reduce]>>((pairs, [key, value]) => { const {value: valueAfterRemoving, wasRemoved} = removeNullValues(key, value, shouldRemoveNestedNulls); - if (!wasRemoved) { + if (!wasRemoved && valueAfterRemoving !== undefined) { pairs.push([key, valueAfterRemoving]); } @@ -1055,7 +1060,7 @@ function prepareKeyValuePairsForStorage(data: Record * * @param changes Array of changes that should be applied to the existing value */ -function applyMerge(existingValue: OnyxValue, changes: Array>, shouldRemoveNestedNulls: boolean): OnyxValue { +function applyMerge | undefined>(existingValue: TValue, changes: TValue[], shouldRemoveNestedNulls: boolean): TValue { const lastChange = changes?.at(-1); if (Array.isArray(lastChange)) { @@ -1064,15 +1069,12 @@ function applyMerge(existingValue: OnyxValue, changes: Array change && typeof change === 'object')) { // Object values are then merged one after the other - return changes.reduce( - (modifiedData, change) => utils.fastMerge(modifiedData as Record, change as Record, shouldRemoveNestedNulls), - existingValue || {}, - ); + return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNestedNulls), (existingValue || {}) as TValue); } // If we have anything else we can't merge it so we'll // simply return the last value that was queued - return lastChange; + return lastChange as TValue; } /** diff --git a/lib/types.ts b/lib/types.ts index f1635026..2f8dd723 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -112,33 +112,6 @@ type CollectionKey = `${CollectionKeyBase}${string}`; */ type OnyxKey = Key | CollectionKey; -/** - * Represents a Onyx value that can be either a single entry or a collection of entries, depending on the `TKey` provided. - */ -type OnyxValue = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; - -/** - * Represents a mapping of Onyx keys to values, where keys are either normal or collection Onyx keys - * and values are the corresponding values in Onyx's state. - * - * For collection keys, `KeyValueMapping` allows any string to be appended - * to the key (e.g., 'report_some-id', 'download_some-id'). - * - * The mapping is derived from the `values` property of the `TypeOptions` type. - */ -type KeyValueMapping = { - [TKey in keyof TypeOptions['values'] as TKey extends CollectionKeyBase ? `${TKey}${string}` : TKey]: TypeOptions['values'][TKey]; -}; - -/** - * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. - * - * It's very similar to `KeyValueMapping` but this type accepts using `null` as well. - */ -type NullableKeyValueMapping = { - [TKey in OnyxKey]: NonUndefined> | null; -}; - /** * Represents a selector function type which operates based on the provided `TKey` and `ReturnType`. * @@ -185,7 +158,8 @@ type OnyxEntry = TOnyxValue | undefined; * Setting a key to `null` will remove the key from the store. * `undefined` is not allowed for setting values, because it will have no effect on the data. */ -type OnyxInput = TOnyxValue | null; +// type OnyxInput = TOnyxValue | null; +type OnyxInput = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollection : NullishDeep | null; /** * Represents an Onyx collection of entries, that can be either a record of `TOnyxValue`s or `null` / `undefined` if it is empty or doesn't exist. @@ -217,6 +191,24 @@ type OnyxInput = TOnyxValue | null; */ type OnyxCollection = OnyxEntry>; +/** + * Represents a mapping of Onyx keys to values, where keys are either normal or collection Onyx keys + * and values are the corresponding values in Onyx's state. + * + * For collection keys, `KeyValueMapping` allows any string to be appended + * to the key (e.g., 'report_some-id', 'download_some-id'). + * + * The mapping is derived from the `values` property of the `TypeOptions` type. + */ +type KeyValueMapping = { + [TKey in keyof TypeOptions['values'] as TKey extends CollectionKeyBase ? `${TKey}${string}` : TKey]: TypeOptions['values'][TKey]; +}; + +/** + * Represents a Onyx value that can be either a single entry or a collection of entries, depending on the `TKey` provided. + */ +type OnyxValue = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; + /** Utility type to extract `TOnyxValue` from `OnyxCollection` */ type ExtractOnyxCollectionValue = TOnyxCollection extends NonNullable> ? U : never; @@ -328,25 +320,35 @@ type Mapping = ConnectOptions & { connectionID: number; }; +/** + * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. + * + * It's very similar to `KeyValueMapping` but this type is used for inputs to Onyx + * (set, merge, mergeCollection) and therefore accepts using `null` as well. + */ +type OnyxInputKeyValueMapping = { + [TKey in OnyxKey]: OnyxInput; +}; + /** * This represents the value that can be passed to `Onyx.set` and to `Onyx.update` with the method "SET" */ -type OnyxSetInput = OnyxInput; +type OnyxSetInput = OnyxInput; /** * This represents the value that can be passed to `Onyx.multiSet` and to `Onyx.update` with the method "MULTI_SET" */ -type OnyxMultiSetInput = Partial; +type OnyxMultiSetInput = Partial; /** * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" */ -type OnyxMergeInput = OnyxInput>; +type OnyxMergeInput = OnyxInput; /** * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" */ -type OnyxMergeCollectionInput = Collection, TMap>; +type OnyxMergeCollectionInput = Collection>, TMap>; /** * Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of @@ -392,7 +394,7 @@ type InitOptions = { keys?: DeepRecord; /** initial data to set when `init()` and `clear()` is called */ - initialKeyStates?: Partial; + initialKeyStates?: Partial; /** * This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged @@ -440,7 +442,7 @@ export type { Mapping, NonNull, NonUndefined, - NullableKeyValueMapping, + OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, diff --git a/lib/utils.ts b/lib/utils.ts index 15faa4ac..76bd46dc 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ -import type {OnyxKey, OnyxValue} from './types'; +import type {OnyxInput, OnyxKey} from './types'; type EmptyObject = Record; type EmptyValue = EmptyObject | null | undefined; @@ -27,19 +27,21 @@ function isMergeableObject(value: unknown): value is Record { * @param shouldRemoveNestedNulls - If true, null object values will be removed. * @returns - The merged object. */ -function mergeObject>(target: TObject | null, source: TObject, shouldRemoveNestedNulls = true): TObject { +function mergeObject>(target: TObject | unknown | null | undefined, source: TObject, shouldRemoveNestedNulls = true): TObject { const destination: Record = {}; + const targetObject = isMergeableObject(target) ? target : undefined; + // First we want to copy over all keys from the target into the destination object, // in case "target" is a mergable object. // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object // and therefore we need to omit keys where either the source or target value is null. - if (isMergeableObject(target)) { - const targetKeys = Object.keys(target); + if (targetObject) { + const targetKeys = Object.keys(targetObject); for (let i = 0; i < targetKeys.length; ++i) { const key = targetKeys[i]; const sourceValue = source?.[key]; - const targetValue = target?.[key]; + const targetValue = targetObject?.[key]; // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object. // Therefore, if either target or source value is null, we want to prevent the key from being set. @@ -57,7 +59,7 @@ function mergeObject>(target: TObject | for (let i = 0; i < sourceKeys.length; ++i) { const key = sourceKeys[i]; const sourceValue = source?.[key]; - const targetValue = target?.[key]; + const targetValue = targetObject?.[key]; // If undefined is passed as the source value for a key, we want to generally ignore it. // If "shouldRemoveNestedNulls" is set to true and the source value is null, @@ -92,7 +94,7 @@ function mergeObject>(target: TObject | * On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values. * To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations. */ -function fastMerge>(target: TObject | null, source: TObject | null, shouldRemoveNestedNulls = true): TObject | null { +function fastMerge(target: TValue, source: TValue, shouldRemoveNestedNulls = true): TValue { // We have to ignore arrays and nullish values here, // otherwise "mergeObject" will throw an error, // because it expects an object as "source" @@ -100,14 +102,14 @@ function fastMerge>(target: TObject | nu return source; } - return mergeObject(target, source, shouldRemoveNestedNulls); + return mergeObject(target, source as Record, shouldRemoveNestedNulls) as TValue; } /** Deep removes the nested null values from the given value. */ -function removeNestedNullValues | unknown | unknown[]>(value: TValue | null): TValue | null { +function removeNestedNullValues | null>(value: TValue): TValue { if (typeof value === 'object' && !Array.isArray(value)) { - const objectValue = value as Record | null; - return fastMerge(objectValue, objectValue) as TValue | null; + const objectValue = value as Record; + return fastMerge(objectValue, objectValue) as TValue; } return value; From b680541297cfaa6ed980adfccdde3dc5e8d5f053 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 17:10:19 +0200 Subject: [PATCH 61/96] fix: onyx connect callback might return undefined --- lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types.ts b/lib/types.ts index 2f8dd723..8cfb55c9 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -283,7 +283,7 @@ type WithOnyxConnectOptions = { canEvict?: boolean; }; -type DefaultConnectCallback = (value: NonUndefined>, key: TKey) => void; +type DefaultConnectCallback = (value: OnyxEntry, key: TKey) => void; type CollectionConnectCallback = (value: NonUndefined>) => void; From 540480f2e33e11164187870a424df78ed70498b7 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 17:30:41 +0200 Subject: [PATCH 62/96] fix: input types --- lib/types.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 8cfb55c9..d251c1b8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -124,7 +124,7 @@ type OnyxKey = Key | CollectionKey; type Selector = (value: OnyxEntry, state?: WithOnyxState) => TReturnType; /** - * Represents a single Onyx entry, that can be either `TOnyxValue` or `null` / `undefined` if it doesn't exist. + * Represents a single Onyx entry, that can be either `TOnyxValue` or `undefined` if it doesn't exist. * * It can be used to specify data retrieved from Onyx e.g. `withOnyx` HOC mappings. * @@ -154,15 +154,7 @@ type Selector = (value: OnyxEntry type OnyxEntry = TOnyxValue | undefined; /** - * Represents an input value that can be passed to Onyx methods, that can be either `TOnyxValue` or `null`. - * Setting a key to `null` will remove the key from the store. - * `undefined` is not allowed for setting values, because it will have no effect on the data. - */ -// type OnyxInput = TOnyxValue | null; -type OnyxInput = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollection : NullishDeep | null; - -/** - * Represents an Onyx collection of entries, that can be either a record of `TOnyxValue`s or `null` / `undefined` if it is empty or doesn't exist. + * Represents an Onyx collection of entries, that can be either a record of `TOnyxValue`s or `undefined` if it is empty or doesn't exist. * * It can be used to specify collection data retrieved from Onyx e.g. `withOnyx` HOC mappings. * @@ -320,11 +312,83 @@ type Mapping = ConnectOptions & { connectionID: number; }; +/** + * Represents a single Onyx input value, that can be either `TOnyxValue` or `null` if the key should be deleted. + * + * It can be used to specify data retrieved from Onyx e.g. `withOnyx` HOC mappings. + * + * @example + * ```ts + * import Onyx, {OnyxEntry, withOnyx} from 'react-native-onyx'; + * + * type OnyxProps = { + * userAccount: OnyxEntry; + * }; + * + * type Props = OnyxProps & { + * prop1: string; + * }; + * + * function Component({prop1, userAccount}: Props) { + * // ... + * } + * + * export default withOnyx({ + * userAccount: { + * key: ONYXKEYS.ACCOUNT, + * }, + * })(Component); + * ``` + */ +type OnyxInputValue = TOnyxValue | null; + +/** + * Represents an Onyx collection of inputs, that can be either a record of `TOnyxValue`s or `null` if the key should be deleted. + * + * It can be used to specify collection data retrieved from Onyx e.g. `withOnyx` HOC mappings. + * + * @example + * ```ts + * import Onyx, {OnyxCollection, withOnyx} from 'react-native-onyx'; + * + * type OnyxProps = { + * reports: OnyxCollection; + * }; + * + * type Props = OnyxProps & { + * prop1: string; + * }; + * + * function Component({prop1, reports}: Props) { + * // ... + * } + * + * export default withOnyx({ + * reports: { + * key: ONYXKEYS.COLLECTION.REPORT, + * }, + * })(Component); + * ``` + */ +type OnyxCollectionInput = OnyxInputValue>; + +/** + * Represents an input value that can be passed to Onyx methods, that can be either `TOnyxValue` or `null`. + * Setting a key to `null` will remove the key from the store. + * `undefined` is not allowed for setting values, because it will have no effect on the data. + */ +// type OnyxInput = TOnyxValue | null; +type OnyxInput = string extends TKey + ? unknown + : TKey extends CollectionKeyBase + ? OnyxCollectionInput + : OnyxInputValue>; + /** * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. * * It's very similar to `KeyValueMapping` but this type is used for inputs to Onyx - * (set, merge, mergeCollection) and therefore accepts using `null` as well. + * (set, merge, mergeCollection) and therefore accepts using `null` to remove a key from Onyx. */ type OnyxInputKeyValueMapping = { [TKey in OnyxKey]: OnyxInput; @@ -446,8 +510,10 @@ export type { NullishDeep, OnyxCollection, OnyxEntry, - OnyxInput, OnyxKey, + OnyxInputValue, + OnyxCollectionInput, + OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, From b0d25058249977eceb4b4fcfa08a96566d242a55 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 17:34:01 +0200 Subject: [PATCH 63/96] fix: export types --- lib/OnyxUtils.ts | 10 +++++++--- lib/index.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index a1cf51fb..cb8df704 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1060,7 +1060,11 @@ function prepareKeyValuePairsForStorage(data: Record * * @param changes Array of changes that should be applied to the existing value */ -function applyMerge | undefined>(existingValue: TValue, changes: TValue[], shouldRemoveNestedNulls: boolean): TValue { +function applyMerge | undefined, TChange extends OnyxInput | undefined>( + existingValue: TValue, + changes: TChange[], + shouldRemoveNestedNulls: boolean, +): TChange { const lastChange = changes?.at(-1); if (Array.isArray(lastChange)) { @@ -1069,12 +1073,12 @@ function applyMerge | undefined>(existingValue if (changes.some((change) => change && typeof change === 'object')) { // Object values are then merged one after the other - return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNestedNulls), (existingValue || {}) as TValue); + return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNestedNulls), (existingValue || {}) as TChange); } // If we have anything else we can't merge it so we'll // simply return the last value that was queued - return lastChange as TValue; + return lastChange as TChange; } /** diff --git a/lib/index.ts b/lib/index.ts index 3352d39e..a4e3cc5f 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -6,10 +6,12 @@ import type { NullishDeep, OnyxCollection, OnyxEntry, - OnyxInput, OnyxKey, OnyxValue, Selector, + OnyxInputValue, + OnyxCollectionInput, + OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, @@ -30,8 +32,10 @@ export type { NullishDeep, OnyxCollection, OnyxEntry, - OnyxInput, OnyxKey, + OnyxInputValue, + OnyxCollectionInput, + OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, From d44df567c955712126183cb208478e8390d9f86e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 18:01:40 +0200 Subject: [PATCH 64/96] fix: input type --- lib/index.ts | 2 -- lib/types.ts | 37 +------------------------------------ 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index a4e3cc5f..bf08c29d 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,7 +10,6 @@ import type { OnyxValue, Selector, OnyxInputValue, - OnyxCollectionInput, OnyxInput, OnyxSetInput, OnyxMultiSetInput, @@ -34,7 +33,6 @@ export type { OnyxEntry, OnyxKey, OnyxInputValue, - OnyxCollectionInput, OnyxInput, OnyxSetInput, OnyxMultiSetInput, diff --git a/lib/types.ts b/lib/types.ts index d251c1b8..f193336d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -342,47 +342,13 @@ type Mapping = ConnectOptions & { */ type OnyxInputValue = TOnyxValue | null; -/** - * Represents an Onyx collection of inputs, that can be either a record of `TOnyxValue`s or `null` if the key should be deleted. - * - * It can be used to specify collection data retrieved from Onyx e.g. `withOnyx` HOC mappings. - * - * @example - * ```ts - * import Onyx, {OnyxCollection, withOnyx} from 'react-native-onyx'; - * - * type OnyxProps = { - * reports: OnyxCollection; - * }; - * - * type Props = OnyxProps & { - * prop1: string; - * }; - * - * function Component({prop1, reports}: Props) { - * // ... - * } - * - * export default withOnyx({ - * reports: { - * key: ONYXKEYS.COLLECTION.REPORT, - * }, - * })(Component); - * ``` - */ -type OnyxCollectionInput = OnyxInputValue>; - /** * Represents an input value that can be passed to Onyx methods, that can be either `TOnyxValue` or `null`. * Setting a key to `null` will remove the key from the store. * `undefined` is not allowed for setting values, because it will have no effect on the data. */ // type OnyxInput = TOnyxValue | null; -type OnyxInput = string extends TKey - ? unknown - : TKey extends CollectionKeyBase - ? OnyxCollectionInput - : OnyxInputValue>; +type OnyxInput = OnyxInputValue>; /** * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. @@ -512,7 +478,6 @@ export type { OnyxEntry, OnyxKey, OnyxInputValue, - OnyxCollectionInput, OnyxInput, OnyxSetInput, OnyxMultiSetInput, From cdf0c7850400b0b0134827b6dc0ef2825bc391a8 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Jun 2024 18:37:17 +0200 Subject: [PATCH 65/96] fix: check for equality --- lib/OnyxUtils.ts | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index cb8df704..13787161 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -546,14 +546,15 @@ function keysChanged( } subscriber.withOnyxInstance.setStateProxy((prevState) => { - const finalCollection = lodashClone(prevState?.[subscriber.statePropertyName] ?? {}); + const prevCollection = prevState?.[subscriber.statePropertyName] ?? {}; + const finalCollection = lodashClone(prevCollection); const dataKeys = Object.keys(partialCollection ?? {}); for (let j = 0; j < dataKeys.length; j++) { const dataKey = dataKeys[j]; finalCollection[dataKey] = cachedCollection[dataKey]; } - if (!prevState.loading && deepEqual(cachedCollection, finalCollection)) { + if (deepEqual(prevCollection, finalCollection)) { return null; } @@ -600,20 +601,21 @@ function keysChanged( } subscriber.withOnyxInstance.setStateProxy((prevState) => { - const data = cachedCollection[subscriber.key]; - const previousData = prevState[subscriber.statePropertyName]; + const prevData = prevState[subscriber.statePropertyName]; + const newData = cachedCollection[subscriber.key]; // Avoids triggering unnecessary re-renders when feeding empty objects - if (utils.isEmptyObject(data) && utils.isEmptyObject(previousData)) { + if (utils.isEmptyObject(newData) && utils.isEmptyObject(prevData)) { return null; } - if (data === previousData) { + + if (deepEqual(prevData, newData)) { return null; } - PerformanceUtils.logSetStateCall(subscriber, previousData, data, 'keysChanged', collectionKey); + PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keysChanged', collectionKey); return { - [subscriber.statePropertyName]: data, + [subscriber.statePropertyName]: newData, }; }); } @@ -705,12 +707,17 @@ function keyChanged( } subscriber.withOnyxInstance.setStateProxy((prevState) => { - const collection = prevState[subscriber.statePropertyName] || {}; + const prevCollection = prevState[subscriber.statePropertyName] || {}; const newCollection = { - ...collection, + ...prevCollection, [key]: value, }; - PerformanceUtils.logSetStateCall(subscriber, collection, newCollection, 'keyChanged', key); + + if (deepEqual(prevCollection, newCollection)) { + return null; + } + + PerformanceUtils.logSetStateCall(subscriber, prevCollection, newCollection, 'keyChanged', key); return { [subscriber.statePropertyName]: newCollection, }; @@ -721,11 +728,11 @@ function keyChanged( // If the subscriber has a selector, then the component's state must only be updated with the data // returned by the selector and only if the selected data has changed. if (selector) { - subscriber.withOnyxInstance.setStateProxy((prevState) => { + subscriber.withOnyxInstance.setStateProxy(() => { const prevValue = selector(previousValue, subscriber.withOnyxInstance.state); const newValue = selector(value, subscriber.withOnyxInstance.state); - if (!prevState.loading && deepEqual(prevValue, newValue)) { + if (deepEqual(prevValue, newValue)) { return null; } From 3a8312ef1a2ac6ebd500f420b485b6098f008e41 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:24:59 -0700 Subject: [PATCH 66/96] adjustment 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b326d1f..e19a4f50 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Awesome persistent storage solution wrapped in a Pub/Sub library. - Onyx allows other code to subscribe to changes in data, and then publishes change events whenever data is changed - Anything needing to read Onyx data needs to: 1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage) - 2. Subscribe to changes of the data for a particular key or set of keys. React functional components use the `useOnyx()` hook (recommended), class components use `withOnyx()` HOC (deprecated, not-recommended) and non-React libs use `Onyx.connect()`. + 2. Subscribe to changes of the data for a particular key or set of keys. React function components use the `useOnyx()` hook (recommended), both class and function components can use `withOnyx()` HOC (deprecated, not-recommended) and non-React libs use `Onyx.connect()`. 3. Get initialized with the current value of that key from persistent storage (Onyx does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process) - Subscribing to Onyx keys is done using a constant defined in `ONYXKEYS`. Each Onyx key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (e.g. display a list of them in the nav menu), then it would subscribe to the key `ONYXKEYS.COLLECTION.REPORT`. From 6adcb937dda26f151889ae363597662d27271883 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:25:23 -0700 Subject: [PATCH 67/96] adjustment 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e19a4f50..c0aee333 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ To teardown the subscription call `Onyx.disconnect()` with the `connectionID` re Onyx.disconnect(connectionID); ``` -We can also access values inside React functional components via the `useOnyx()` [hook](https://react.dev/reference/react/hooks) (recommended) or class components via the `withOnyx()` [higher order component](https://reactjs.org/docs/higher-order-components.html) (deprecated, not-recommended). When the data changes the component will re-render. +We can also access values inside React function components via the `useOnyx()` [hook](https://react.dev/reference/react/hooks) (recommended) or class and function components via the `withOnyx()` [higher order component](https://reactjs.org/docs/higher-order-components.html) (deprecated, not-recommended). When the data changes the component will re-render. ```javascript import React from 'react'; From 7ba5b0654e5c334656468d1f5c2071e205f1b49b Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:37:55 -0700 Subject: [PATCH 68/96] adjustment 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c0aee333..5e594100 100644 --- a/README.md +++ b/README.md @@ -135,11 +135,18 @@ const App = () => { export default App; ``` -While `Onyx.connect()` gives you more control on how your component reacts as data is fetched from disk, `useOnyx()` will delay the rendering of the wrapped component until all keys/entities have been fetched and passed to the component, this can be convenient for simple cases. This however, can really delay your application if many entities are connected to the same component, you can pass an `initialValue` to each key to allow Onyx to eagerly render your component with this value. +The `useOnyx()` hook won't delay the rendering of the component using it while the key/entity is being fetched and passed to the component. However, you can simulate this behavior by checking if the `status` of the hook's result metadata is `loading`. When `status` is `loading` it means that the Onyx data is being loaded into cache and thus is not immediately available, while `loaded` means that the data is already loaded and available to be consumed. -```javascript -const [session] = useOnyx('session', {initialValue: {}}); -``` +\```javascript +const [reports, reportsResult] = useOnyx(ONYXKEYS.COLLECTION.REPORT); +const [session, sessionResult] = useOnyx(ONYXKEYS.SESSION); + +if (reportsResult.status === 'loading' || sessionResult.status === 'loading') { + return ; // or `null` if you don't want to render anything. +} + +// rest of the component's code. +\``` > **Deprecated Note**: Please note, `withOnyx()` Higher Order Component (HOC) is now considered deprecated. Use `useOnyx()` hook instead. From aee9495cbed33545b2420352ee6405bb73f19ea0 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:38:20 -0700 Subject: [PATCH 69/96] adjustment 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e594100..3b7a53e9 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ export default App; 1. The component mounts with a `reportID={1234}` prop. 2. The `useOnyx` hook evaluates the mapping and subscribes to the key `reports_1234` using the `reportID` prop. 3. The `useOnyx` hook fetches the data for the key `reports_1234` from Onyx and sets the state with the initial value (if provided). -4. Since `policyID` is not defined yet, it defaults to `undefined`. The `useOnyx` hook subscribes to the key `policies_undefined`. +4. Since `report` is not defined yet, `report?.policyID` defaults to `undefined`. The `useOnyx` hook subscribes to the key `policies_undefined`. 5. The `useOnyx` hook reads the data and updates the state of the component: - `report={{reportID: 1234, policyID: 1, ...rest of the object...}}` - `policy={undefined}` (since there is no policy with ID `undefined`) 6. As there is still an `undefined` key, the `useOnyx` hook again evaluates the key `policies_1` after fetching the updated `report` object which has `policyID: 1`. From fe656d5da4a39403a5bd75d2e64649ae7caf9715 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:38:31 -0700 Subject: [PATCH 70/96] adjustment 5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b7a53e9..568f2699 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,8 @@ export default App; 3. The `useOnyx` hook fetches the data for the key `reports_1234` from Onyx and sets the state with the initial value (if provided). 4. Since `report` is not defined yet, `report?.policyID` defaults to `undefined`. The `useOnyx` hook subscribes to the key `policies_undefined`. 5. The `useOnyx` hook reads the data and updates the state of the component: - - `report={{reportID: 1234, policyID: 1, ...rest of the object...}}` - `policy={undefined}` (since there is no policy with ID `undefined`) + - `report={{reportID: 1234, policyID: 1, ...rest of the object...}}` + - `policy={undefined}` (since there is no policy with ID `undefined`) 6. As there is still an `undefined` key, the `useOnyx` hook again evaluates the key `policies_1` after fetching the updated `report` object which has `policyID: 1`. 7. The `useOnyx` hook reads the data and updates the state with: - `policy={{policyID: 1, ...rest of the object...}}` From c80984b50ab688d5f6ccc458edc699cf256903bd Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:38:41 -0700 Subject: [PATCH 71/96] adjustment 6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 568f2699..566378d1 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ export default App; 5. The `useOnyx` hook reads the data and updates the state of the component: - `report={{reportID: 1234, policyID: 1, ...rest of the object...}}` - `policy={undefined}` (since there is no policy with ID `undefined`) -6. As there is still an `undefined` key, the `useOnyx` hook again evaluates the key `policies_1` after fetching the updated `report` object which has `policyID: 1`. +6. The `useOnyx` hook again evaluates the key `policies_1` after fetching the updated `report` object which has `policyID: 1`. 7. The `useOnyx` hook reads the data and updates the state with: - `policy={{policyID: 1, ...rest of the object...}}` 8. Now, all mappings have values that are defined (not undefined), and the component is rendered with all necessary data. From 708b7ffaab03936d462d381559aa26f8280c30c7 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:38:54 -0700 Subject: [PATCH 72/96] adjustment 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 566378d1..220bf1f4 100644 --- a/README.md +++ b/README.md @@ -407,9 +407,9 @@ Onyx.init({ ``` ```js -const ReportActionsView = ({isActiveReport}) => { +const ReportActionsView = ({reportID, isActiveReport}) => { const [reportActions] = useOnyx( - ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, {canEvict: () => !isActiveReport} ); From 2d62063f9ddc3c388b91d7e9f274d99a4963156c Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 4 Jun 2024 14:26:47 -0700 Subject: [PATCH 73/96] final adjustment --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index 220bf1f4..b99d54ab 100644 --- a/README.md +++ b/README.md @@ -201,32 +201,6 @@ Some components need to subscribe to multiple Onyx keys at once and sometimes, o Example: To get the policy of a report, the `policy` key depends on the `report` key. -```javascript -import React from 'react'; -import {useOnyx} from 'react-native-onyx'; -const ONYXKEYS = { - REPORT: 'report_1234', - POLICY: 'policy_' -}; - -const App = () => { - const [report] = useOnyx(ONYXKEYS.REPORT); - const [policy] = useOnyx(`${ONYXKEYS.POLICY}${report.policyID}`); - - return ( - - {/* Render with policy data */} - - ); -}; - -export default App; -``` - -Background info: -- The `key` value can be a function that returns the key that Onyx subscribes to -- The first argument to the `key` function is the `props` from the component - ```javascript const App = ({reportID}) => { const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); From 4c29663f125374ff788b98dd66f179a3a9e87747 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 4 Jun 2024 14:31:45 -0700 Subject: [PATCH 74/96] final adjustment 2.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b99d54ab..36b087e1 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ export default withOnyx({ })(App); ``` -While `Onyx.connect()` gives you more control on how your component reacts as data is fetched from disk, `withOnyx()` will delay the rendering of the wrapped component until all keys/entities have been fetched and passed to the component, this can be convenient for simple cases. This however, can really delay your application if many entities are connected to the same component, you can pass an `initialValue` to each key to allow Onyx to eagerly render your component with this value. +Differently from `useOnyx()`, `withOnyx()` will delay the rendering of the wrapped component until all keys/entities have been fetched and passed to the component, this can be convenient for simple cases. This however, can really delay your application if many entities are connected to the same component, you can pass an `initialValue` to each key to allow Onyx to eagerly render your component with this value. ```javascript export default withOnyx({ From 3700231adb114e0a911e9b8e7ebf6a47c55c9986 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 4 Jun 2024 14:36:25 -0700 Subject: [PATCH 75/96] fixed code block formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36b087e1..cca2d8fc 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ export default App; The `useOnyx()` hook won't delay the rendering of the component using it while the key/entity is being fetched and passed to the component. However, you can simulate this behavior by checking if the `status` of the hook's result metadata is `loading`. When `status` is `loading` it means that the Onyx data is being loaded into cache and thus is not immediately available, while `loaded` means that the data is already loaded and available to be consumed. -\```javascript +```javascript const [reports, reportsResult] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [session, sessionResult] = useOnyx(ONYXKEYS.SESSION); @@ -146,7 +146,7 @@ if (reportsResult.status === 'loading' || sessionResult.status === 'loading') { } // rest of the component's code. -\``` +``` > **Deprecated Note**: Please note, `withOnyx()` Higher Order Component (HOC) is now considered deprecated. Use `useOnyx()` hook instead. From f19ac5d5b1e9c4837477b3aaedaf0e4400c46c84 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 4 Jun 2024 14:39:40 -0700 Subject: [PATCH 76/96] added deprecation warning note --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cca2d8fc..3bc9338c 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,9 @@ if (reportsResult.status === 'loading' || sessionResult.status === 'loading') { // rest of the component's code. ``` -> **Deprecated Note**: Please note, `withOnyx()` Higher Order Component (HOC) is now considered deprecated. Use `useOnyx()` hook instead. +> [!warning] +> ## Deprecated Note +> Please note that the `withOnyx()` Higher Order Component (HOC) is now considered deprecated. Use `useOnyx()` hook instead. ```javascript import React from 'react'; From 4e94f2091a669d0d5517e448dca4d1ed004833e5 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 4 Jun 2024 14:42:32 -0700 Subject: [PATCH 77/96] added (long) paragraph spacing for better readability --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bc9338c..f8291991 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,9 @@ export default withOnyx({ })(App); ``` -Additionally, if your component has many keys/entities when your component will mount but will receive many updates as data is fetched from DB and passed down to it, as every key that gets fetched will trigger a `setState` on the `withOnyx` HOC. This might cause re-renders on the initial mounting, preventing the component from mounting/rendering in reasonable time, making your app feel slow and even delaying animations. You can workaround this by passing an additional object with the `shouldDelayUpdates` property set to true. Onyx will then put all the updates in a queue until you decide when then should be applied, the component will receive a function `markReadyForHydration`. A good place to call this function is on the `onLayout` method, which gets triggered after your component has been rendered. +Additionally, if your component has many keys/entities when your component will mount but will receive many updates as data is fetched from DB and passed down to it, as every key that gets fetched will trigger a `setState` on the `withOnyx` HOC. This might cause re-renders on the initial mounting, preventing the component from mounting/rendering in reasonable time, making your app feel slow and even delaying animations. + +You can workaround this by passing an additional object with the `shouldDelayUpdates` property set to true. Onyx will then put all the updates in a queue until you decide when then should be applied, the component will receive a function `markReadyForHydration`. A good place to call this function is on the `onLayout` method, which gets triggered after your component has been rendered. ```javascript const App = ({session, markReadyForHydration}) => ( From c8dd37593b4e3a9bef84bb4875ba5e9714bcb3e4 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 09:47:20 +0200 Subject: [PATCH 78/96] fix: removing items won't return undefined --- lib/OnyxUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 13787161..2e8fbda6 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -945,7 +945,7 @@ function scheduleNotifyCollectionSubscribers( function remove(key: TKey): Promise { const prevValue = cache.get(key, false) as OnyxValue; cache.drop(key); - scheduleSubscriberUpdate(key, null as OnyxValue, prevValue); + scheduleSubscriberUpdate(key, undefined as OnyxValue, prevValue); return Storage.removeItem(key).then(() => undefined); } From 1095fa469e0a620cbf3f314ba81749c0184a5037 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 09:53:15 +0200 Subject: [PATCH 79/96] check for undefined values in fastMerge --- lib/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/utils.ts b/lib/utils.ts index 76bd46dc..502b4da7 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -45,7 +45,10 @@ function mergeObject>(target: TObject | // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object. // Therefore, if either target or source value is null, we want to prevent the key from being set. - const isSourceOrTargetNull = targetValue === null || sourceValue === null; + // targetValue should techincally never be "undefined", because it will always be a value from cache or storage + // and we never set "undefined" there. Still, if there targetValue is undefined we don't want to set + // the key explicitly to prevent loose undefined values in objects in cache and storage. + const isSourceOrTargetNull = targetValue === undefined || targetValue === null || sourceValue === null; const shouldOmitTargetKey = shouldRemoveNestedNulls && isSourceOrTargetNull; if (!shouldOmitTargetKey) { From da1bd7e200605cc4d5e6dabeda35f49a44c00413 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 09:53:26 +0200 Subject: [PATCH 80/96] add tests regarding undefined handling --- tests/unit/onyxTest.ts | 117 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 95c189be..756c4de7 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -298,6 +298,7 @@ describe('Onyx', () => { }, }, }); + return Onyx.merge(ONYX_KEYS.TEST_KEY, { test1: { test3: { @@ -313,6 +314,7 @@ describe('Onyx', () => { test3: {}, }, }); + return Onyx.merge(ONYX_KEYS.TEST_KEY, { test1: { test3: null, @@ -321,6 +323,7 @@ describe('Onyx', () => { }) .then(() => { expect(testKeyValue).toEqual({test1: {test2: 'test2'}}); + return Onyx.merge(ONYX_KEYS.TEST_KEY, {test1: null}); }) .then(() => { @@ -328,7 +331,114 @@ describe('Onyx', () => { }); }); - it('should ignore `undefined` values when merging', () => { + it('should ignore top-level and remove nested `undefined` values in Onyx.set', () => { + let testKeyValue: unknown; + + connectionID = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + return Onyx.set(ONYX_KEYS.TEST_KEY, { + test1: { + test2: 'test2', + test3: 'test3', + }, + }) + .then(() => { + expect(testKeyValue).toEqual({ + test1: { + test2: 'test2', + test3: 'test3', + }, + }); + + return Onyx.set(ONYX_KEYS.TEST_KEY, { + test1: { + test2: undefined, + test3: 'test3', + }, + }); + }) + .then(() => { + expect(testKeyValue).toEqual({test1: {test3: 'test3'}}); + + return Onyx.set(ONYX_KEYS.TEST_KEY, {test1: undefined}); + }) + .then(() => { + expect(testKeyValue).toEqual({}); + + return Onyx.set(ONYX_KEYS.TEST_KEY, undefined); + }) + .then(() => { + expect(testKeyValue).toEqual({}); + + return Onyx.set(ONYX_KEYS.TEST_KEY, {test1: undefined}); + }); + }); + + it('should ignore top-level and remove nested `undefined` values in Onyx.multiSet', () => { + let testKeyValue: unknown; + connectionID = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + let otherTestKeyValue: unknown; + connectionID = Onyx.connect({ + key: ONYX_KEYS.OTHER_TEST, + initWithStoredValues: false, + callback: (value) => { + otherTestKeyValue = value; + }, + }); + + return Onyx.multiSet({ + [ONYX_KEYS.TEST_KEY]: { + test1: 'test1', + test2: 'test2', + }, + [ONYX_KEYS.OTHER_TEST]: 'otherTest', + }) + .then(() => { + expect(testKeyValue).toEqual({ + test1: 'test1', + test2: 'test2', + }); + expect(otherTestKeyValue).toEqual('otherTest'); + + return Onyx.multiSet({ + [ONYX_KEYS.TEST_KEY]: { + test1: 'test1', + test2: undefined, + }, + [ONYX_KEYS.OTHER_TEST]: undefined, + }); + }) + .then(() => { + expect(testKeyValue).toEqual({ + test1: 'test1', + }); + expect(otherTestKeyValue).toEqual('otherTest'); + + return Onyx.multiSet({ + [ONYX_KEYS.TEST_KEY]: null, + [ONYX_KEYS.OTHER_TEST]: null, + }); + }) + .then(() => { + expect(testKeyValue).toEqual(undefined); + expect(otherTestKeyValue).toEqual(undefined); + }); + }); + + it('should ignore top-level and remove nested `undefined` values in Onyx.merge', () => { let testKeyValue: unknown; connectionID = Onyx.connect({ @@ -364,6 +474,11 @@ describe('Onyx', () => { }) .then(() => { expect(testKeyValue).toEqual({test1: {test2: 'test2', test3: 'test3'}}); + return Onyx.merge(ONYX_KEYS.TEST_KEY, undefined); + }) + .then(() => { + expect(testKeyValue).toEqual({test1: {test2: 'test2', test3: 'test3'}}); + return Onyx.merge(ONYX_KEYS.TEST_KEY, undefined); }); }); From ae64bfcf2621840aded89191c1dbaeeb84d6f8fe Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 10:02:03 +0200 Subject: [PATCH 81/96] add test for mergeCollection --- tests/unit/onyxTest.ts | 91 +++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 756c4de7..d5e93382 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -249,27 +249,6 @@ describe('Onyx', () => { }); }); - it('should ignore top-level undefined values', () => { - let testKeyValue: unknown; - - connectionID = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => { - testKeyValue = value; - }, - }); - - return Onyx.set(ONYX_KEYS.TEST_KEY, {test1: 'test1'}) - .then(() => { - expect(testKeyValue).toEqual({test1: 'test1'}); - return Onyx.merge(ONYX_KEYS.TEST_KEY, undefined); - }) - .then(() => { - expect(testKeyValue).toEqual({test1: 'test1'}); - }); - }); - it('should remove keys that are set to null when merging', () => { let testKeyValue: unknown; @@ -478,10 +457,49 @@ describe('Onyx', () => { }) .then(() => { expect(testKeyValue).toEqual({test1: {test2: 'test2', test3: 'test3'}}); - return Onyx.merge(ONYX_KEYS.TEST_KEY, undefined); }); }); + it('should ignore top-level and remove nested `undefined` values in Onyx.mergeCollection', () => { + let result: OnyxCollection; + + const routineRoute = `${ONYX_KEYS.COLLECTION.TEST_KEY}routine`; + const holidayRoute = `${ONYX_KEYS.COLLECTION.TEST_KEY}holiday`; + const workRoute = `${ONYX_KEYS.COLLECTION.TEST_KEY}work`; + + connectionID = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_KEY, + initWithStoredValues: false, + callback: (value) => (result = value), + waitForCollectionCallback: true, + }); + + return Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { + [routineRoute]: { + waypoints: { + 1: 'Home', + 2: 'Work', + 3: undefined, + }, + }, + [holidayRoute]: { + waypoints: undefined, + }, + [workRoute]: undefined, + } as GenericCollection).then(() => { + expect(result).toEqual({ + [routineRoute]: { + waypoints: { + 1: 'Home', + 2: 'Work', + }, + }, + [holidayRoute]: {}, + [workRoute]: undefined, + }); + }); + }); + it('should overwrite an array key nested inside an object', () => { let testKeyValue: unknown; connectionID = Onyx.connect({ @@ -1202,7 +1220,7 @@ describe('Onyx', () => { }); }); - it('should merge a non-existing key with a nested null removed', () => { + it("should not set null values in Onyx.merge, when the key doesn't exist yet", () => { let testKeyValue: unknown; connectionID = Onyx.connect({ @@ -1261,33 +1279,6 @@ describe('Onyx', () => { }); }); - it('should merge a non-existing key with a nested null removed', () => { - let testKeyValue: unknown; - - connectionID = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => { - testKeyValue = value; - }, - }); - - return Onyx.merge(ONYX_KEYS.TEST_KEY, { - waypoints: { - 1: 'Home', - 2: 'Work', - 3: null, - }, - }).then(() => { - expect(testKeyValue).toEqual({ - waypoints: { - 1: 'Home', - 2: 'Work', - }, - }); - }); - }); - it('mergeCollection should omit nested null values', () => { let result: OnyxCollection; From 8292acb8f09e655da15a9ecc25b0a28b4453c3d0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 10:04:59 +0200 Subject: [PATCH 82/96] fix: callbacks always return undefined instead of null --- tests/unit/onyxTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index d5e93382..bd9212e7 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -165,7 +165,7 @@ describe('Onyx', () => { }) .then(() => { // Test key should be cleared - expect(testKeyValue).toBeNull(); + expect(testKeyValue).toBeUndefined(); // Other test key should be returned to its default state expect(otherTestValue).toBe(42); @@ -1212,7 +1212,7 @@ describe('Onyx', () => { return waitForPromisesToResolve(); }) .then(() => { - expect(testKeyValue).toEqual(null); + expect(testKeyValue).toEqual(undefined); return Onyx.merge(ONYX_KEYS.TEST_KEY, 2); }) .then(() => { @@ -1275,7 +1275,7 @@ describe('Onyx', () => { return waitForPromisesToResolve(); }) .then(() => { - expect(testKeyValue).toEqual(null); + expect(testKeyValue).toEqual(undefined); }); }); From 81291e76826607c16cc5ea7315e8deeb9c481013 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 10:07:48 +0200 Subject: [PATCH 83/96] fix: tests after null -> undefined changes --- lib/Onyx.ts | 2 +- tests/unit/withOnyxTest.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index edbf058e..5d2acdec 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] ?? null; + const newValue = defaultKeyStates[key] ?? undefined; if (newValue !== oldValue) { cache.set(key, newValue); const collectionKey = key.substring(0, key.indexOf('_') + 1); diff --git a/tests/unit/withOnyxTest.tsx b/tests/unit/withOnyxTest.tsx index 34eb9de7..b29e8f40 100644 --- a/tests/unit/withOnyxTest.tsx +++ b/tests/unit/withOnyxTest.tsx @@ -810,7 +810,7 @@ describe('withOnyxTest', () => { // Correct data has been passed to the selector expect(selector).not.toHaveBeenCalledWith('world', expect.anything()); expect(selector).not.toHaveBeenCalledWith('dougal', expect.anything()); - expect(selector).toHaveBeenLastCalledWith(sourceData, {loading: false, text: 'world'}); + expect(selector).toHaveBeenLastCalledWith(undefined, {loading: false, text: 'world'}); // Default text has been rendered expect(onRender).toHaveBeenCalledTimes(1); From 790e2b799b46915f39f711774708d7ae43e56b94 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 14:02:22 +0200 Subject: [PATCH 84/96] remove custom UseOnyxValue type --- lib/useOnyx.ts | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 958170bb..230cbafa 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -2,21 +2,11 @@ import {deepEqual} from 'fast-equals'; import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react'; import type {IsEqual} from 'type-fest'; import OnyxUtils from './OnyxUtils'; -import type {CollectionKeyBase, KeyValueMapping, NonNull, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; +import type {CollectionKeyBase, OnyxCollection, OnyxKey, OnyxValue, Selector} from './types'; import useLiveRef from './useLiveRef'; import usePrevious from './usePrevious'; import Onyx from './Onyx'; -/** - * Represents a Onyx value that can be either a single entry or a collection of entries, depending on the `TKey` provided. - * It's a variation of `OnyxValue` type that is read-only and excludes the `null` type. - */ -type UseOnyxValue = string extends TKey - ? unknown - : TKey extends CollectionKeyBase - ? NonNull> - : NonNull>; - type BaseUseOnyxOptions = { /** * Determines if this key in this subscription is safe to be evicted. @@ -55,7 +45,7 @@ type UseOnyxOptions = BaseUseOnyxOptions & U type FetchStatus = 'loading' | 'loaded'; -type CachedValue = IsEqual> extends true ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; +type CachedValue = IsEqual> extends true ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; type ResultMetadata = { status: FetchStatus; @@ -67,15 +57,15 @@ function getCachedValue(key: TKey, selector?: Sele return OnyxUtils.tryGetCachedValue(key, {selector}) as CachedValue | undefined; } -function useOnyx>( +function useOnyx>( key: TKey, options?: BaseUseOnyxOptions & UseOnyxInitialValueOption & Required>, ): UseOnyxResult; -function useOnyx>( +function useOnyx>( key: TKey, options?: BaseUseOnyxOptions & UseOnyxInitialValueOption>, ): UseOnyxResult; -function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxResult { +function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxResult { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); From b386809bb5249d9951ab8f0236add16b9402aeeb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 14:04:40 +0200 Subject: [PATCH 85/96] update comments --- lib/types.ts | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index f193336d..8ba92f68 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -314,31 +314,7 @@ type Mapping = ConnectOptions & { /** * Represents a single Onyx input value, that can be either `TOnyxValue` or `null` if the key should be deleted. - * - * It can be used to specify data retrieved from Onyx e.g. `withOnyx` HOC mappings. - * - * @example - * ```ts - * import Onyx, {OnyxEntry, withOnyx} from 'react-native-onyx'; - * - * type OnyxProps = { - * userAccount: OnyxEntry; - * }; - * - * type Props = OnyxProps & { - * prop1: string; - * }; - * - * function Component({prop1, userAccount}: Props) { - * // ... - * } - * - * export default withOnyx({ - * userAccount: { - * key: ONYXKEYS.ACCOUNT, - * }, - * })(Component); - * ``` + * This type is used for data passed to Onyx e.g. in `Onyx.merge` and `Onyx.set`. */ type OnyxInputValue = TOnyxValue | null; @@ -347,7 +323,6 @@ type OnyxInputValue = TOnyxValue | null; * Setting a key to `null` will remove the key from the store. * `undefined` is not allowed for setting values, because it will have no effect on the data. */ -// type OnyxInput = TOnyxValue | null; type OnyxInput = OnyxInputValue>; /** From b1315b3733d5508869270481c19a9cce21dc49cc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 16:51:53 +0200 Subject: [PATCH 86/96] add a OnyxCollectionInput --- lib/index.ts | 2 ++ lib/types.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/index.ts b/lib/index.ts index bf08c29d..a4e3cc5f 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,6 +10,7 @@ import type { OnyxValue, Selector, OnyxInputValue, + OnyxCollectionInput, OnyxInput, OnyxSetInput, OnyxMultiSetInput, @@ -33,6 +34,7 @@ export type { OnyxEntry, OnyxKey, OnyxInputValue, + OnyxCollectionInput, OnyxInput, OnyxSetInput, OnyxMultiSetInput, diff --git a/lib/types.ts b/lib/types.ts index 8ba92f68..e0c34779 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -318,12 +318,18 @@ type Mapping = ConnectOptions & { */ type OnyxInputValue = TOnyxValue | null; +/** + * Represents an Onyx collection input, that can be either a record of `TOnyxValue`s or `null` if the key should be deleted. + */ +type OnyxCollectionInput = OnyxInputValue>; + /** * Represents an input value that can be passed to Onyx methods, that can be either `TOnyxValue` or `null`. * Setting a key to `null` will remove the key from the store. * `undefined` is not allowed for setting values, because it will have no effect on the data. */ -type OnyxInput = OnyxInputValue>; +// type OnyxInput = OnyxInputValue>; +type OnyxInput = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollectionInput : OnyxInputValue; /** * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. @@ -453,6 +459,7 @@ export type { OnyxEntry, OnyxKey, OnyxInputValue, + OnyxCollectionInput, OnyxInput, OnyxSetInput, OnyxMultiSetInput, From 08c2bfbabd4fb60d3fbf9a5dc3ebf715e934d4d3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 16:53:47 +0200 Subject: [PATCH 87/96] fix: OnyxInput type --- lib/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index e0c34779..0ec6fb1b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -328,8 +328,7 @@ type OnyxCollectionInput = OnyxInputValue = OnyxInputValue>; -type OnyxInput = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollectionInput : OnyxInputValue; +type OnyxInput = OnyxInputValue>; /** * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. From 4e893aa277ceacce0a903afb765ed1e724ec0d5a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 17:05:26 +0200 Subject: [PATCH 88/96] update mergeCollection input type --- lib/Onyx.ts | 4 ++-- lib/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 5d2acdec..74ff100e 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -423,12 +423,12 @@ function merge(key: TKey, changes: OnyxMergeInput): * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` * @param collection Object collection keyed by individual collection member keys and values */ -function mergeCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { +function mergeCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { if (typeof collection !== 'object' || Array.isArray(collection) || utils.isEmptyObject(collection)) { Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.'); return Promise.resolve(); } - const mergedCollection: OnyxInputKeyValueMapping = collection; + const mergedCollection = collection; // Confirm all the collection keys belong to the same parent let hasCollectionKeyCheckFailed = false; diff --git a/lib/types.ts b/lib/types.ts index 0ec6fb1b..605e19a5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -358,7 +358,7 @@ type OnyxMergeInput = OnyxInput; /** * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" */ -type OnyxMergeCollectionInput = Collection>, TMap>; +type OnyxMergeCollectionInput = OnyxCollectionInput>; /** * Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of From 4b1dccc8d14366c53448a2b2e6127ffaa97837b2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 17:05:48 +0200 Subject: [PATCH 89/96] remove unused ts-expect-error pragmas --- tests/unit/useOnyxTest.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 04160c51..d45eef97 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -163,7 +163,6 @@ describe('useOnyx', () => { }); it('should return selected data from a collection key', async () => { - // @ts-expect-error bypass Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, @@ -196,7 +195,6 @@ describe('useOnyx', () => { }); it('should not change selected data if a property outside the selector was changed', async () => { - // @ts-expect-error bypass Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, From 7b72e607eba5828a7befc4baea76e96d2c2a69e6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Jun 2024 17:13:59 +0200 Subject: [PATCH 90/96] rename type --- lib/index.ts | 4 ++-- lib/types.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index a4e3cc5f..04ec3fb9 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,7 +10,7 @@ import type { OnyxValue, Selector, OnyxInputValue, - OnyxCollectionInput, + OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, @@ -34,7 +34,7 @@ export type { OnyxEntry, OnyxKey, OnyxInputValue, - OnyxCollectionInput, + OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, diff --git a/lib/types.ts b/lib/types.ts index 605e19a5..3b098b07 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -321,7 +321,7 @@ type OnyxInputValue = TOnyxValue | null; /** * Represents an Onyx collection input, that can be either a record of `TOnyxValue`s or `null` if the key should be deleted. */ -type OnyxCollectionInput = OnyxInputValue>; +type OnyxCollectionInputValue = OnyxInputValue>; /** * Represents an input value that can be passed to Onyx methods, that can be either `TOnyxValue` or `null`. @@ -358,7 +358,7 @@ type OnyxMergeInput = OnyxInput; /** * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" */ -type OnyxMergeCollectionInput = OnyxCollectionInput>; +type OnyxMergeCollectionInput = OnyxCollectionInputValue>; /** * Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of @@ -458,7 +458,7 @@ export type { OnyxEntry, OnyxKey, OnyxInputValue, - OnyxCollectionInput, + OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, From 0c9b49c62aac60f637e1ddec0d66568fc2ee071d Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 6 Jun 2024 06:37:53 +0000 Subject: [PATCH 91/96] 2.0.45 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb2ce6a1..57b29b72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.44", + "version": "2.0.45", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.44", + "version": "2.0.45", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 301cb3ed..fdb8a493 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.44", + "version": "2.0.45", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native", From 5fc63a9cfae1f11dfd55ea2fb644f1e1147dedbd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Jun 2024 11:53:07 +0200 Subject: [PATCH 92/96] Revert "update mergeCollection input type" This reverts commit 4e893aa277ceacce0a903afb765ed1e724ec0d5a. --- lib/Onyx.ts | 4 ++-- lib/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 74ff100e..5d2acdec 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -423,12 +423,12 @@ function merge(key: TKey, changes: OnyxMergeInput): * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` * @param collection Object collection keyed by individual collection member keys and values */ -function mergeCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { +function mergeCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { if (typeof collection !== 'object' || Array.isArray(collection) || utils.isEmptyObject(collection)) { Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.'); return Promise.resolve(); } - const mergedCollection = collection; + const mergedCollection: OnyxInputKeyValueMapping = collection; // Confirm all the collection keys belong to the same parent let hasCollectionKeyCheckFailed = false; diff --git a/lib/types.ts b/lib/types.ts index 3b098b07..f7e491cc 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -358,7 +358,7 @@ type OnyxMergeInput = OnyxInput; /** * This represents the value that can be passed to `Onyx.merge` and to `Onyx.update` with the method "MERGE" */ -type OnyxMergeCollectionInput = OnyxCollectionInputValue>; +type OnyxMergeCollectionInput = Collection>, TMap>; /** * Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of From 2cabca8264b556bed6f0b6af05e030ac20d4a420 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Jun 2024 12:00:51 +0200 Subject: [PATCH 93/96] add back @ts-expect-error --- tests/unit/useOnyxTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index d45eef97..04160c51 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -163,6 +163,7 @@ describe('useOnyx', () => { }); it('should return selected data from a collection key', async () => { + // @ts-expect-error bypass Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, @@ -195,6 +196,7 @@ describe('useOnyx', () => { }); it('should not change selected data if a property outside the selector was changed', async () => { + // @ts-expect-error bypass Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, From 434e1f8e50723ef41b3605405730f6a33d1e2315 Mon Sep 17 00:00:00 2001 From: Rafe Colton Date: Thu, 6 Jun 2024 17:09:06 -0700 Subject: [PATCH 94/96] Update node version to match latest --- .nvmrc | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.nvmrc b/.nvmrc index d5a15960..48b14e6b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.10.0 +20.14.0 diff --git a/package-lock.json b/package-lock.json index 57b29b72..f6ebb9af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,8 +59,8 @@ "typescript": "^5.4.5" }, "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" + "node": ">=20.14.0", + "npm": ">=10.7.0" }, "peerDependencies": { "idb-keyval": "^6.2.1", diff --git a/package.json b/package.json index fdb8a493..054391a1 100644 --- a/package.json +++ b/package.json @@ -111,8 +111,8 @@ } }, "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" + "node": ">=20.14.0", + "npm": ">=10.7.0" }, "sideEffects": false } From d5f9c75cd9cc90536bcfcd4edfcfc7afeb53ba96 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 7 Jun 2024 00:15:05 +0000 Subject: [PATCH 95/96] 2.0.46 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6ebb9af..db208a29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.45", + "version": "2.0.46", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.45", + "version": "2.0.46", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 054391a1..a2eb3eba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.45", + "version": "2.0.46", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native", From a4f75c1c4ef286659ac7eeaf2a390d2177effe93 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 7 Jun 2024 20:13:29 +0000 Subject: [PATCH 96/96] 2.0.47 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db208a29..afa18e8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.46", + "version": "2.0.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.46", + "version": "2.0.47", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index a2eb3eba..2234ca31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.46", + "version": "2.0.47", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native",