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

Merged
merged 35 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 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
5c609d7
Merge branch 'main' into Rory-OnyxDerivedV2
roryabraham Feb 26, 2025
2576aeb
Don't always rely on previously computed value
roryabraham Feb 27, 2025
bf9c5e6
Merge branch 'main' into Rory-OnyxDerivedV2
roryabraham Feb 28, 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 @@ -849,3 +849,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
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind moving this to the section specifically about Onyx? https://github.com/Expensify/App?tab=readme-ov-file#onyx

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
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to add a little bit more to this section to explain why you should or shouldn't use derived values. Would you mind adding a bit of details like:

### When should I use derived values?

### When should I avoid using 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`
6 changes: 4 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,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 | undefined;
};

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
17 changes: 17 additions & 0 deletions src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

It appears like this file is only imported in one place. I suggest moving this config only to the file that needs it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type {ValueOf} from 'type-fest';
import ONYXKEYS from '@src/ONYXKEYS';
import conciergeChatReportIDConfig from './configs/conciergeChatReportID';
import type {OnyxDerivedValueConfig} from './types';

/**
* 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 satisfies {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
};

export default ONYX_DERIVED_VALUES;
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) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am concerned this OnyxUtils.get() pattern is going to make it's way into the rest of the app and I think that is going to be a very bad pattern for people to follow. I suggest adding an ESLint rule that throws an error on these. We can disable the eslint rule where we need to, but add very explicit comments that the pattern is NOT supposed to be followed.

I've already seen we've struggled to keep the usage of makeRequestWithSideEffects() out of common patterns and I think the same thing is going to happen here.

Is there a reason why get() and tupleGet() are necessary? Why can we not get them from Onyx like we normally would (a connection that immediately disconnects itself)?

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, derivedValue);
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, derivedValue);
if (newDerivedValue !== derivedValue) {
Log.info(`[OnyxDerived] value for key ${key} changed, updating it in Onyx`, false, {old: derivedValue ?? null, new: newDerivedValue ?? null});
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB but I think we can simplify this logic by just setting the flag below and getting rid of the the if/else

Suggested change
waitForCollectionCallback: true,
waitForCollectionCallback: OnyxUtils.isCollectionKey(dependencyOnyxKey),

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like TS fails with this suggestion. So we'll keep the if/else here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This actually causes a type error. I wasn't able to figure out how to fix it. cc @fabioh8010 also spent a bit of time looking into it

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah no luck so far, I will revisit this again to see if I manage to make this work as expected

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;
25 changes: 25 additions & 0 deletions src/libs/actions/OnyxDerived/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {OnyxValue} from 'react-native-onyx';
import type {NonEmptyTuple, ValueOf} from 'type-fest';
import type {OnyxDerivedValuesMapping, 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: {
[Index in keyof Deps]: OnyxValue<Deps[Index]>;
},
currentValue: OnyxValue<Key>,
) => OnyxDerivedValuesMapping[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.

Why not use a default export here? The only reason I wouldn't use one is if you're expecting to immediately be adding more exported methods.

export type {OnyxDerivedValueConfig};
14 changes: 1 addition & 13 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ Onyx.connect({
});

Onyx.connect({
key: ONYXKEYS.CONCIERGE_REPORT_ID,
key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID,
callback: (value) => (conciergeChatReportID = value),
});

Expand Down Expand Up @@ -1568,14 +1568,6 @@ function handleReportChanged(report: OnyxEntry<Report>) {
}

saveReportDraftComment(preexistingReportID, draftReportComment, callback);

return;
}

if (reportID) {
if (isConciergeChatReport(report)) {
conciergeChatReportID = reportID;
}
Comment on lines -1575 to -1578
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we set the Onyx ONYXKEYS.CONCIERGE_REPORT_ID key here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We weren't before, why would we start now?

Copy link
Member

Choose a reason for hiding this comment

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

We were technically updating reference to the conciergeChatReportID with the correct id here. But it might be the case that we are only using this conciergeChatReportID in this file so updating local variable works but in case we use this Onyx Key somewhere else, it would be good to update the Onyx data instead of local.

Then, I have a followup question on why are we removing this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are removing this code because it was previously performing the computation done by our derived value to check if a new report is the concierge chat, and if so update the local value for conciergeChatReportID.

But now, the local variable conciergeChatReportID is subscribed to our derived value, and that value is updated as needed.

in case we use this Onyx Key somewhere else, it would be good to update the Onyx data instead of local

The piece of data that was being modified locally here (conciergeChatReportID) is now stored centrally in Onyx (computed via its derived value config).

Does that satisfy your concerns @parasharrajat? Or am I missing something?

Copy link
Member

Choose a reason for hiding this comment

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

Got it.

}
}

Expand Down Expand Up @@ -4655,9 +4647,6 @@ function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams,

fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_REPORT_TO_CSV}), 'Expensify.csv', '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed);
}
function getConciergeReportID() {
return conciergeChatReportID;
}

function setDeleteTransactionNavigateBackUrl(url: string) {
Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, url);
Expand Down Expand Up @@ -4695,7 +4684,6 @@ export {
exportReportToCSV,
exportToIntegration,
flagComment,
getConciergeReportID,
getCurrentUserAccountID,
getDraftPrivateNote,
getMostRecentReportID,
Expand Down
Loading
Loading