Generates React Native components with platform-specific code, TypeScript support, and mobile-first patterns. Use when user asks to "create React Native component", "generate RN component", "scaffold mobile component", or "create cross-platform component".
Generates React Native components with platform-specific optimizations, TypeScript support, and mobile best practices.
Ask for:
Basic Functional Component:
// components/Button/Button.tsx
import React from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
TouchableOpacityProps,
ViewStyle,
TextStyle,
} from 'react-native';
interface ButtonProps extends TouchableOpacityProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
}
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
size = 'medium',
style,
...props
}) => {
return (
<TouchableOpacity
style={[
styles.button,
styles[variant],
styles[size],
style,
]}
onPress={onPress}
activeOpacity={0.7}
{...props}
>
<Text style={[styles.text, styles[`${variant}Text`]]}>
{title}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
primary: {
backgroundColor: '#007AFF',
},
secondary: {
backgroundColor: '#6c757d',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#007AFF',
},
small: {
paddingVertical: 8,
paddingHorizontal: 16,
},
medium: {
paddingVertical: 12,
paddingHorizontal: 24,
},
large: {
paddingVertical: 16,
paddingHorizontal: 32,
},
text: {
fontSize: 16,
fontWeight: '600',
},
primaryText: {
color: '#ffffff',
},
secondaryText: {
color: '#ffffff',
},
outlineText: {
color: '#007AFF',
},
});
Component with State and Hooks:
// components/SearchInput/SearchInput.tsx
import React, { useState, useCallback, useEffect } from 'react';
import {
TextInput,
View,
StyleSheet,
TextInputProps,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
interface SearchInputProps extends Omit<TextInputProps, 'onChangeText'> {
onSearch: (query: string) => void;
onClear?: () => void;
debounceMs?: number;
loading?: boolean;
}
export const SearchInput: React.FC<SearchInputProps> = ({
onSearch,
onClear,
debounceMs = 300,
loading = false,
placeholder = 'Search...',
...props
}) => {
const [query, setQuery] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
if (query) {
onSearch(query);
}
}, debounceMs);
return () => clearTimeout(timer);
}, [query, debounceMs, onSearch]);
const handleClear = useCallback(() => {
setQuery('');
onClear?.();
}, [onClear]);
return (
<View style={styles.container}>
<Ionicons name="search" size={20} color="#666" style={styles.icon} />
<TextInput
style={styles.input}
value={query}
onChangeText={setQuery}
placeholder={placeholder}
placeholderTextColor="#999"
autoCapitalize="none"
autoCorrect={false}
{...props}
/>
{loading && <ActivityIndicator size="small" color="#007AFF" />}
{query.length > 0 && !loading && (
<TouchableOpacity onPress={handleClear}>
<Ionicons name="close-circle" size={20} color="#666" />
</TouchableOpacity>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f0f0f0',
borderRadius: 10,
paddingHorizontal: 12,
height: 44,
},
icon: {
marginRight: 8,
},
input: {
flex: 1,
fontSize: 16,
color: '#000',
},
});
Using Platform Module:
// components/Header/Header.tsx
import React from 'react';
import { View, Text, StyleSheet, Platform, StatusBar } from 'react-native';
interface HeaderProps {
title: string;
}
export const Header: React.FC<HeaderProps> = ({ title }) => {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
paddingTop: Platform.select({
ios: 50,
android: StatusBar.currentHeight || 0,
default: 0,
}),
paddingBottom: 10,
paddingHorizontal: 16,
backgroundColor: '#007AFF',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#fff',
...Platform.select({
ios: {
fontFamily: 'System',
},
android: {
fontFamily: 'Roboto',
},
}),
},
});
Using Platform-Specific Files:
// components/DatePicker/DatePicker.ios.tsx
import React from 'react';
import { DatePickerIOS } from '@react-native-community/datetimepicker';
interface DatePickerProps {
date: Date;
onDateChange: (date: Date) => void;
}
export const DatePicker: React.FC<DatePickerProps> = ({ date, onDateChange }) => {
return <DatePickerIOS date={date} onDateChange={onDateChange} mode="date" />;
};
// components/DatePicker/DatePicker.android.tsx
import React, { useState } from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';
interface DatePickerProps {
date: Date;
onDateChange: (date: Date) => void;
}
export const DatePicker: React.FC<DatePickerProps> = ({ date, onDateChange }) => {
const [show, setShow] = useState(false);
const onChange = (_event: any, selectedDate?: Date) => {
setShow(false);
if (selectedDate) {
onDateChange(selectedDate);
}
};
return (
<>
<TouchableOpacity onPress={() => setShow(true)} style={styles.button}>
<Text>{date.toLocaleDateString()}</Text>
</TouchableOpacity>
{show && (
<DateTimePicker value={date} mode="date" display="default" onChange={onChange} />
)}
</>
);
};
const styles = StyleSheet.create({
button: {
padding: 12,
backgroundColor: '#f0f0f0',
borderRadius: 8,
},
});
// components/DatePicker/index.ts
export { DatePicker } from './DatePicker';
Basic Animation:
// components/FadeIn/FadeIn.tsx
import React, { useEffect, useRef } from 'react';
import { Animated, ViewProps } from 'react-native';
interface FadeInProps extends ViewProps {
duration?: number;
delay?: number;
}
export const FadeIn: React.FC<FadeInProps> = ({
children,
duration = 300,
delay = 0,
style,
...props
}) => {
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration,
delay,
useNativeDriver: true,
}).start();
}, [fadeAnim, duration, delay]);
return (
<Animated.View style={[style, { opacity: fadeAnim }]} {...props}>
{children}
</Animated.View>
);
};
Complex Animation:
// components/SlideUp/SlideUp.tsx
import React, { useEffect, useRef } from 'react';
import { Animated, Dimensions } from 'react-native';
const { height } = Dimensions.get('window');
interface SlideUpProps {
visible: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const SlideUp: React.FC<SlideUpProps> = ({ visible, onClose, children }) => {
const slideAnim = useRef(new Animated.Value(height)).current;
useEffect(() => {
if (visible) {
Animated.spring(slideAnim, {
toValue: 0,
useNativeDriver: true,
tension: 65,
friction: 11,
}).start();
} else {
Animated.timing(slideAnim, {
toValue: height,
duration: 250,
useNativeDriver: true,
}).start();
}
}, [visible, slideAnim]);
if (!visible) return null;
return (
<Animated.View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
transform: [{ translateY: slideAnim }],
}}
>
{children}
</Animated.View>
);
};
Optimized FlatList:
// components/UserList/UserList.tsx
import React, { useCallback } from 'react';
import {
FlatList,
View,
Text,
StyleSheet,
ActivityIndicator,
RefreshControl,
} from 'react-native';
interface User {
id: string;
name: string;
email: string;
}
interface UserListProps {
users: User[];
loading?: boolean;
onRefresh?: () => void;
onEndReached?: () => void;
refreshing?: boolean;
}
export const UserList: React.FC<UserListProps> = ({
users,
loading,
onRefresh,
onEndReached,
refreshing = false,
}) => {
const renderItem = useCallback(({ item }: { item: User }) => (
<View style={styles.item}>
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.email}>{item.email}</Text>
</View>
), []);
const keyExtractor = useCallback((item: User) => item.id, []);
const renderFooter = useCallback(() => {
if (!loading) return null;
return (
<View style={styles.footer}>
<ActivityIndicator size="small" color="#007AFF" />
</View>
);
}, [loading]);
const renderEmpty = useCallback(() => (
<View style={styles.empty}>
<Text style={styles.emptyText}>No users found</Text>
</View>
), []);
return (
<FlatList
data={users}
renderItem={renderItem}
keyExtractor={keyExtractor}
ListFooterComponent={renderFooter}
ListEmptyComponent={renderEmpty}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
refreshControl={
onRefresh ? (
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
) : undefined
}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={5}
/>
);
};
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
name: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
email: {
fontSize: 14,
color: '#666',
},
footer: {
paddingVertical: 20,
alignItems: 'center',
},
empty: {
padding: 40,
alignItems: 'center',
},
emptyText: {
fontSize: 16,
color: '#999',
},
});
Input with Validation:
// components/TextInputField/TextInputField.tsx
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, TextInputProps } from 'react-native';
interface TextInputFieldProps extends TextInputProps {
label: string;
error?: string;
required?: boolean;
validate?: (value: string) => string | undefined;
}
export const TextInputField: React.FC<TextInputFieldProps> = ({
label,
error,
required,
validate,
onBlur,
onChangeText,
...props
}) => {
const [localError, setLocalError] = useState<string>();
const handleBlur = (e: any) => {
if (validate && props.value) {
setLocalError(validate(props.value));
}
onBlur?.(e);
};
const handleChange = (text: string) => {
setLocalError(undefined);
onChangeText?.(text);
};
const displayError = error || localError;
return (
<View style={styles.container}>
<Text style={styles.label}>
{label}
{required && <Text style={styles.required}> *</Text>}
</Text>
<TextInput
style={[styles.input, displayError && styles.inputError]}
onBlur={handleBlur}
onChangeText={handleChange}
{...props}
/>
{displayError && <Text style={styles.errorText}>{displayError}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
required: {
color: '#ff0000',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
inputError: {
borderColor: '#ff0000',
},
errorText: {
color: '#ff0000',
fontSize: 12,
marginTop: 4,
},
});
Using Native Modules:
// components/BiometricAuth/BiometricAuth.tsx
import React from 'react';
import { Alert, Platform } from 'react-native';
import TouchID from 'react-native-touch-id';
import ReactNativeBiometrics from 'react-native-biometrics';
interface BiometricAuthProps {
onSuccess: () => void;
onError: (error: string) => void;
}
export const useBiometricAuth = ({ onSuccess, onError }: BiometricAuthProps) => {
const authenticate = async () => {
try {
if (Platform.OS === 'ios') {
const isSupported = await TouchID.isSupported();
if (!isSupported) {
onError('Biometric authentication not supported');
return;
}
await TouchID.authenticate('Authenticate to continue', {
fallbackLabel: 'Use Passcode',
});
onSuccess();
} else {
const rnBiometrics = new ReactNativeBiometrics();
const { available } = await rnBiometrics.isSensorAvailable();
if (!available) {
onError('Biometric authentication not available');
return;
}
const { success } = await rnBiometrics.simplePrompt({
promptMessage: 'Authenticate to continue',
});
if (success) {
onSuccess();
} else {
onError('Authentication failed');
}
}
} catch (error: any) {
onError(error.message);
}
};
return { authenticate };
};
Using Dimensions:
// components/ResponsiveCard/ResponsiveCard.tsx
import React from 'react';
import { View, Text, StyleSheet, Dimensions, ScaledSize } from 'react-native';
import { useDeviceOrientation } from '@react-native-community/hooks';
const getStyles = (window: ScaledSize) => {
const { width } = window;
const isTablet = width >= 768;
return StyleSheet.create({
card: {
padding: isTablet ? 24 : 16,
margin: isTablet ? 16 : 8,
backgroundColor: '#fff',
borderRadius: isTablet ? 12 : 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: {
fontSize: isTablet ? 24 : 18,
fontWeight: 'bold',
marginBottom: isTablet ? 12 : 8,
},
});
};
export const ResponsiveCard: React.FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => {
const window = Dimensions.get('window');
const styles = getStyles(window);
return (
<View style={styles.card}>
<Text style={styles.title}>{title}</Text>
{children}
</View>
);
};
Create barrel export:
// components/index.ts
export { Button } from './Button/Button';
export { SearchInput } from './SearchInput/SearchInput';
export { Header } from './Header/Header';
export { DatePicker } from './DatePicker';
export { FadeIn } from './FadeIn/FadeIn';
export { SlideUp } from './SlideUp/SlideUp';
export { UserList } from './UserList/UserList';
export { TextInputField } from './TextInputField/TextInputField';
export { ResponsiveCard } from './ResponsiveCard/ResponsiveCard';
Component Test:
// components/Button/__tests__/Button.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Button } from '../Button';
describe('Button', () => {
it('renders correctly', () => {
const { getByText } = render(<Button title="Click me" onPress={() => {}} />);
expect(getByText('Click me')).toBeTruthy();
});
it('calls onPress when pressed', () => {
const onPress = jest.fn();
const { getByText } = render(<Button title="Click me" onPress={onPress} />);
fireEvent.press(getByText('Click me'));
expect(onPress).toHaveBeenCalledTimes(1);
});
it('applies correct variant styles', () => {
const { getByText, rerender } = render(
<Button title="Primary" onPress={() => {}} variant="primary" />
);
let button = getByText('Primary').parent;
expect(button?.props.style).toContainEqual(
expect.objectContaining({ backgroundColor: '#007AFF' })
);
rerender(<Button title="Outline" onPress={() => {}} variant="outline" />);
button = getByText('Outline').parent;
expect(button?.props.style).toContainEqual(
expect.objectContaining({ backgroundColor: 'transparent' })
);
});
});
DO:
DON'T:
npx skills add Dexploarer/react-native-component-generator下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer