How do you expose an imperative clear() on a React Native Fabric component with codegenNativeCommands, and how does each platform receive it?
answer
- a Commands object exported from the spec
- first parameter is the component ref
- supportedCommands must match the interface
- iOS: handleCommand plus RCT<Name>HandleCommand
- Android: delegate's receiveCommand calls your manager
basics
~10 sDeclare a NativeCommands interface whose methods take the component ref first, export Commands = codegenNativeCommands<NativeCommands>({supportedCommands: ['clear']}) from the spec, and call Commands.clear(ref.current). Codegen routes it to the Android manager's clear(view) and iOS handleCommand:args:.
solid answer
~30 sIn the `SignaturePadNativeComponent.ts` spec, declare `interface NativeCommands { clear: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void; }` and export `Commands = codegenNativeCommands<NativeCommands>({supportedCommands: ['clear']})`; Codegen fails if `supportedCommands` and the interface disagree. JavaScript holds a ref to the pad and calls `Commands.clear(padRef.current)`, which dispatches the command to that specific native view and returns nothing — any result must come back as an event. On Android, the generated `SignaturePadManagerInterface` gains `clear(view)`, and the generated delegate's `receiveCommand` switch calls your manager's implementation. On iOS, the `RCTViewComponentView` subclass implements `handleCommand:args:` by calling the generated `RCTSignaturePadHandleCommand(self, commandName, args)`, which validates the command and its arguments before invoking `- (void)clear`.
code
typescript · 22 lines// specs/SignaturePadNativeComponent.ts (commands part)
import type * as React from 'react';
import type {HostComponent, ViewProps} from 'react-native';
import {codegenNativeCommands, codegenNativeComponent} from 'react-native';
export interface NativeProps extends ViewProps {}
interface NativeCommands {
clear: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
exportSignature: (
viewRef: React.ElementRef<HostComponent<NativeProps>>,
format: string,
) => void;
}
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['clear', 'exportSignature'],
});
export default codegenNativeComponent<NativeProps>(
'SignaturePad',
) as HostComponent<NativeProps>;go deeper
Remember that commands are imperative calls on one mounted native view, declared with codegenNativeCommands and called with the ref.
Walk through the spec interface, the ref-first rule, supportedCommands validation, and how the Android delegate and iOS helper route the call.
Design command-plus-event pairs for actions with results, choose props versus commands deliberately, and debug commands that reach one platform only.
Set API conventions for native components, such as state in props, actions in commands and results in events, so every component library behaves the same way.
## Why components need commands Props describe what a **Fabric native component** should look like; events report what happened. Some actions fit neither: "erase the signature now" is not a state you hold in a prop, and toggling a `clearedAt` prop to fake it is fragile. React Native's answer is **native commands** — imperative methods on a specific mounted view, declared in the component spec and generated by **Codegen** like everything else. ## Declaring commands in the spec Commands live in the same `SignaturePadNativeComponent.ts` file as the props: 1. Declare an interface, conventionally `NativeCommands`, with one method per command. 2. Make the **first parameter the component ref**, typed as `React.ElementRef<HostComponent<NativeProps>>`, as React Native's guide shows; Codegen rejects a command whose first argument is not a React ref type. 3. Add further parameters for arguments, using the same primitive types as props (`boolean`, `CodegenTypes.Int32`, `CodegenTypes.Double`, `string`). 4. Export the result of `codegenNativeCommands<NativeCommands>({supportedCommands: [...]})`, conventionally as `Commands`. Codegen checks the call: `supportedCommands` must list exactly the interface's methods, the options object is mandatory, and the interface must be a named type in the file rather than written inline. In React Native 0.87, import `codegenNativeCommands` from `'react-native'`; guides that deep-import it from `react-native/Libraries/Utilities/codegenNativeCommands` produce a type error under the default Strict TypeScript API. ## Calling a command from JavaScript ```tsx const padRef = useRef<React.ElementRef<typeof SignaturePad>>(null); <SignaturePad ref={padRef} style={{height: 240}} />; if (padRef.current) { Commands.clear(padRef.current); } ``` At runtime `codegenNativeCommands` builds an object whose methods call React Native's `dispatchCommand(ref, commandName, args)`. That has two consequences: - The command targets **the one mounted view** behind the ref, so two pads on a screen are cleared independently. - It is **fire-and-forget**: there is no return value and no Promise. An `exportSignature()` command reports its file through an event such as `onSignatureExported`. ## Receiving the command on each platform | Step | Android | iOS | |---|---|---| | Generated contract | `clear(view)` added to `SignaturePadManagerInterface` | `- (void)clear` on the generated view protocol | | Dispatch | Generated `SignaturePadManagerDelegate.receiveCommand` switches on the name and calls your manager | Your `handleCommand:args:` calls the generated `RCTSignaturePadHandleCommand(self, commandName, args)` | | Validation | The delegate reads typed arguments from the `ReadableArray` | The helper checks the command is supported and the arguments match before calling your method | | Your code | `override fun clear(view: SignaturePadView) { view.clear() }` in the manager | `- (void)clear { [_canvas clear]; }` in the component view | On Android the manager must return the generated delegate from `getDelegate()`; the base `ViewManager.receiveCommand` forwards to that delegate. On iOS the helper's name follows the pattern `RCT<ComponentName>HandleCommand` — `RCTCustomWebViewHandleCommand` in React Native's own guide. ## Command arguments Arguments after the ref are serialized with the command and read back natively in order: - **Supported shapes** include `boolean`, `string`, `CodegenTypes.Int32`, `CodegenTypes.Float`, `CodegenTypes.Double` and arrays; Android's generated delegate reads them from a `ReadableArray` by index, for example `args.getString(0)` for `exportSignature`'s `format`. - **Keep them few and primitive.** A command is a verb with a couple of options, not a way to ship data structures; large inputs belong in props. - **Validate natively.** The Android delegate trusts the types it reads, and a malformed value is a native error, so check ranges in your implementation. ## Commands versus alternatives - **Versus props:** use a prop for anything that is state ("is the pad disabled?"); use a command for a one-off action ("clear now"). - **Versus a Turbo Module:** a module method has no idea which of several pads to act on; a command is bound to one view through its ref. - **Versus events:** events flow native to JavaScript; commands flow JavaScript to native. A request with a result uses both. ## After changing commands Adding a command changes generated code on both platforms, so rerun Codegen — `pod install` on iOS, and the Android build (which runs the `generateCodegenArtifactsFromSchema` task) — before implementing the native side. ## Mistakes interviewers listen for - Expecting `Commands.clear(...)` to return a value or a Promise. - Calling a command with `padRef` instead of `padRef.current`, or before the view is mounted. - Listing a command in `supportedCommands` that the interface does not declare. - Implementing `clear` on iOS but forgetting `handleCommand:args:`, so the command never reaches it.
- How does an exportSignature command give JavaScript the resulting file?It cannot return it: commands dispatch to the native view and return nothing. The native implementation renders the strokes, writes the file, and emits an event such as `onSignatureExported` with `{uri}`, which the screen handles. If the export can fail, the event payload carries an outcome field, for example `result: 'success' | 'error'`.
- Why is a command better than a Turbo Module method for clear()?A command is dispatched through the component's ref, so it reaches exactly the mounted pad the ref points to. A module method has no built-in notion of which view instance to act on, and reaching into renderer-managed views from a module is unsupported.
saying these in an interview costs you the question
- Commands return a Promise with the native method's result
- A command can be called with the ref object itself rather than ref.current
- supportedCommands may list extra names that the interface does not declare
- On iOS, implementing - (void)clear is enough without handleCommand:args:
- A command broadcasts to every mounted instance of the component