react navite封装一个RN 通用按钮组件

可以。面试里如果让你封装一个 RN 通用 Button 组件 ,建议不要只写一个 TouchableOpacity,而是把 类型、loading、disabled、尺寸、主题、icon、样式扩展 都考虑进去。

下面给你一个比较适合面试手写/实际项目的版本。

1. 通用 Button

复制代码
import React from 'react';
import {
  ActivityIndicator,
  Pressable,
  StyleProp,
  StyleSheet,
  Text,
  TextStyle,
  View,
  ViewStyle,
} from 'react-native';

type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text';
type ButtonSize = 'small' | 'medium' | 'large';

interface ButtonProps {
  title: string;
  onPress?: () => void;

  // 状态
  disabled?: boolean;
  loading?: boolean;

  // 样式
  variant?: ButtonVariant;
  size?: ButtonSize;
  fullWidth?: boolean;
  style?: StyleProp<ViewStyle>;
  textStyle?: StyleProp<TextStyle>;

  // 扩展
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

const COLORS = {
  primary: '#1677FF',
  white: '#FFFFFF',
  text: '#333333',
  border: '#1677FF',
  disabled: '#CCCCCC',
};

const Button = ({
  title,
  onPress,
  disabled = false,
  loading = false,
  variant = 'primary',
  size = 'medium',
  fullWidth = false,
  style,
  textStyle,
  leftIcon,
  rightIcon,
}: ButtonProps) => {
  const isDisabled = disabled || loading;

  const getButtonStyle = (): ViewStyle => {
    const baseStyle: ViewStyle = {
      ...styles.base,
      ...styles[size],
    };

    if (fullWidth) {
      baseStyle.width = '100%';
    }

    switch (variant) {
      case 'primary':
        return {
          ...baseStyle,
          backgroundColor: isDisabled
            ? COLORS.disabled
            : COLORS.primary,
        };

      case 'secondary':
        return {
          ...baseStyle,
          backgroundColor: '#F5F5F5',
        };

      case 'outline':
        return {
          ...baseStyle,
          backgroundColor: COLORS.white,
          borderWidth: 1,
          borderColor: isDisabled
            ? COLORS.disabled
            : COLORS.border,
        };

      case 'text':
        return {
          ...baseStyle,
          backgroundColor: 'transparent',
        };

      default:
        return baseStyle;
    }
  };

  const getTextStyle = (): TextStyle => {
    const baseTextStyle: TextStyle = {
      ...styles.text,
    };

    switch (variant) {
      case 'primary':
        return {
          ...baseTextStyle,
          color: COLORS.white,
        };

      case 'outline':
      case 'text':
        return {
          ...baseTextStyle,
          color: isDisabled
            ? COLORS.disabled
            : COLORS.primary,
        };

      case 'secondary':
        return {
          ...baseTextStyle,
          color: COLORS.text,
        };

      default:
        return baseTextStyle;
    }
  };

  return (
    <Pressable
      onPress={onPress}
      disabled={isDisabled}
      style={({ pressed }) => [
        getButtonStyle(),
        pressed && !isDisabled && styles.pressed,
        style,
      ]}
    >
      {loading ? (
        <ActivityIndicator
          size="small"
          color={
            variant === 'primary'
              ? COLORS.white
              : COLORS.primary
          }
        />
      ) : (
        <View style={styles.content}>
          {leftIcon}

          <Text style={[getTextStyle(), textStyle]}>
            {title}
          </Text>

          {rightIcon}
        </View>
      )}
    </Pressable>
  );
};

const styles = StyleSheet.create({
  base: {
    borderRadius: 8,
    alignItems: 'center',
    justifyContent: 'center',
  },

  small: {
    height: 36,
    paddingHorizontal: 16,
  },

  medium: {
    height: 44,
    paddingHorizontal: 20,
  },

  large: {
    height: 52,
    paddingHorizontal: 24,
  },

  content: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    gap: 8,
  },

  text: {
    fontSize: 16,
    fontWeight: '500',
  },

  pressed: {
    opacity: 0.7,
  },
});

export default Button;

2. 使用方式

复制代码
<Button
  title="登录"
  onPress={handleLogin}
/>

不同类型:

复制代码
<Button
  title="主要按钮"
  variant="primary"
/>

<Button
  title="次要按钮"
  variant="secondary"
/>

<Button
  title="边框按钮"
  variant="outline"
/>

<Button
  title="文字按钮"
  variant="text"
/>

不同尺寸:

复制代码
<Button
  title="Small"
  size="small"
/>

<Button
  title="Medium"
  size="medium"
/>

<Button
  title="Large"
  size="large"
/>

Loading:

复制代码
<Button
  title="提交"
  loading={isSubmitting}
  onPress={handleSubmit}
/>

此时按钮自动:

复制代码
loading = true
       ↓
disabled
       ↓
不允许重复点击
       ↓
显示 ActivityIndicator

3. 面试时可以重点讲这几个点

这个题真正考的不是 JSX,而是组件抽象能力

可以这样回答:

我会把 Button 分成基础能力和业务表现两层。基础层统一处理点击、disabled、loading;表现层通过 variant、size 控制不同视觉样式,同时通过 style、textStyle、icon 保留扩展能力。

另外有一个比较容易被面试官追问的问题:

为什么使用 Pressable 而不是 TouchableOpacity

可以答:

Pressable 对 pressed、disabled、hover、focus 等交互状态控制更加灵活。现在的新 RN 项目里,如果没有历史兼容需求,我更倾向使用 Pressable。

4. 如果是"高频 RN 手写题",我建议继续把它升级

实际面试可以进一步要求你实现:

复制代码
<Button
  title="立即购买"
  variant="primary"
  size="large"
  loading={loading}
  disabled={disabled}
  leftIcon={<ShoppingCart />}
  onPress={handleBuy}
/>

然后继续考:

  • forwardRef

  • ref 获取 Button

  • 防止重复点击

  • debounce

  • Theme / Design Token

  • icon

  • children

  • onPress 类型

  • Android ripple

  • hitSlop

  • accessibility

  • testID

  • Dark Mode

如果你是在准备 RN 面试手写题 ,这个 Button 很适合作为第一题,后面可以继续练 Input、Modal、Toast、Loading、FlatList、下拉刷新、分页 Hook、useRequest、倒计时、图片组件

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄1 天前
JavaScript 隐式全局变量解析
javascript
汉堡大王95271 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大1 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师1 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端