skip to content

How do you expose an imperative clear() on a React Native Fabric component with codegenNativeCommands, and how does each platform receive it?

level: middleimportance: should knowfreq 25%

answer

  1. a Commands object exported from the spec
  2. first parameter is the component ref
  3. supportedCommands must match the interface
  4. iOS: handleCommand plus RCT<Name>HandleCommand
  5. Android: delegate's receiveCommand calls your manager

basics

~10 s

Declare 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 s

In 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
typescript
// 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

for a junior

Remember that commands are imperative calls on one mounted native view, declared with codegenNativeCommands and called with the ref.

for a middle

Walk through the spec interface, the ref-first rule, supportedCommands validation, and how the Android delegate and iOS helper route the call.

for a senior

Design command-plus-event pairs for actions with results, choose props versus commands deliberately, and debug commands that reach one platform only.

for a principal

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