gluestack-components
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesegluestack-ui - Components
gluestack-ui - 组件
Expert knowledge of gluestack-ui's universal component library for building accessible, performant UI across React and React Native platforms.
精通gluestack-ui的通用组件库,可用于在React和React Native平台上构建无障碍、高性能的UI。
Overview
概述
gluestack-ui provides 50+ unstyled, accessible components that work seamlessly on web and mobile. Components are copy-pasteable into your project and styled with NativeWind (Tailwind CSS for React Native).
gluestack-ui 提供50+个无样式、无障碍的组件,可在网页和移动平台上无缝运行。组件可直接复制粘贴到你的项目中,并通过NativeWind(React Native版Tailwind CSS)进行样式设置。
Key Concepts
核心概念
Component Installation
组件安装
Add components using the CLI:
bash
undefined通过CLI添加组件:
bash
undefinedInitialize gluestack-ui in your project
Initialize gluestack-ui in your project
npx gluestack-ui init
npx gluestack-ui init
Add individual components
Add individual components
npx gluestack-ui add button
npx gluestack-ui add input
npx gluestack-ui add modal
npx gluestack-ui add button
npx gluestack-ui add input
npx gluestack-ui add modal
Add multiple components
Add multiple components
npx gluestack-ui add button input select modal
npx gluestack-ui add button input select modal
Add all components
Add all components
npx gluestack-ui add --all
undefinednpx gluestack-ui add --all
undefinedComponent Anatomy
组件结构
Every gluestack-ui component follows a consistent pattern:
tsx
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';
// Components are composable with sub-components
<Button size="md" variant="solid" action="primary">
<ButtonIcon as={PlusIcon} />
<ButtonText>Add Item</ButtonText>
<ButtonSpinner />
</Button>每个gluestack-ui组件都遵循一致的模式:
tsx
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';
// Components are composable with sub-components
<Button size="md" variant="solid" action="primary">
<ButtonIcon as={PlusIcon} />
<ButtonText>Add Item</ButtonText>
<ButtonSpinner />
</Button>Variants, Sizes, and Actions
变体、尺寸和操作类型
Components support consistent prop APIs:
tsx
// Button variants
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="link">Link</Button>
// Button sizes
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
// Button actions (semantic colors)
<Button action="primary">Primary</Button>
<Button action="secondary">Secondary</Button>
<Button action="positive">Positive</Button>
<Button action="negative">Negative</Button>组件支持统一的属性API:
tsx
// Button variants
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="link">Link</Button>
// Button sizes
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
// Button actions (semantic colors)
<Button action="primary">Primary</Button>
<Button action="secondary">Secondary</Button>
<Button action="positive">Positive</Button>
<Button action="negative">Negative</Button>Core Components
核心组件
Button
按钮(Button)
Interactive button with loading states and icons:
tsx
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';
import { SaveIcon } from 'lucide-react-native';
function SaveButton({ isLoading, onPress }: { isLoading: boolean; onPress: () => void }) {
return (
<Button
size="md"
variant="solid"
action="primary"
isDisabled={isLoading}
onPress={onPress}
>
{isLoading ? (
<ButtonSpinner />
) : (
<ButtonIcon as={SaveIcon} />
)}
<ButtonText>{isLoading ? 'Saving...' : 'Save'}</ButtonText>
</Button>
);
}带有加载状态和图标的交互式按钮:
tsx
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';
import { SaveIcon } from 'lucide-react-native';
function SaveButton({ isLoading, onPress }: { isLoading: boolean; onPress: () => void }) {
return (
<Button
size="md"
variant="solid"
action="primary"
isDisabled={isLoading}
onPress={onPress}
>
{isLoading ? (
<ButtonSpinner />
) : (
<ButtonIcon as={SaveIcon} />
)}
<ButtonText>{isLoading ? 'Saving...' : 'Save'}</ButtonText>
</Button>
);
}Input
输入框(Input)
Text input with labels and validation:
tsx
import {
FormControl,
FormControlLabel,
FormControlLabelText,
FormControlHelper,
FormControlHelperText,
FormControlError,
FormControlErrorIcon,
FormControlErrorText,
} from '@/components/ui/form-control';
import { Input, InputField, InputSlot, InputIcon } from '@/components/ui/input';
import { AlertCircleIcon, MailIcon } from 'lucide-react-native';
function EmailInput({ value, onChange, error }: {
value: string;
onChange: (text: string) => void;
error?: string;
}) {
return (
<FormControl isInvalid={!!error}>
<FormControlLabel>
<FormControlLabelText>Email</FormControlLabelText>
</FormControlLabel>
<Input variant="outline" size="md">
<InputSlot pl="$3">
<InputIcon as={MailIcon} />
</InputSlot>
<InputField
placeholder="Enter your email"
value={value}
onChangeText={onChange}
keyboardType="email-address"
autoCapitalize="none"
/>
</Input>
{error ? (
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
) : (
<FormControlHelper>
<FormControlHelperText>We'll never share your email</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
);
}带有标签和验证功能的文本输入框:
tsx
import {
FormControl,
FormControlLabel,
FormControlLabelText,
FormControlHelper,
FormControlHelperText,
FormControlError,
FormControlErrorIcon,
FormControlErrorText,
} from '@/components/ui/form-control';
import { Input, InputField, InputSlot, InputIcon } from '@/components/ui/input';
import { AlertCircleIcon, MailIcon } from 'lucide-react-native';
function EmailInput({ value, onChange, error }: {
value: string;
onChange: (text: string) => void;
error?: string;
}) {
return (
<FormControl isInvalid={!!error}>
<FormControlLabel>
<FormControlLabelText>Email</FormControlLabelText>
</FormControlLabel>
<Input variant="outline" size="md">
<InputSlot pl="$3">
<InputIcon as={MailIcon} />
</InputSlot>
<InputField
placeholder="Enter your email"
value={value}
onChangeText={onChange}
keyboardType="email-address"
autoCapitalize="none"
/>
</Input>
{error ? (
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
) : (
<FormControlHelper>
<FormControlHelperText>We'll never share your email</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
);
}Select
选择器(Select)
Dropdown selection component:
tsx
import {
Select,
SelectTrigger,
SelectInput,
SelectIcon,
SelectPortal,
SelectBackdrop,
SelectContent,
SelectDragIndicatorWrapper,
SelectDragIndicator,
SelectItem,
} from '@/components/ui/select';
import { ChevronDownIcon } from 'lucide-react-native';
function CountrySelect({ value, onValueChange }: {
value: string;
onValueChange: (value: string) => void;
}) {
return (
<Select selectedValue={value} onValueChange={onValueChange}>
<SelectTrigger variant="outline" size="md">
<SelectInput placeholder="Select country" />
<SelectIcon as={ChevronDownIcon} mr="$3" />
</SelectTrigger>
<SelectPortal>
<SelectBackdrop />
<SelectContent>
<SelectDragIndicatorWrapper>
<SelectDragIndicator />
</SelectDragIndicatorWrapper>
<SelectItem label="United States" value="us" />
<SelectItem label="Canada" value="ca" />
<SelectItem label="United Kingdom" value="uk" />
<SelectItem label="Australia" value="au" />
</SelectContent>
</SelectPortal>
</Select>
);
}下拉选择组件:
tsx
import {
Select,
SelectTrigger,
SelectInput,
SelectIcon,
SelectPortal,
SelectBackdrop,
SelectContent,
SelectDragIndicatorWrapper,
SelectDragIndicator,
SelectItem,
} from '@/components/ui/select';
import { ChevronDownIcon } from 'lucide-react-native';
function CountrySelect({ value, onValueChange }: {
value: string;
onValueChange: (value: string) => void;
}) {
return (
<Select selectedValue={value} onValueChange={onValueChange}>
<SelectTrigger variant="outline" size="md">
<SelectInput placeholder="Select country" />
<SelectIcon as={ChevronDownIcon} mr="$3" />
</SelectTrigger>
<SelectPortal>
<SelectBackdrop />
<SelectContent>
<SelectDragIndicatorWrapper>
<SelectDragIndicator />
</SelectDragIndicatorWrapper>
<SelectItem label="United States" value="us" />
<SelectItem label="Canada" value="ca" />
<SelectItem label="United Kingdom" value="uk" />
<SelectItem label="Australia" value="au" />
</SelectContent>
</SelectPortal>
</Select>
);
}Modal
模态框(Modal)
Dialog overlay component:
tsx
import {
Modal,
ModalBackdrop,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
} from '@/components/ui/modal';
import { Heading } from '@/components/ui/heading';
import { Text } from '@/components/ui/text';
import { Button, ButtonText } from '@/components/ui/button';
import { CloseIcon, Icon } from '@/components/ui/icon';
function ConfirmModal({ isOpen, onClose, onConfirm, title, message }: {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
}) {
return (
<Modal isOpen={isOpen} onClose={onClose} size="md">
<ModalBackdrop />
<ModalContent>
<ModalHeader>
<Heading size="lg">{title}</Heading>
<ModalCloseButton>
<Icon as={CloseIcon} />
</ModalCloseButton>
</ModalHeader>
<ModalBody>
<Text>{message}</Text>
</ModalBody>
<ModalFooter>
<Button variant="outline" action="secondary" onPress={onClose}>
<ButtonText>Cancel</ButtonText>
</Button>
<Button action="negative" onPress={onConfirm}>
<ButtonText>Delete</ButtonText>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}对话框覆盖层组件:
tsx
import {
Modal,
ModalBackdrop,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
} from '@/components/ui/modal';
import { Heading } from '@/components/ui/heading';
import { Text } from '@/components/ui/text';
import { Button, ButtonText } from '@/components/ui/button';
import { CloseIcon, Icon } from '@/components/ui/icon';
function ConfirmModal({ isOpen, onClose, onConfirm, title, message }: {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
}) {
return (
<Modal isOpen={isOpen} onClose={onClose} size="md">
<ModalBackdrop />
<ModalContent>
<ModalHeader>
<Heading size="lg">{title}</Heading>
<ModalCloseButton>
<Icon as={CloseIcon} />
</ModalCloseButton>
</ModalHeader>
<ModalBody>
<Text>{message}</Text>
</ModalBody>
<ModalFooter>
<Button variant="outline" action="secondary" onPress={onClose}>
<ButtonText>Cancel</ButtonText>
</Button>
<Button action="negative" onPress={onConfirm}>
<ButtonText>Delete</ButtonText>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}Toast
提示框(Toast)
Notification component:
tsx
import {
Toast,
ToastTitle,
ToastDescription,
useToast,
} from '@/components/ui/toast';
function NotificationExample() {
const toast = useToast();
const showToast = () => {
toast.show({
placement: 'top',
render: ({ id }) => (
<Toast nativeID={`toast-${id}`} action="success" variant="solid">
<ToastTitle>Success!</ToastTitle>
<ToastDescription>Your changes have been saved.</ToastDescription>
</Toast>
),
});
};
return (
<Button onPress={showToast}>
<ButtonText>Show Toast</ButtonText>
</Button>
);
}通知组件:
tsx
import {
Toast,
ToastTitle,
ToastDescription,
useToast,
} from '@/components/ui/toast';
function NotificationExample() {
const toast = useToast();
const showToast = () => {
toast.show({
placement: 'top',
render: ({ id }) => (
<Toast nativeID={`toast-${id}`} action="success" variant="solid">
<ToastTitle>Success!</ToastTitle>
<ToastDescription>Your changes have been saved.</ToastDescription>
</Toast>
),
});
};
return (
<Button onPress={showToast}>
<ButtonText>Show Toast</ButtonText>
</Button>
);
}Accordion
折叠面板(Accordion)
Expandable content sections:
tsx
import {
Accordion,
AccordionItem,
AccordionHeader,
AccordionTrigger,
AccordionTitleText,
AccordionIcon,
AccordionContent,
AccordionContentText,
} from '@/components/ui/accordion';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react-native';
function FAQAccordion({ items }: { items: { question: string; answer: string }[] }) {
return (
<Accordion type="multiple" defaultValue={['item-0']}>
{items.map((item, index) => (
<AccordionItem key={index} value={`item-${index}`}>
<AccordionHeader>
<AccordionTrigger>
{({ isExpanded }) => (
<>
<AccordionTitleText>{item.question}</AccordionTitleText>
<AccordionIcon as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
</>
)}
</AccordionTrigger>
</AccordionHeader>
<AccordionContent>
<AccordionContentText>{item.answer}</AccordionContentText>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}可展开的内容区域:
tsx
import {
Accordion,
AccordionItem,
AccordionHeader,
AccordionTrigger,
AccordionTitleText,
AccordionIcon,
AccordionContent,
AccordionContentText,
} from '@/components/ui/accordion';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react-native';
function FAQAccordion({ items }: { items: { question: string; answer: string }[] }) {
return (
<Accordion type="multiple" defaultValue={['item-0']}>
{items.map((item, index) => (
<AccordionItem key={index} value={`item-${index}`}>
<AccordionHeader>
<AccordionTrigger>
{({ isExpanded }) => (
<>
<AccordionTitleText>{item.question}</AccordionTitleText>
<AccordionIcon as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
</>
)}
</AccordionTrigger>
</AccordionHeader>
<AccordionContent>
<AccordionContentText>{item.answer}</AccordionContentText>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}Checkbox and Radio
复选框和单选框(Checkbox and Radio)
Selection controls:
tsx
import {
Checkbox,
CheckboxIndicator,
CheckboxIcon,
CheckboxLabel,
} from '@/components/ui/checkbox';
import {
RadioGroup,
Radio,
RadioIndicator,
RadioIcon,
RadioLabel,
} from '@/components/ui/radio';
import { CheckIcon, CircleIcon } from 'lucide-react-native';
function TermsCheckbox({ isChecked, onChange }: {
isChecked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<Checkbox value="terms" isChecked={isChecked} onChange={onChange}>
<CheckboxIndicator>
<CheckboxIcon as={CheckIcon} />
</CheckboxIndicator>
<CheckboxLabel>I agree to the terms and conditions</CheckboxLabel>
</Checkbox>
);
}
function ShippingOptions({ value, onChange }: {
value: string;
onChange: (value: string) => void;
}) {
return (
<RadioGroup value={value} onChange={onChange}>
<Radio value="standard">
<RadioIndicator>
<RadioIcon as={CircleIcon} />
</RadioIndicator>
<RadioLabel>Standard Shipping (5-7 days)</RadioLabel>
</Radio>
<Radio value="express">
<RadioIndicator>
<RadioIcon as={CircleIcon} />
</RadioIndicator>
<RadioLabel>Express Shipping (2-3 days)</RadioLabel>
</Radio>
<Radio value="overnight">
<RadioIndicator>
<RadioIcon as={CircleIcon} />
</RadioIndicator>
<RadioLabel>Overnight Shipping (1 day)</RadioLabel>
</Radio>
</RadioGroup>
);
}选择控件:
tsx
import {
Checkbox,
CheckboxIndicator,
CheckboxIcon,
CheckboxLabel,
} from '@/components/ui/checkbox';
import {
RadioGroup,
Radio,
RadioIndicator,
RadioIcon,
RadioLabel,
} from '@/components/ui/radio';
import { CheckIcon, CircleIcon } from 'lucide-react-native';
function TermsCheckbox({ isChecked, onChange }: {
isChecked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<Checkbox value="terms" isChecked={isChecked} onChange={onChange}>
<CheckboxIndicator>
<CheckboxIcon as={CheckIcon} />
</CheckboxIndicator>
<CheckboxLabel>I agree to the terms and conditions</CheckboxLabel>
</Checkbox>
);
}
function ShippingOptions({ value, onChange }: {
value: string;
onChange: (value: string) => void;
}) {
return (
<RadioGroup value={value} onChange={onChange}>
<Radio value="standard">
<RadioIndicator>
<RadioIcon as={CircleIcon} />
</RadioIndicator>
<RadioLabel>Standard Shipping (5-7 days)</RadioLabel>
</Radio>
<Radio value="express">
<RadioIndicator>
<RadioIcon as={CircleIcon} />
</RadioIndicator>
<RadioLabel>Express Shipping (2-3 days)</RadioLabel>
</Radio>
<Radio value="overnight">
<RadioIndicator>
<RadioIcon as={CircleIcon} />
</RadioIndicator>
<RadioLabel>Overnight Shipping (1 day)</RadioLabel>
</Radio>
</RadioGroup>
);
}Best Practices
最佳实践
1. Use Composition Over Configuration
1. 优先使用组件组合而非配置
Compose components with sub-components for flexibility:
tsx
// Good: Composable structure
<Button>
<ButtonIcon as={PlusIcon} />
<ButtonText>Add</ButtonText>
</Button>
// Avoid: Prop-heavy configuration
<Button icon={PlusIcon} text="Add" iconPosition="left" />通过子组件组合来实现灵活性:
tsx
// Good: Composable structure
<Button>
<ButtonIcon as={PlusIcon} />
<ButtonText>Add</ButtonText>
</Button>
// Avoid: Prop-heavy configuration
<Button icon={PlusIcon} text="Add" iconPosition="left" />2. Leverage FormControl for Form Fields
2. 为表单字段使用FormControl
Wrap inputs in FormControl for consistent validation:
tsx
<FormControl isRequired isInvalid={!!error}>
<FormControlLabel>
<FormControlLabelText>Password</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField type="password" />
</Input>
<FormControlError>
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
</FormControl>将输入框包裹在FormControl中以实现统一的验证:
tsx
<FormControl isRequired isInvalid={!!error}>
<FormControlLabel>
<FormControlLabelText>Password</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField type="password" />
</Input>
<FormControlError>
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
</FormControl>3. Handle Platform Differences
3. 处理平台差异
Use platform-specific logic when needed:
tsx
import { Platform } from 'react-native';
function ResponsiveModal({ children }: { children: React.ReactNode }) {
return (
<Modal size={Platform.OS === 'web' ? 'lg' : 'full'}>
<ModalContent>
{children}
</ModalContent>
</Modal>
);
}必要时使用平台特定逻辑:
tsx
import { Platform } from 'react-native';
function ResponsiveModal({ children }: { children: React.ReactNode }) {
return (
<Modal size={Platform.OS === 'web' ? 'lg' : 'full'}>
<ModalContent>
{children}
</ModalContent>
</Modal>
);
}4. Use Proper Loading States
4. 使用正确的加载状态
Show loading feedback for async operations:
tsx
function SubmitButton({ isLoading, onPress }: {
isLoading: boolean;
onPress: () => void;
}) {
return (
<Button isDisabled={isLoading} onPress={onPress}>
{isLoading && <ButtonSpinner mr="$2" />}
<ButtonText>{isLoading ? 'Submitting...' : 'Submit'}</ButtonText>
</Button>
);
}为异步操作显示加载反馈:
tsx
function SubmitButton({ isLoading, onPress }: {
isLoading: boolean;
onPress: () => void;
}) {
return (
<Button isDisabled={isLoading} onPress={onPress}>
{isLoading && <ButtonSpinner mr="$2" />}
<ButtonText>{isLoading ? 'Submitting...' : 'Submit'}</ButtonText>
</Button>
);
}5. Create Reusable Component Wrappers
5. 创建可复用的组件包装器
Build app-specific components on top of gluestack-ui:
tsx
// components/app/PrimaryButton.tsx
import { Button, ButtonText, ButtonSpinner } from '@/components/ui/button';
interface PrimaryButtonProps {
children: string;
isLoading?: boolean;
onPress: () => void;
}
export function PrimaryButton({ children, isLoading, onPress }: PrimaryButtonProps) {
return (
<Button
size="lg"
variant="solid"
action="primary"
isDisabled={isLoading}
onPress={onPress}
className="rounded-full"
>
{isLoading && <ButtonSpinner mr="$2" />}
<ButtonText>{children}</ButtonText>
</Button>
);
}在gluestack-ui基础上构建特定于应用的组件:
tsx
// components/app/PrimaryButton.tsx
import { Button, ButtonText, ButtonSpinner } from '@/components/ui/button';
interface PrimaryButtonProps {
children: string;
isLoading?: boolean;
onPress: () => void;
}
export function PrimaryButton({ children, isLoading, onPress }: PrimaryButtonProps) {
return (
<Button
size="lg"
variant="solid"
action="primary"
isDisabled={isLoading}
onPress={onPress}
className="rounded-full"
>
{isLoading && <ButtonSpinner mr="$2" />}
<ButtonText>{children}</ButtonText>
</Button>
);
}Common Patterns
常见模式
Form with Validation
带验证的表单
tsx
import { useState } from 'react';
import { VStack } from '@/components/ui/vstack';
import { Button, ButtonText } from '@/components/ui/button';
import { FormControl, FormControlError, FormControlErrorText } from '@/components/ui/form-control';
import { Input, InputField } from '@/components/ui/input';
interface FormData {
email: string;
password: string;
}
interface FormErrors {
email?: string;
password?: string;
}
function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const [formData, setFormData] = useState<FormData>({ email: '', password: '' });
const [errors, setErrors] = useState<FormErrors>({});
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = () => {
if (validate()) {
onSubmit(formData);
}
};
return (
<VStack space="md">
<FormControl isInvalid={!!errors.email}>
<Input>
<InputField
placeholder="Email"
value={formData.email}
onChangeText={(text) => setFormData({ ...formData, email: text })}
/>
</Input>
<FormControlError>
<FormControlErrorText>{errors.email}</FormControlErrorText>
</FormControlError>
</FormControl>
<FormControl isInvalid={!!errors.password}>
<Input>
<InputField
placeholder="Password"
type="password"
value={formData.password}
onChangeText={(text) => setFormData({ ...formData, password: text })}
/>
</Input>
<FormControlError>
<FormControlErrorText>{errors.password}</FormControlErrorText>
</FormControlError>
</FormControl>
<Button onPress={handleSubmit}>
<ButtonText>Login</ButtonText>
</Button>
</VStack>
);
}tsx
import { useState } from 'react';
import { VStack } from '@/components/ui/vstack';
import { Button, ButtonText } from '@/components/ui/button';
import { FormControl, FormControlError, FormControlErrorText } from '@/components/ui/form-control';
import { Input, InputField } from '@/components/ui/input';
interface FormData {
email: string;
password: string;
}
interface FormErrors {
email?: string;
password?: string;
}
function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const [formData, setFormData] = useState<FormData>({ email: '', password: '' });
const [errors, setErrors] = useState<FormErrors>({});
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = () => {
if (validate()) {
onSubmit(formData);
}
};
return (
<VStack space="md">
<FormControl isInvalid={!!errors.email}>
<Input>
<InputField
placeholder="Email"
value={formData.email}
onChangeText={(text) => setFormData({ ...formData, email: text })}
/>
</Input>
<FormControlError>
<FormControlErrorText>{errors.email}</FormControlErrorText>
</FormControlError>
</FormControl>
<FormControl isInvalid={!!errors.password}>
<Input>
<InputField
placeholder="Password"
type="password"
value={formData.password}
onChangeText={(text) => setFormData({ ...formData, password: text })}
/>
</Input>
<FormControlError>
<FormControlErrorText>{errors.password}</FormControlErrorText>
</FormControlError>
</FormControl>
<Button onPress={handleSubmit}>
<ButtonText>Login</ButtonText>
</Button>
</VStack>
);
}Data List with Actions
带操作的数据列表
tsx
import { HStack } from '@/components/ui/hstack';
import { VStack } from '@/components/ui/vstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Heading } from '@/components/ui/heading';
import { Button, ButtonIcon } from '@/components/ui/button';
import { Pressable } from '@/components/ui/pressable';
import { TrashIcon, EditIcon } from 'lucide-react-native';
interface Item {
id: string;
title: string;
description: string;
}
function ItemList({ items, onEdit, onDelete }: {
items: Item[];
onEdit: (id: string) => void;
onDelete: (id: string) => void;
}) {
return (
<VStack space="sm">
{items.map((item) => (
<Box
key={item.id}
className="p-4 bg-background-50 rounded-lg border border-outline-200"
>
<HStack justifyContent="space-between" alignItems="center">
<VStack space="xs" flex={1}>
<Heading size="sm">{item.title}</Heading>
<Text size="sm" className="text-typography-500">
{item.description}
</Text>
</VStack>
<HStack space="xs">
<Button
size="sm"
variant="outline"
action="secondary"
onPress={() => onEdit(item.id)}
>
<ButtonIcon as={EditIcon} />
</Button>
<Button
size="sm"
variant="outline"
action="negative"
onPress={() => onDelete(item.id)}
>
<ButtonIcon as={TrashIcon} />
</Button>
</HStack>
</HStack>
</Box>
))}
</VStack>
);
}tsx
import { HStack } from '@/components/ui/hstack';
import { VStack } from '@/components/ui/vstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Heading } from '@/components/ui/heading';
import { Button, ButtonIcon } from '@/components/ui/button';
import { Pressable } from '@/components/ui/pressable';
import { TrashIcon, EditIcon } from 'lucide-react-native';
interface Item {
id: string;
title: string;
description: string;
}
function ItemList({ items, onEdit, onDelete }: {
items: Item[];
onEdit: (id: string) => void;
onDelete: (id: string) => void;
}) {
return (
<VStack space="sm">
{items.map((item) => (
<Box
key={item.id}
className="p-4 bg-background-50 rounded-lg border border-outline-200"
>
<HStack justifyContent="space-between" alignItems="center">
<VStack space="xs" flex={1}>
<Heading size="sm">{item.title}</Heading>
<Text size="sm" className="text-typography-500">
{item.description}
</Text>
</VStack>
<HStack space="xs">
<Button
size="sm"
variant="outline"
action="secondary"
onPress={() => onEdit(item.id)}
>
<ButtonIcon as={EditIcon} />
</Button>
<Button
size="sm"
variant="outline"
action="negative"
onPress={() => onDelete(item.id)}
>
<ButtonIcon as={TrashIcon} />
</Button>
</HStack>
</HStack>
</Box>
))}
</VStack>
);
}Anti-Patterns
反模式
Do Not Mix Styling Approaches
不要混合使用样式方法
tsx
// Bad: Mixing inline styles with NativeWind
<Button style={{ backgroundColor: 'blue' }} className="p-4">
<ButtonText>Click</ButtonText>
</Button>
// Good: Use NativeWind classes consistently
<Button className="bg-blue-500 p-4">
<ButtonText>Click</ButtonText>
</Button>tsx
// Bad: Mixing inline styles with NativeWind
<Button style={{ backgroundColor: 'blue' }} className="p-4">
<ButtonText>Click</ButtonText>
</Button>
// Good: Use NativeWind classes consistently
<Button className="bg-blue-500 p-4">
<ButtonText>Click</ButtonText>
</Button>Do Not Skip Accessibility Props
不要跳过无障碍属性
tsx
// Bad: Missing accessibility information
<Pressable onPress={handlePress}>
<Icon as={MenuIcon} />
</Pressable>
// Good: Include accessibility props
<Pressable
onPress={handlePress}
accessibilityLabel="Open menu"
accessibilityRole="button"
>
<Icon as={MenuIcon} />
</Pressable>tsx
// Bad: Missing accessibility information
<Pressable onPress={handlePress}>
<Icon as={MenuIcon} />
</Pressable>
// Good: Include accessibility props
<Pressable
onPress={handlePress}
accessibilityLabel="Open menu"
accessibilityRole="button"
>
<Icon as={MenuIcon} />
</Pressable>Do Not Ignore Platform Constraints
不要忽略平台限制
tsx
// Bad: Using web-only APIs on native
<Modal>
<ModalContent onClick={handleClick}> {/* onClick doesn't work on native */}
...
</ModalContent>
</Modal>
// Good: Use cross-platform events
<Modal>
<ModalContent>
<Pressable onPress={handlePress}>
...
</Pressable>
</ModalContent>
</Modal>tsx
// Bad: Using web-only APIs on native
<Modal>
<ModalContent onClick={handleClick}> {/* onClick doesn't work on native */}
...
</ModalContent>
</Modal>
// Good: Use cross-platform events
<Modal>
<ModalContent>
<Pressable onPress={handlePress}>
...
</Pressable>
</ModalContent>
</Modal>Do Not Hardcode Colors
不要硬编码颜色
tsx
// Bad: Hardcoded colors
<Box className="bg-[#3B82F6]">
<Text className="text-[#FFFFFF]">Hello</Text>
</Box>
// Good: Use theme tokens
<Box className="bg-primary-500">
<Text className="text-typography-0">Hello</Text>
</Box>tsx
// Bad: Hardcoded colors
<Box className="bg-[#3B82F6]">
<Text className="text-[#FFFFFF]">Hello</Text>
</Box>
// Good: Use theme tokens
<Box className="bg-primary-500">
<Text className="text-typography-0">Hello</Text>
</Box>Related Skills
相关技能
- gluestack-theming: Customizing themes and design tokens
- gluestack-accessibility: Ensuring accessible implementations
- gluestack-theming: 自定义主题和设计令牌
- gluestack-accessibility: 确保无障碍实现