Skip to content

Commit

Permalink
Merge pull request #56891 from Expensify/Rory-OnyxDerivedV2
Browse files Browse the repository at this point in the history
Onyx derived values
  • Loading branch information
luacmartins authored Feb 28, 2025
2 parents 0be2a40 + bf9c5e6 commit 84762db
Show file tree
Hide file tree
Showing 19 changed files with 292 additions and 51 deletions.
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
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`
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';
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
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]) => {
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, 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,
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
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;
}
}
}

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

0 comments on commit 84762db

Please sign in to comment.