Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Onyx derived values #56891

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f57151d
Bump type-fest for NonEmptyTuple
roryabraham Feb 14, 2025
0733f7d
Create simplified implementation storing derived values directly in Onyx
roryabraham Feb 14, 2025
f05f33c
fix types and remove await
roryabraham Feb 14, 2025
8a576be
Implement example
roryabraham Feb 14, 2025
502b3c6
Add type assertions
roryabraham Feb 14, 2025
4c343ef
Prevent circular dependencies
roryabraham Feb 14, 2025
fe56c68
Include key in config for straightforward inferencing
roryabraham Feb 14, 2025
f505c0b
Merge branch 'main' into Rory-OnyxDerivedV2
roryabraham Feb 17, 2025
cb92cfc
Update README with instructions for adding new derived values
roryabraham Feb 17, 2025
4817676
Bump type-fest to latest cuz why not
roryabraham Feb 17, 2025
05e17ff
Fix withToggleVisibilityView types after type-fest upgrade
roryabraham Feb 17, 2025
7de6fc5
Add initOnyxDerivedValues to ReportUtilsTest
roryabraham Feb 17, 2025
1cb4cbe
Set concierge chat report in Onyx to fix ReportUtilsTest
roryabraham Feb 17, 2025
d3e7010
Fix navigateAfterOnboardingTest
roryabraham Feb 17, 2025
1ab4d35
Added a spot of logging
roryabraham Feb 17, 2025
a922c34
Add top-level comment
roryabraham Feb 17, 2025
c045ef2
Add unit tests
roryabraham Feb 17, 2025
867f5e4
Add logs for if derived value is restored from disk
roryabraham Feb 17, 2025
b96d4a6
Cleanup in libs/actions/Report as well
roryabraham Feb 17, 2025
0d08483
Merge branch 'main' into Rory-OnyxDerivedV2
roryabraham Feb 18, 2025
27ece8d
Split up dervied value configs into their own files
roryabraham Feb 18, 2025
25bdbf4
Use OnyxValue instead of GetOnyxTypeForKey
roryabraham Feb 18, 2025
aed0d95
Use OnyxUtils.tupleGet, remove unused GetOnyxTypeForKey
roryabraham Feb 18, 2025
446fead
Move ONYX_DERIVED_VALUES to its own file
roryabraham Feb 18, 2025
2a76669
Bump Onyx for OnyxUtils.tupleGet
roryabraham Feb 18, 2025
0a91c78
Fix lint
roryabraham Feb 18, 2025
d226365
Merge branch 'main' into Rory-OnyxDerivedV2
roryabraham Feb 21, 2025
c040ec0
Include current value as second arg to compute
roryabraham Feb 21, 2025
140c793
Remove readonly
roryabraham Feb 21, 2025
b9f1d50
Merge branch 'main' into Rory-OnyxDerivedV2
roryabraham Feb 24, 2025
88ba460
Return early if conciergeChatReportID is already computed
roryabraham Feb 24, 2025
9e57693
Simplify types using satisfies
roryabraham Feb 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,14 @@ In order to compile a production iOS build, run `npm run ios-build`, this will g

#### Local production build the Android app
To build an APK to share run (e.g. via Slack), run `npm run android-build`, this will generate a new APK in the `android/app` folder.

# Onyx derived values
Onyx derived values are special Onyx keys which contain values derived from other Onyx values. These are available as a performance optimization, so that if the result of a common computation of Onyx values is needed in many places across the app, the computation can be done only as needed in a centralized location, and then shared across the app. Once created, Onyx derived values are stored and consumed just like any other Onyx value.

## Creating new Onyx derived values
1. Add the new Onyx key. The keys for Onyx derived values are stored in `ONYXKEYS.ts`, in the `ONYXKEYS.DERIVED` object.
2. Declare the type for the derived value in `ONYXKEYS.ts`, in the `OnyxDerivedValuesMapping` type.
3. Add the derived value config to `ONYX_DERIVED_VALUES` in `src/libs/OnyxDerived.ts`. A derived value config is defined by:
1. The Onyx key for the derived value
2. An array of dependent Onyx keys (which can be any keys, not including the one from the previous step. Including other derived values!)
3. A `compute` function, which takes an array of dependent Onyx values (in the same order as the array of keys from the previous step), and returns a value matching the type you declared in `OnyxDerivedValuesMapping`
14 changes: 8 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
"react-native-launch-arguments": "^4.0.2",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
"react-native-onyx": "2.0.92",
"react-native-onyx": "2.0.93",
"react-native-pager-view": "6.5.1",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
Expand Down Expand Up @@ -339,7 +339,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"type-fest": "4.20.0",
"type-fest": "4.35.0",
"typescript": "^5.4.5",
"wait-port": "^0.2.9",
"webpack": "^5.94.0",
Expand Down
27 changes: 24 additions & 3 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,9 @@ const ONYXKEYS = {
WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm',
WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft',
},
DERIVED: {
CONCIERGE_CHAT_REPORT_ID: 'conciergeChatReportID',
},
} as const;

