capacitor-react
Capacitor React
Develop Capacitor apps with React — project structure, hooks, state management, and React-specific patterns for accessing native device features.
Prerequisites
- Capacitor 6, 7, or 8 app with React.
- Node.js and npm installed.
- React 18 or later.
- For iOS: Xcode installed.
- For Android: Android Studio installed.
Agent Behavior
- Auto-detect before asking. Check the project for
package.jsondependencies (react,react-dom,@capacitor/core), platforms (android/,ios/), build tools (vite.config.ts,next.config.js,webpack.config.js), and TypeScript usage. Only ask the user when something cannot be detected. - Guide step-by-step. Walk the user through the process one step at a time. Never present multiple unrelated questions at once.
- Adapt to the project. Detect the existing code style (functional vs. class components, TypeScript vs. JavaScript, CSS modules vs. styled-components) and generate code that matches.
Procedures
Step 1: Analyze the Project
Auto-detect the following by reading project files:
- Framework variant: Check if this is a plain React app (Vite/CRA), Next.js, or Remix by examining
package.jsondependencies and config files (vite.config.ts,next.config.js,remix.config.js). - Capacitor version: Read
@capacitor/coreversion frompackage.json. - React version: Read
reactversion frompackage.json. - TypeScript: Check if
tsconfig.jsonexists and if.tsxfiles are used. - Platforms: Check which directories exist (
android/,ios/). - Capacitor config format: Check if the project uses
capacitor.config.tsorcapacitor.config.json. - State management: Check
package.jsonforredux,@reduxjs/toolkit,zustand,jotai,@tanstack/react-query, or similar. - Router: Check
package.jsonforreact-router-dom,@tanstack/react-router, or similar.
Step 2: Project Structure
A standard Capacitor React project follows this structure:
project-root/
├── android/ # Android native project (generated by Capacitor)
├── ios/ # iOS native project (generated by Capacitor)
├── public/
├── src/
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks (including native feature hooks)
│ ├── pages/ # Page/route components
│ ├── services/ # Service modules for Capacitor plugin calls
│ ├── App.tsx # Root component
│ └── main.tsx # Entry point
├── capacitor.config.ts # Capacitor configuration
├── package.json
├── tsconfig.json
└── vite.config.ts # Or other bundler config
If the project does not follow this structure, adapt all guidance to the project's actual directory layout. Do not restructure the project unless the user explicitly asks.
Step 3: Using Capacitor Plugins in React
Read references/plugin-usage-patterns.md for detailed patterns on how to use Capacitor plugins in React components and hooks.
Key principles:
- Import plugins directly — Capacitor plugins are imported as ES modules.
- Call plugin methods in event handlers or effects — never at the module top level.
- Use
useEffectfor listeners — register and clean up Capacitor event listeners insideuseEffect. - Check platform before calling — use
Capacitor.isNativePlatform()orCapacitor.getPlatform()to guard platform-specific calls.
Step 4: Custom Hooks for Native Features
Read references/custom-hooks.md for reusable custom hook patterns that wrap Capacitor plugins.
Custom hooks encapsulate native feature access and provide a React-idiomatic API. When the user needs to access a native feature from multiple components, create a custom hook in src/hooks/ (or wherever the project keeps hooks).
Step 5: State Management with Native Data
When the project uses a state management library, integrate native data as follows:
- React Query / TanStack Query: Use query functions that call Capacitor plugins. This works well for data that is fetched from native APIs (e.g., device info, contacts, filesystem reads).
- Redux / Zustand / Jotai: Dispatch actions or update atoms from Capacitor plugin callbacks. Keep native API calls in action creators or service modules, not in reducers or stores.
- No state library: Use React context or custom hooks with
useState/useReducerto share native data across components.
Do not recommend adding a state management library unless the user's requirements justify it.
Step 6: Navigation and Deep Links
If the project uses react-router-dom or another router:
- Deep links: Register a listener for
appUrlOpenevents from the@capacitor/appplugin inside auseEffectin the root component or a dedicated hook. Navigate programmatically using the router'suseNavigate()hook.
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { App, URLOpenListenerEvent } from '@capacitor/app';
const useDeepLinks = () => {
const navigate = useNavigate();
useEffect(() => {
const listener = App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
const path = new URL(event.url).pathname;
navigate(path);
});
return () => {
listener.then(handle => handle.remove());
};
}, [navigate]);
};
- Back button handling (Android): Register a listener for the
backButtonevent from the@capacitor/appplugin to handle Android hardware back button presses.
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { App } from '@capacitor/app';
const useBackButton = () => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const listener = App.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
navigate(-1);
} else {
App.exitApp();
}
});
return () => {
listener.then(handle => handle.remove());
};
}, [navigate, location]);
};
Step 7: Platform-Specific Rendering
Use Capacitor.getPlatform() or Capacitor.isNativePlatform() to conditionally render components or apply platform-specific behavior:
import { Capacitor } from '@capacitor/core';
const MyComponent: React.FC = () => {
const platform = Capacitor.getPlatform(); // 'ios' | 'android' | 'web'
const isNative = Capacitor.isNativePlatform();
return (
<div>
{platform === 'ios' && <IOSSpecificComponent />}
{platform === 'android' && <AndroidSpecificComponent />}
{!isNative && <WebFallbackComponent />}
</div>
);
};
For reusable platform checks, create a utility or hook:
import { Capacitor } from '@capacitor/core';
export const usePlatform = () => {
return {
platform: Capacitor.getPlatform(),
isNative: Capacitor.isNativePlatform(),
isIOS: Capacitor.getPlatform() === 'ios',
isAndroid: Capacitor.getPlatform() === 'android',
isWeb: Capacitor.getPlatform() === 'web',
};
};
Step 8: Lifecycle and App State
Use the @capacitor/app plugin to respond to app lifecycle events in React:
import { useEffect } from 'react';
import { App } from '@capacitor/app';
const useAppState = (onResume?: () => void, onPause?: () => void) => {
useEffect(() => {
const resumeListener = App.addListener('resume', () => {
onResume?.();
});
const pauseListener = App.addListener('pause', () => {
onPause?.();
});
return () => {
resumeListener.then(handle => handle.remove());
pauseListener.then(handle => handle.remove());
};
}, [onResume, onPause]);
};
Use this to refresh data when the app returns to the foreground, pause media, or save state when the app is backgrounded.
Step 9: Build and Run
After implementing changes:
npm run build
npx cap sync
npx cap run android
npx cap run ios
For development with live reload:
npx cap run android --livereload --external
npx cap run ios --livereload --external
The --external flag makes the dev server accessible from the device/emulator. The --livereload flag enables automatic reloads when source files change.
Error Handling
- Plugin not found at runtime: Ensure
npx cap syncwas run after installing a plugin. Verify the plugin is listed inpackage.jsondependencies. Capacitor is not defined: The@capacitor/corepackage must be installed. Runnpm install @capacitor/core.- Native method fails on web: Guard native-only calls with
Capacitor.isNativePlatform(). Many plugins have web implementations, but some (e.g.,@capacitor/camerawith native UI) only work on iOS/Android. - Event listener memory leak: Always return a cleanup function from
useEffectthat callsremove()on the listener handle. Failing to do so causes duplicate listeners on re-renders. - Stale closure in event listener: If a Capacitor event listener references React state that changes over time, use a
useRefto hold the latest value, or add the state variable to theuseEffectdependency array and re-register the listener. - Live reload not connecting: Ensure the device and development machine are on the same network. Check that the
--externalflag is used withnpx cap run. Verify no firewall is blocking the dev server port. - Build works on web but fails on native: Check for browser-only APIs (
window.localStorage,navigator.geolocation) used without Capacitor alternatives. Use Capacitor plugins (@capacitor/preferences,@capacitor/geolocation) instead. - React strict mode double-mounting: In development, React 18 strict mode mounts components twice. This can cause duplicate Capacitor event listeners. Ensure cleanup functions properly remove listeners — the double-mount behavior validates that cleanup works correctly.
Related Skills
ionic-react— Ionic Framework-specific React patterns (IonReactRouter, lifecycle hooks, overlay hooks) for apps using@ionic/react.capacitor-angular— Angular-specific patterns and best practices for Capacitor app development.capacitor-app-upgrades— Upgrade a Capacitor app to a newer major version.capacitor-plugins— Install, configure, and use Capacitor plugins from official and community sources.capacitor-push-notifications— Set up push notifications with Firebase Cloud Messaging in a Capacitor app.