diff --git a/API-INTERNAL.md b/API-INTERNAL.md
index aae82ccc7..db3e45f5f 100644
--- a/API-INTERNAL.md
+++ b/API-INTERNAL.md
@@ -115,7 +115,8 @@ whatever it is we attempted to do.
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
diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts
index e06496851..68054766e 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/types';
// 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 e7e818893..79bcf8567 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 withOnyx from './withOnyx';
+import type {WithOnyxState} from './withOnyx/types';
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 72c6afbb4..7ea33e52f 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/types';
/**
* 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;
@@ -400,6 +390,9 @@ type InitOptions = {
debugSetState?: boolean;
};
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type GenericFunction = (...args: any[]) => any;
+
export type {
BaseConnectOptions,
Collection,
@@ -413,6 +406,7 @@ export type {
DefaultConnectCallback,
DefaultConnectOptions,
ExtractOnyxCollectionValue,
+ GenericFunction,
InitOptions,
Key,
KeyValueMapping,
@@ -428,6 +422,4 @@ export type {
OnyxValue,
Selector,
WithOnyxConnectOptions,
- WithOnyxInstance,
- WithOnyxInstanceState,
};
diff --git a/lib/types/modules/react.d.ts b/lib/types/modules/react.d.ts
new file mode 100644
index 000000000..3e2e8fb37
--- /dev/null
+++ b/lib/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;
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index 64c56c0da..fb64cea7e 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -138,4 +138,58 @@ function checkCompatibilityWithExistingValue(value: unknown, existingValue: unkn
};
}
-export default {isEmptyObject, fastMerge, formatActionName, removeNestedNullValues, checkCompatibilityWithExistingValue};
+/**
+ * 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)) {
+ shouldInclude = condition.includes(key);
+ } else if (typeof condition === 'string') {
+ shouldInclude = key === condition;
+ } else {
+ shouldInclude = condition(entries[i]);
+ }
+
+ if (include ? shouldInclude : !shouldInclude) {
+ result[key] = value;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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.js b/lib/withOnyx/index.tsx
similarity index 56%
rename from lib/withOnyx.js
rename to lib/withOnyx/index.tsx
index cd862637c..3245bc8d4 100644
--- a/lib/withOnyx.js
+++ b/lib/withOnyx/index.tsx
@@ -3,50 +3,76 @@
* 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 React from 'react';
-import _ from 'underscore';
-import Onyx from './Onyx';
-import * as Str from './Str';
-import utils from './utils';
-import OnyxUtils from './OnyxUtils';
+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.
-const mappingPropertiesToIgnoreChangesTo = ['initialValue', 'allowStaleData'];
+const mappingPropertiesToIgnoreChangesTo: Array = ['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
+ */
+function getOnyxDataFromState(state: WithOnyxState, onyxToStateMapping: MapOnyxToState) {
+ return utils.pick(state, Object.keys(onyxToStateMapping)) as Partial>;
+}
+
+/**
+ * Utility function to return the properly typed entries of the `withOnyx` mapping object.
*/
-const getOnyxDataFromState = (state, onyxToStateMapping) => _.pick(state, _.keys(onyxToStateMapping));
+function mapOnyxToStateEntries(mapOnyxToState: MapOnyxToState) {
+ return Object.entries(mapOnyxToState) as Array<[keyof TOnyxProps, WithOnyxMapping]>;
+}
-export default function (mapOnyxToState, shouldDelayUpdates = false) {
+/**
+ * @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,
+): (component: React.ComponentType) => 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)
- .keys()
- .value();
- return (WrappedComponent) => {
+ const requiredKeysForInit = Object.keys(
+ utils.omit(mapOnyxToState, ([, options]) => (options as MapOnyxToState[keyof TOnyxProps]).initWithStoredValues === false),
+ );
+
+ return (WrappedComponent: React.ComponentType): React.ComponentType> => {
const displayName = getDisplayName(WrappedComponent);
- class withOnyx extends React.Component {
- pendingSetStates = [];
- constructor(props) {
+ class withOnyx extends React.Component, WithOnyxState> {
+ // eslint-disable-next-line react/static-property-placement
+ static displayName: string;
+
+ pendingSetStates: Array | ((state: WithOnyxState) => WithOnyxState | null)> = [];
+
+ shouldDelayUpdates: boolean;
+
+ activeConnectionIDs: Record;
+
+ tempState: WithOnyxState | undefined;
+
+ constructor(props: WithOnyxProps) {
super(props);
+
this.shouldDelayUpdates = shouldDelayUpdates;
this.setWithOnyxState = this.setWithOnyxState.bind(this);
this.flushPendingSetStates = this.flushPendingSetStates.bind(this);
@@ -55,38 +81,34 @@ 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 = mapOnyxToStateEntries(mapOnyxToState).reduce>((resultObj, [propName, mapping]) => {
+ const key = Str.result(mapping.key as GenericFunction, props);
+ let value = OnyxUtils.tryGetCachedValue(key, mapping as Partial>);
+ if (!value && mapping.initialValue) {
+ value = mapping.initialValue;
+ }
- /**
- * 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 (mapping.initWithStoredValues !== false && ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData)) {
- // eslint-disable-next-line no-param-reassign
- resultObj[propertyName] = value;
- }
+ /**
+ * 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 (mapping.initWithStoredValues !== false && ((value !== undefined && !OnyxUtils.hasPendingMergeForKey(key)) || mapping.allowStaleData)) {
+ // eslint-disable-next-line no-param-reassign
+ resultObj[propName] = value as WithOnyxState[keyof TOnyxProps];
+ }
- return resultObj;
- },
- {},
- );
+ 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;
+ 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;
@@ -98,17 +120,19 @@ 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)) {
+ mapOnyxToStateEntries(mapOnyxToState).forEach(([propName, mapping]) => {
+ if (mappingPropertiesToIgnoreChangesTo.includes(propName)) {
return;
}
- const key = Str.result(mapping.key, {...this.props, ...onyxDataFromState});
- this.connectMappingToOnyx(mapping, propertyName, key);
+
+ const key = Str.result(mapping.key as GenericFunction, {...this.props, ...onyxDataFromState});
+ this.connectMappingToOnyx(mapping, propName, 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 +140,9 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) {
const onyxDataFromState = getOnyxDataFromState(this.state, mapOnyxToState);
const prevOnyxDataFromState = getOnyxDataFromState(prevState, mapOnyxToState);
- _.each(mapOnyxToState, (mapping, propName) => {
+ mapOnyxToStateEntries(mapOnyxToState).forEach(([propName, mapping]) => {
// 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,26 +153,27 @@ 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 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];
this.connectMappingToOnyx(mapping, propName, newKey);
}
});
+
this.checkEvictableKeys();
}
componentWillUnmount() {
// Disconnect everything from Onyx
- _.each(mapOnyxToState, (mapping) => {
- const key = Str.result(mapping.key, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)});
+ mapOnyxToStateEntries(mapOnyxToState).forEach(([, mapping]) => {
+ const key = Str.result(mapping.key as GenericFunction, {...this.props, ...getOnyxDataFromState(this.state, mapOnyxToState)});
Onyx.disconnect(this.activeConnectionIDs[key], key);
});
}
- setStateProxy(modifier) {
+ setStateProxy(modifier: WithOnyxState | ((state: WithOnyxState) => WithOnyxState | null)) {
if (this.shouldDelayUpdates) {
this.pendingSetStates.push(modifier);
} else {
@@ -170,11 +195,8 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) {
* 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, 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 +212,14 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) {
return;
}
- this.setStateProxy({[statePropertyName]: val});
+ 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 = _.some(requiredKeysForInit, (key) => _.isUndefined(this.tempState[key]));
+ const tempStateIsMissingKey = requiredKeysForInit.some((key) => this.tempState?.[key as keyof TOnyxProps] === undefined);
if (tempStateIsMissingKey) {
return;
}
@@ -207,35 +229,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 (prevState[key] !== undefined) {
+ // 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 +271,13 @@ 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)) {
+ mapOnyxToStateEntries(mapOnyxToState).forEach(([, mapping]) => {
+ 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 as GenericFunction, this.props);
+ 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: []}).`);
@@ -272,26 +294,27 @@ 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) {
+ 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.
// 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,
- withOnyxInstance: this,
+ statePropertyName: statePropertyName as string,
+ withOnyxInstance: this as unknown as WithOnyxInstance,
displayName,
});
}
@@ -304,14 +327,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 as Omit, ([, propValue]) => propValue === null);
if (this.state.loading) {
return null;
@@ -319,8 +343,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, ([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
@@ -328,7 +351,7 @@ export default function (mapOnyxToState, shouldDelayUpdates = false) {
{
const Component = withOnyx;
return (
diff --git a/lib/withOnyx.d.ts b/lib/withOnyx/types.ts
similarity index 73%
rename from lib/withOnyx.d.ts
rename to lib/withOnyx/types.ts
index f665cea9f..06145551a 100644
--- a/lib/withOnyx.d.ts
+++ b/lib/withOnyx/types.ts
@@ -1,5 +1,6 @@
-import {IsEqual} from 'type-fest';
-import {CollectionKeyBase, ExtractOnyxCollectionValue, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types';
+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.
@@ -10,10 +11,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;
};
@@ -61,6 +68,7 @@ type BaseMappingKey = {
key: TOnyxKey;
selector: Selector;
@@ -97,6 +105,14 @@ type Mapping
);
+/**
+ * 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.
*/
@@ -125,17 +141,30 @@ type OnyxPropCollectionMapping = {
+ [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping;
+};
+
+/**
+ * Represents the `withOnyx` internal component props.
+ */
+type WithOnyxProps = Omit & {forwardedRef?: ForwardedRef};
+
+/**
+ * Represents the `withOnyx` internal component state.
*/
-declare function withOnyx(
- mapping: {
- [TOnyxProp in keyof TOnyxProps]: OnyxPropMapping | OnyxPropCollectionMapping;
- },
- shouldDelayUpdates?: boolean,
-): (component: React.ComponentType) => React.ComponentType>;
-
-export default withOnyx;
+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};
diff --git a/package.json b/package.json
index e47231ca3..777e3aa01 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",