type AllOnyxKeys = DeepValueOf<typeof ONYXKEYS>;
Expand Down Expand Up @@ -1079,14 +1082,20 @@ type OnyxValuesMapping = {
[ONYXKEYS.LAST_FULL_RECONNECT_TIME]: string;
[ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;

type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: string;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;

type OnyxCollectionKey = keyof OnyxCollectionValuesMapping;
type OnyxFormKey = keyof OnyxFormValuesMapping;
type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;
type OnyxDerivedKey = keyof OnyxDerivedValuesMapping;

type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey | OnyxDerivedKey;
type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES;

type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude<AllOnyxKeys, OnyxKey>}`;
Expand All @@ -1095,4 +1104,16 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing:
type AssertOnyxKeys = AssertTypesEqual<AllOnyxKeys, OnyxKey, MissingOnyxKeysError>;

export default ONYXKEYS;
export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues};
export type {
OnyxCollectionKey,
OnyxCollectionValuesMapping,
OnyxFormDraftKey,
OnyxFormKey,
OnyxFormValuesMapping,
OnyxKey,
OnyxPagesKey,
OnyxValueKey,
OnyxValues,
OnyxDerivedKey,
OnyxDerivedValuesMapping,
};
9 changes: 4 additions & 5 deletions src/components/withToggleVisibilityView.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import type {ComponentType, ForwardedRef, ReactElement, RefAttributes} from 'react';
import React from 'react';
import {View} from 'react-native';
import type {SetOptional} from 'type-fest';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes were only necessary because I upgraded type-fest and a bug fix in that repo exposed existing issues with the types in this code.

import useThemeStyles from '@hooks/useThemeStyles';
import getComponentDisplayName from '@libs/getComponentDisplayName';

type WithToggleVisibilityViewProps = {
/** Whether the content is visible. */
isVisible: boolean;
isVisible?: boolean;
};

export default function withToggleVisibilityView<TProps extends WithToggleVisibilityViewProps, TRef>(
export default function withToggleVisibilityView<TProps, TRef>(
WrappedComponent: ComponentType<TProps & RefAttributes<TRef>>,
): (props: TProps & RefAttributes<TRef>) => ReactElement | null {
function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional<TProps, 'isVisible'>, ref: ForwardedRef<TRef>) {
): (props: TProps & WithToggleVisibilityViewProps & RefAttributes<TRef>) => ReactElement | null {
function WithToggleVisibilityView({isVisible = false, ...rest}: WithToggleVisibilityViewProps, ref: ForwardedRef<TRef>) {
const styles = useThemeStyles();
return (
<View
Expand Down
20 changes: 6 additions & 14 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,10 +731,12 @@ let isAnonymousUser = false;
// Example case: when we need to get a report name of a thread which is dependent on a report action message.
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeChatReportID: string | undefined;
let conciergeChatReportID: OnyxEntry<string>;
Onyx.connect({
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => (conciergeChatReportID = value),
key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID,
callback: (value) => {
conciergeChatReportID = value;
},
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Expand Down Expand Up @@ -1538,21 +1540,11 @@ function getReportNotificationPreference(report: OnyxEntry<Report>): ValueOf<typ
return participant?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
}

const CONCIERGE_ACCOUNT_ID_STRING = CONST.ACCOUNT_ID.CONCIERGE.toString();
/**
* Only returns true if this is our main 1:1 DM report with Concierge.
*/
function isConciergeChatReport(report: OnyxInputOrEntry<Report>): boolean {
if (!report?.participants || isThread(report)) {
return false;
}

const participantAccountIDs = new Set(Object.keys(report.participants));
if (participantAccountIDs.size !== 2) {
return false;
}

return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING) || report?.reportID === conciergeChatReportID;
return !!report && report?.reportID === conciergeChatReportID;
}

function findSelfDMReportID(): string | undefined {
Expand Down
41 changes: 41 additions & 0 deletions src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {OnyxEntry} from 'react-native-onyx';
import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual';
import type SymmetricDifference from '@src/types/utils/SymmetricDifference';
import conciergeChatReportIDConfig from './configs/conciergeChatReportID';

/**
* Global map of derived configs.
* This object holds our derived value configurations.
*/
const ONYX_DERIVED_VALUES = {
[ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig,
} as const;

export default ONYX_DERIVED_VALUES;

// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity.
// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types,
// according to the type definitions for derived keys in ONYXKEYS.ts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we can't use satisfies? 🤔 I feel like it works well for me:

Diff
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index d0555ed5412..57d9d99c810 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -761,6 +761,7 @@ const ONYXKEYS = {
     },
     DERIVED: {
         CONCIERGE_CHAT_REPORT_ID: 'conciergeChatReportID',
+        CONCIERGE_CHAT_REPORT_ID2: 'conciergeChatReportID2',
     },
 } as const;
 
@@ -1085,6 +1086,7 @@ type OnyxValuesMapping = {
 
 type OnyxDerivedValuesMapping = {
     [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: string;
+    [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID2]: string;
 };
 
 type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
index b9625e82871..e8efddbc4ab 100644
--- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
+++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
@@ -1,9 +1,7 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS';
+import type {ValueOf} from 'type-fest';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual';
-import type SymmetricDifference from '@src/types/utils/SymmetricDifference';
 import conciergeChatReportIDConfig from './configs/conciergeChatReportID';
+import type {OnyxDerivedValueConfig} from './types';
 
 /**
  * Global map of derived configs.
@@ -11,31 +9,8 @@ import conciergeChatReportIDConfig from './configs/conciergeChatReportID';
  */
 const ONYX_DERIVED_VALUES = {
     [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig,
-} as const;
+    [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID2]: conciergeChatReportIDConfig,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+} as const satisfies Record<ValueOf<typeof ONYXKEYS.DERIVED>, OnyxDerivedValueConfig<any, any>>;
 
 export default ONYX_DERIVED_VALUES;
-
-// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity.
-// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types,
-// according to the type definitions for derived keys in ONYXKEYS.ts.
-type MismatchedDerivedKeysError =
-    `Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference<
-        keyof typeof ONYX_DERIVED_VALUES,
-        OnyxDerivedKey
-    >}`;
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-type KeyAssertion = AssertTypesEqual<keyof typeof ONYX_DERIVED_VALUES, OnyxDerivedKey, MismatchedDerivedKeysError>;
-
-type ExpectedDerivedValueComputeReturnTypes = {
-    [Key in keyof OnyxDerivedValuesMapping]: OnyxEntry<OnyxDerivedValuesMapping[Key]>;
-};
-type ActualDerivedValueComputeReturnTypes = {
-    [Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>;
-};
-type MismatchedDerivedValues = {
-    [Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key;
-}[keyof ExpectedDerivedValueComputeReturnTypes];
-type MismatchedDerivedValuesError =
-    `Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`;
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-type ComputeReturnTypeAssertion = AssertTypesEqual<MismatchedDerivedValues, never, MismatchedDerivedValuesError>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record<ValueOf, OnyxDerivedValueConfig<any, any>>

Right, but you'd actually want it to, rather than being an OnyxDerivedValueConfig<any, any> be and OnyxDerivedValueConfig for that specific key.

Plus, I like how this provides clearer error messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okaaay, I believe we can achieve it with a mapped type! Recorder a video to showcase the DX:

Screen.Recording.2025-02-24.at.11.15.31.mov
Diff as always
diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
index b9625e82871..bac7c7e7f8e 100644
--- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
+++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
@@ -1,9 +1,8 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS';
+import type {OnyxValue} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual';
-import type SymmetricDifference from '@src/types/utils/SymmetricDifference';
 import conciergeChatReportIDConfig from './configs/conciergeChatReportID';
+import type {OnyxDerivedValueConfig} from './types';
 
 /**
  * Global map of derived configs.
@@ -11,31 +10,9 @@ import conciergeChatReportIDConfig from './configs/conciergeChatReportID';
  */
 const ONYX_DERIVED_VALUES = {
     [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig,
-} as const;
+} satisfies {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    [Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any, OnyxValue<Key>>;
+};
 
 export default ONYX_DERIVED_VALUES;
-
-// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity.
-// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types,
-// according to the type definitions for derived keys in ONYXKEYS.ts.
-type MismatchedDerivedKeysError =
-    `Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference<
-        keyof typeof ONYX_DERIVED_VALUES,
-        OnyxDerivedKey
-    >}`;
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-type KeyAssertion = AssertTypesEqual<keyof typeof ONYX_DERIVED_VALUES, OnyxDerivedKey, MismatchedDerivedKeysError>;
-
-type ExpectedDerivedValueComputeReturnTypes = {
-    [Key in keyof OnyxDerivedValuesMapping]: OnyxEntry<OnyxDerivedValuesMapping[Key]>;
-};
-type ActualDerivedValueComputeReturnTypes = {
-    [Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>;
-};
-type MismatchedDerivedValues = {
-    [Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key;
-}[keyof ExpectedDerivedValueComputeReturnTypes];
-type MismatchedDerivedValuesError =
-    `Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`;
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-type ComputeReturnTypeAssertion = AssertTypesEqual<MismatchedDerivedValues, never, MismatchedDerivedValuesError>;
diff --git a/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts b/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts
index 6c8c090ff7d..bbec3a075cf 100644
--- a/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts
+++ b/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts
@@ -1,3 +1,4 @@
+import type {OnyxValue} from 'react-native-onyx';
 import type {NonEmptyTuple, ValueOf} from 'type-fest';
 import type {OnyxKey} from '@src/ONYXKEYS';
 import type ONYXKEYS from '@src/ONYXKEYS';
@@ -17,8 +18,8 @@ import type {OnyxDerivedValueConfig} from './types';
  *     dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID]
  * })
  */
-export default function createOnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>>(
-    config: OnyxDerivedValueConfig<Key, Deps>,
-): OnyxDerivedValueConfig<Key, Deps> {
+export default function createOnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>, DerivedValue extends OnyxValue<Key>>(
+    config: OnyxDerivedValueConfig<Key, Deps, DerivedValue>,
+): OnyxDerivedValueConfig<Key, Deps, DerivedValue> {
     return config;
 }
diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts
index 419a5357421..a43bba8cb4f 100644
--- a/src/libs/actions/OnyxDerived/types.ts
+++ b/src/libs/actions/OnyxDerived/types.ts
@@ -10,15 +10,15 @@ import type ONYXKEYS from '@src/ONYXKEYS';
  *    The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies.
  *    For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection<Report>, OnyxEntry<Account>]
  */
-type OnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>> = {
+type OnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>, DerivedValue extends OnyxValue<Key>> = {
     key: Key;
     dependencies: Deps;
     compute: (
         args: {
             [Index in keyof Deps]: OnyxValue<Deps[Index]>;
         },
-        currentValue: OnyxValue<Key>,
-    ) => OnyxValue<Key>;
+        currentValue: DerivedValue,
+    ) => DerivedValue;
 };
 
 // eslint-disable-next-line import/prefer-default-export

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roryabraham Let me know if this is what you wanted to achieve 😄 With this approach ONYXKEYS' mapping is the source of truth while keeping type safety in ONYX_DERIVED_VALUES per key, please test it yourself!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great work! I just pushed a variant of this that I believe is fully correct. The main difference between my version and yours is that mine does not coerce the return type of compute to be an OnyxEntry.

The error messages suck, in particular when you have a collection in the dependencies. But that seems like a common problem in TS in general. I tried adding some assertions to provide better error messages, but didn't get it working and don't want to sink too much time into it.

type MismatchedDerivedKeysError =
`Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference<
keyof typeof ONYX_DERIVED_VALUES,
OnyxDerivedKey
>}`;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type KeyAssertion = AssertTypesEqual<keyof typeof ONYX_DERIVED_VALUES, OnyxDerivedKey, MismatchedDerivedKeysError>;

type ExpectedDerivedValueComputeReturnTypes = {
[Key in keyof OnyxDerivedValuesMapping]: OnyxEntry<OnyxDerivedValuesMapping[Key]>;
};
type ActualDerivedValueComputeReturnTypes = {
[Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>;
};
type MismatchedDerivedValues = {
[Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key;
}[keyof ExpectedDerivedValueComputeReturnTypes];
type MismatchedDerivedValuesError =
`Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ComputeReturnTypeAssertion = AssertTypesEqual<MismatchedDerivedValues, never, MismatchedDerivedValuesError>;
29 changes: 29 additions & 0 deletions src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {isThread} from '@libs/ReportUtils';
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

