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 configIf 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 iosFor development with live reload:
npx cap run android --livereload --external
npx cap run ios --livereload --externalThe --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.