| name | add-sheet |
| description | Use when adding, registering, or debugging a bottom sheet (action sheet) in the mobile app, including when a sheet's cancel or dismissal reaches the caller as the wrong result. |
| argument-hint | ["sheet-name"] |
Add Sheet
Registry, show call and mount lifecycle: CLAUDE.md "Sheets (Bottom Sheets)". This file adds
the procedure plus the result convention new sheets here should follow.
Resolve with an object, gate on a named field
react-native-actions-sheet 0.9.7, node_modules/react-native-actions-sheet/dist/src/index.js:408:
actionSheetEventManager.publish("onclose_".concat(sheetId), data || payloadRef.current || data, currentContext);
data is what you passed to SheetManager.hide(id, { payload: data }). payloadRef.current is
NOT the show payload: it tracks <ActionSheet>'s own payload prop (payload = _a.payload at
index.js:53, useRef(payload) at 87, payloadRef.current = payload at 139). The
SheetManager.show payload goes somewhere else entirely, to the registered component as a prop
from the provider (<Sheet sheetId={id} payload={payload}/>, dist/src/provider.js:160).
Sheets in src/ do not forward that prop down into <ActionSheet>: grep -rn "payload=" src/
returns zero hits. So payloadRef.current is undefined for the sheets here.
data || payloadRef.current || data collapses to data, so a falsy return does reach the caller
intact today. Same expression on onBeforeClose (line 385) and onClose (line 401).
Still resolve with an object and gate on a named field. Two reasons:
- A backdrop tap, swipe down or hardware back closes with
data === undefined, so show()
resolves undefined. Truthiness cannot separate that dismissal from a sheet that deliberately
answered false, 0 or ''. A named field can.
- The substitution is one prop away from going live. Adding
payload={payload} to an
<ActionSheet> would silently turn every falsy cancel in that sheet into the truthy show
payload, with no type error and no crash.
Copy modNotesSheet, communityRoleEditSheet, walletHistoryFiltersSheet or
newsletterDigestSheet. All four resolve { cancelled: true } on cancel.
searchFiltersSheet is apply-only, with no cancel control, so it is not a model here.
src/components/authUpgradeSheet/authUpgradeSheet.tsx is one counter-example: it cancels with
_close(false) while src/providers/sdk/mobilePlatformAdapter.ts:317 gates on
if (!result) return false;. That reads correctly right now, since cancel and dismissal are both
falsy there and both mean the same thing, but it is one of the sheets that breaks first if anyone
gives it a payload prop.
Comments across src/ (in sheets.tsx, in several sheet components, in several screens) justify
this convention by claiming a dismissal resolves the payload object. The convention is right; that
reason is not.
Step 1: Component
src/components/<sheetName>/<sheetName>.tsx is the usual path for a component typed with
SheetProps<'...'>. Other sheets use the enum form SheetProps<SheetNames.X>, which is equally
accepted. Trimmed from src/components/modNotesSheet/modNotesSheet.tsx:
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import ActionSheet, { SheetManager, SheetProps } from 'react-native-actions-sheet';
import EStyleSheet from 'react-native-extended-stylesheet';
import { MainButton } from '../mainButton';
const FALLBACK_SHEET_ID = 'my_sheet';
export interface MySheetResult {
value?: string;
cancelled?: boolean;
}
const MySheet: React.FC<SheetProps<'my_sheet'>> = ({ sheetId, payload }) => {
const intl = useIntl();
const [value, setValue] = useState('');
closedRef = ();
_reset = ( {
closedRef. = ;
();
}, []);
( {
();
}, [payload, _reset]);
= () => {
(closedRef.) ;
closedRef. = ;
.(sheetId || , { : result });
};
(
);
};
styles = .({
: { : , : },
});
;
Colors come from EStyleSheet theme variables in src/themes/ ($primaryBackgroundColor,
$primaryBlack, $primaryDarkGray, $iconColor) rather than a hex literal. For a color a prop
needs as a plain string rather than a style, resolve it with EStyleSheet.value('$primaryDarkGray'), as
modNotesSheet does for placeholderTextColor.
Step 2: Folder index
src/components/<sheetName>/index.ts. Sheet folders re-export their sheet with the first line;
those that also publish a result type add the second:
export { default as MySheet } from './<sheetName>';
export type { MySheetResult } from './<sheetName>';
A sheet that sheets.tsx imports by path can skip this file entirely. walletHistoryFiltersSheet
has no index.ts.
Step 3: Components barrel
src/components/index.tsx is an import list plus one export { ... } block at line 164. Add
import { MySheet } from './<sheetName>'; plus a MySheet, entry inside that block. There is no
export ... from line to add. A sheet nothing else imports can skip this step and be imported
in sheets.tsx by path, as some registrations are.
Step 4: Register in src/navigation/sheets.tsx
Add the SheetNames member (MY_SHEET = 'my_sheet',), the
registerSheet(SheetNames.MY_SHEET, MySheet); call, then extend Sheets. The key must be a
string literal, not [SheetNames.MY_SHEET]: string enum member types are nominal, so with a
computed key keyof Sheets carries the enum member rather than the string literal and
SheetProps<'my_sheet'> stops resolving.
declare module 'react-native-actions-sheet' {
interface Sheets {
my_sheet: SheetDefinition<{
payload: { someParam: string };
returnValue: MySheetResult | undefined;
}>;
}
}
Skipping this fails yarn typecheck: the everySheetHasDefinition assertion at the bottom of
the file names any SheetNames member missing from the augmentation.
Step 5: Show it and read the result
const result = await SheetManager.show(SheetNames.MY_SHEET, { payload: { someParam: 'x' } });
const confirmed = typeof result?.value === 'string' ? result.value : '';
if (!confirmed) return;
Real callers: src/components/postOptionsModal/container/postOptionsModal.tsx:742
(typeof result?.notes === 'string') and
src/screens/assetDetails/screen/assetDetailsScreen.tsx:265 (!Array.isArray(result?.operations)).
Step 6: i18n strings
Edit src/config/locales/en-US.json only; Crowdin owns the other locales. The file is nested
objects, not dotted keys, while formatMessage ids stay dotted:
"my_sheet": { "title": "Sheet Title", "confirm": "Confirm", "cancel": "Cancel" },
Lifecycle
- Sheets mount on a fresh show and unmount on hide: the library renders the registered
component only while that sheet is visible.
useState initials are fresh on that open, so
nothing stale needs clearing on mount; unmount cleanups run on close. The exception is a
show() against a sheet that is already open: the stored payload is replaced with no remount,
so reset on the payload prop as well, as Step 1 does. The CLAUDE.md bullet states the mount
rule without that exception.
- Sheets render outside the ErrorBoundary:
SheetProvider returns <>{children}{sheets}</>,
so sheets are siblings of <Application/> while the boundary sits inside it
(src/screens/application/index.tsx:17). A throw in a sheet render or effect cleanup is fatal.
Be careful with native or Expo shared objects in cleanups.
- The payload is captured at show time: it is stored in state at that show, so a callback
passed inside a payload keeps the closure it had then. Route anything that changes through a
ref, as
src/components/quickPostModal/quickPostModalContent.tsx:565-575 does.