export default createOnyxDerivedValueConfig({
key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID,
dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID],
compute: ([reports, conciergeChatReportID]) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API could be improved a little, so that array isn't needed:

Suggested change
compute: ([reports, conciergeChatReportID]) => {
compute: (reports, conciergeChatReportID) => {
Full diff in case you're interested:
diff --git a/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts b/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts
index ce246616b18..7dbe50ed25d 100644
--- a/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts
+++ b/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts
@@ -6,7 +6,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 export default createOnyxDerivedValueConfig({
     key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID,
     dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID],
-    compute: ([reports, conciergeChatReportID]) => {
+    compute: (reports, conciergeChatReportID) => {
         if (!reports) {
             return undefined;
         }
diff --git a/src/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts
index a30a7a0ca0f..0b858589c3e 100644
--- a/src/libs/actions/OnyxDerived/index.ts
+++ b/src/libs/actions/OnyxDerived/index.ts
@@ -18,7 +18,7 @@ function init() {
     for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) {
         // Create an array to hold the current values for each dependency.
         // We cast its type to match the tuple expected by config.compute.
-        let dependencyValues = new Array(dependencies.length) as Parameters<typeof compute>[0];
+        let dependencyValues = new Array(dependencies.length) as Parameters<typeof compute>;
 
         OnyxUtils.get(key).then((storedDerivedValue) => {
             let derivedValue = storedDerivedValue;
@@ -27,17 +27,17 @@ function init() {
             } else {
                 OnyxUtils.tupleGet(dependencies).then((values) => {
                     dependencyValues = values;
-                    derivedValue = compute(values);
+                    derivedValue = compute(...values);
                     Onyx.set(key, derivedValue ?? null);
                 });
             }
 
-            const setDependencyValue = <Index extends number>(i: Index, value: Parameters<typeof compute>[0][Index]) => {
+            const setDependencyValue = <Index extends number>(i: Index, value: Parameters<typeof compute>[Index]) => {
                 dependencyValues[i] = value;
             };
 
             const recomputeDerivedValue = () => {
-                const newDerivedValue = compute(dependencyValues);
+                const newDerivedValue = compute(...dependencyValues);
                 if (newDerivedValue !== derivedValue) {
                     Log.info(`[OnyxDerived] value for key ${key} changed, updating it in Onyx`, false, {old: derivedValue, new: newDerivedValue});
                     derivedValue = newDerivedValue;
diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts
index 3f05b6ea8c3..2aac96849a7 100644
--- a/src/libs/actions/OnyxDerived/types.ts
+++ b/src/libs/actions/OnyxDerived/types.ts
@@ -13,9 +13,11 @@ import type ONYXKEYS from '@src/ONYXKEYS';
 type OnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>> = {
     key: Key;
     dependencies: Deps;
-    compute: (args: {
-        -readonly [Index in keyof Deps]: OnyxValue<Deps[Index]>;
-    }) => OnyxValue<Key>;
+    compute: (
+        ...args: {
+            -readonly [Index in keyof Deps]: OnyxValue<Deps[Index]>;
+        }
+    ) => OnyxValue<Key>;
 };
 
 // eslint-disable-next-line import/prefer-default-export

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the same time I'm thinking if it'd be clear enough without a tuple? I think my only concern is that swapping dependencies in both these cases can break the whole pipeline.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the same would happen with or without an array, right? In the end you have TS type checking to make sure you use it correctly

Copy link
Member

@parasharrajat parasharrajat Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like array-like syntax for the reason that its kind of norm for many modules. e.g. Promise.all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just suggesting that it is possible! I'm good with both approaches 👍

Copy link
Contributor Author

@roryabraham roryabraham Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we added the current value as a second arg to compute, I think I'm going to leave the first as a tuple. That way it's not like you have n args and the last one happens to be the current value

if (!reports) {
return undefined;
}

const conciergeReport = Object.values(reports).find((report) => {
if (!report?.participants || isThread(report)) {
return false;
}

const participantAccountIDs = new Set(Object.keys(report.participants));
if (participantAccountIDs.size !== 2) {
return false;
}

return participantAccountIDs.has(CONST.ACCOUNT_ID.CONCIERGE.toString()) || report?.reportID === conciergeChatReportID;
});

return conciergeReport?.reportID;
},
});
24 changes: 24 additions & 0 deletions src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {NonEmptyTuple, ValueOf} from 'type-fest';
import type {OnyxKey} from '@src/ONYXKEYS';
import type ONYXKEYS from '@src/ONYXKEYS';
import type {OnyxDerivedValueConfig} from './types';

/**
* Helper function to create a derived value config. This function is just here to help TypeScript infer Deps, so instead of writing this:
*
* const conciergeChatReportIDConfig: OnyxDerivedValueConfig<[typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.CONCIERGE_REPORT_ID]> = {
* dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID],
* ...
* };
*
* We can just write this:
*
* const conciergeChatReportIDConfig = createOnyxDerivedValueConfig({
* dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID]
* })
*/
export default function createOnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>>(
config: OnyxDerivedValueConfig<Key, Deps>,
): OnyxDerivedValueConfig<Key, Deps> {
return config;
}
76 changes: 76 additions & 0 deletions src/libs/actions/OnyxDerived/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* This file contains logic for derived Onyx keys. The idea behind derived keys is that if there is a common computation
* that we're doing in many places across the app to derive some value from multiple Onyx values, we can move that
* computation into this file, run it only once, and then share it across the app by storing the result of that computation in Onyx.
*
* The primary purpose is to optimize performance by reducing redundant computations. More info can be found in the README.
*/
import Onyx from 'react-native-onyx';
import OnyxUtils from 'react-native-onyx/dist/OnyxUtils';
import Log from '@libs/Log';
import ObjectUtils from '@src/types/utils/ObjectUtils';
import ONYX_DERIVED_VALUES from './ONYX_DERIVED_VALUES';

/**
* Initialize all Onyx derived values, store them in Onyx, and setup listeners to update them when dependencies change.
*/
function init() {
for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) {
// Create an array to hold the current values for each dependency.
// We cast its type to match the tuple expected by config.compute.
let dependencyValues = new Array(dependencies.length) as Parameters<typeof compute>[0];

OnyxUtils.get(key).then((storedDerivedValue) => {
let derivedValue = storedDerivedValue;
if (derivedValue) {
Log.info(`Derived value ${derivedValue} for ${key} restored from disk`);
} else {
OnyxUtils.tupleGet(dependencies).then((values) => {
dependencyValues = values;
derivedValue = compute(values);
Onyx.set(key, derivedValue ?? null);
});
}

const setDependencyValue = <Index extends number>(i: Index, value: Parameters<typeof compute>[0][Index]) => {
dependencyValues[i] = value;
};

const recomputeDerivedValue = () => {
const newDerivedValue = compute(dependencyValues);
if (newDerivedValue !== derivedValue) {
Log.info(`[OnyxDerived] value for key ${key} changed, updating it in Onyx`, false, {old: derivedValue, new: newDerivedValue});
derivedValue = newDerivedValue;
Onyx.set(key, derivedValue ?? null);
}
};

for (let i = 0; i < dependencies.length; i++) {
// eslint-disable-next-line rulesdir/prefer-at
const dependencyOnyxKey = dependencies[i];
if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) {
Onyx.connect({
key: dependencyOnyxKey,
waitForCollectionCallback: true,
callback: (value) => {
Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`);
setDependencyValue(i, value);
recomputeDerivedValue();
},
});
} else {
Onyx.connect({
key: dependencyOnyxKey,
callback: (value) => {
Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`);
setDependencyValue(i, value);
recomputeDerivedValue();
},
});
}
}
});
}
}

export default init;
22 changes: 22 additions & 0 deletions src/libs/actions/OnyxDerived/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {OnyxValue} from 'react-native-onyx';
import type {NonEmptyTuple, ValueOf} from 'type-fest';
import type {OnyxKey} from '@src/ONYXKEYS';
import type ONYXKEYS from '@src/ONYXKEYS';

/**
* A derived value configuration describes:
* - a tuple of Onyx keys to subscribe to (dependencies),
* - a compute function that derives a value from the dependent Onyx values.
* The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies.
* For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection<Report>, OnyxEntry<Account>]
*/
type OnyxDerivedValueConfig<Key extends ValueOf<typeof ONYXKEYS.DERIVED>, Deps extends NonEmptyTuple<Exclude<OnyxKey, Key>>> = {
key: Key;
dependencies: Deps;
compute: (args: {
-readonly [Index in keyof Deps]: OnyxValue<Deps[Index]>;
}) => OnyxValue<Key>;
};

// eslint-disable-next-line import/prefer-default-export
export type {OnyxDerivedValueConfig};
Loading
Loading