react、react-native 实现 tab 切换(实现缓存加载)

typescript 复制代码
import { PropsWithChildren, ReactElement, useState } from "react";
import {
  View,
  Text,
  Image,
  ViewStyle,
  StyleSheet,
  ScrollView,
  TouchableWithoutFeedback,
  ImageStyle,
} from "react-native";

interface Tab {
  icon?: string;
  label?: string;
  value?: string | number;
}

type Props = PropsWithChildren<{
  tabs: Array<string> | Array<Tab>;
  css?: Array<ViewStyle>;
  iconCss?: Array<ImageStyle>;
  labelCss?: Array<ViewStyle>;
  listCss?: Array<ViewStyle>;
  lineCss?: Array<ViewStyle>;
  active?: number;
  onChange?: (value: number) => void;
  children: Array<ReactElement>;
}>;

// 子组件是否加载过(用于缓存处理,加载过将不再重新渲染)
const childrenLoaded: boolean[] = [];

/**导航栏TabNav */
export function TabNav({
  tabs,
  css,
  iconCss,
  labelCss,
  listCss,
  lineCss,
  active,
  onChange,
  children,
}: Props) {
  const [activeIndex, setActiveIndex] = useState<number>(active ? active : 0);

  // 加载状态初始化
  if (childrenLoaded.length == 0 && children?.length) {
    for (let i = 0; i < children.length; i++) childrenLoaded[i] = active == i;
  }

  const setActive = (index: number) => {
    setActiveIndex(index);
    // 加载状态更新(只需处理为加载过的子组件)
    if (childrenLoaded[index] == false) childrenLoaded[index] = true;
    // 通知父组件切换更新
    if (onChange) onChange(index);
  };

  return (
    <View style={{ width: "100%", display: "flex", flexDirection: "column" }}>
      <ScrollView
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        style={[{ width: "100%", minHeight: 45 }, ...(css || [])]}
        contentContainerStyle={{ width: "100%", height: "100%" }}>
        <View
          style={[
            {
              height: "100%",
              minWidth: "100%",
              display: "flex",
              flexDirection: "row",
              flexWrap: "nowrap",
            },
            ...(listCss || []),
          ]}>
          {tabs.map((item, index) => (
            <TouchableWithoutFeedback
              key={index}
              onPress={() => setActive(index)}>
              <View
                style={{
                  height: "100%",
                  padding: 15,
                  display: "flex",
                  flexDirection: "column",
                  alignItems: "center",
                  justifyContent: "center",
                }}>
                {/* icon显示 */}
                {typeof item != "string" && item.icon && (
                  <Image
                    style={[{ width: 45, height: 45 }, ...(iconCss || [])]}
                    source={item.icon || require("@/assets/images/logo.png")}
                  />
                )}

                {/* label显示 */}
                {(typeof item == "string" ||
                  (typeof item != "string" && item.label)) && (
                  <Text
                    style={[
                      { fontWeight: "bold" },
                      ...(labelCss || []),
                      activeIndex == index ? styles.active : null,
                    ]}>
                    {typeof item == "string" ? item : (item as Tab).label}
                  </Text>
                )}

                {/* 激活线条 */}
                <View
                  style={[
                    styles.line,
                    { opacity: activeIndex == index ? 1 : 0 },
                    ...(lineCss || []),
                  ]}
                />
              </View>
            </TouchableWithoutFeedback>
          ))}
        </View>
      </ScrollView>

      {/* 加载子组件,并加入缓存处理 */}
      {children
        ? children.map((child, i) => {
            return (
              <View
                style={{ display: i == activeIndex ? "flex" : "none" }}
                key={i}>
                {childrenLoaded[i] ? child : null}
              </View>
            );
          })
        : null}
    </View>
  );
}

const styles = StyleSheet.create({
  active: {
    color: "green",
  },
  line: {
    height: 2,
    width: 20,
    backgroundColor: "green",
    marginTop: 8,
  },
});

使用举例

typescript 复制代码
<TabNav tabs={["00", "11", "22"]}>
   {/* TabNavItem 00 */}
   <View>
     <Text>0</Text>
   </View>
   {/* TabNavItem 11 */}
   <View>
     <Text>1</Text>
   </View>
   {/* TabNavItem 22 */}
   <View>
     <Text>2</Text>
   </View>
 </TabNav>

TabNavItem 可以是自定义组件,注意:如果是自定义组件,切换到对应的组件才会触发组件内部所有逻辑,且第二次切换后会直接使用缓存渲染(不再重新初始化组件);TabNavItem 需要对应 tabs 数组

相关推荐
GitLqr9 小时前
提升 Web 开发效率:我常用的 10 个 TypeScript 实战技巧
vue.js·react.js·typescript
難釋懷11 小时前
Nginx内存缓存
nginx·缓存·junit
飞翔的馒12 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
数据库·缓存
无人生还13 小时前
从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer
前端·vue.js·react.js
张元清14 小时前
React usePrevious Hook:追踪上一次的 State 和 Props(2026)
javascript·react.js
怪兽学LLM15 小时前
AI 应用中的高并发处理:从限流、异步到缓存与队列
人工智能·缓存
LaughingZhu16 小时前
Product Hunt 每日热榜 | 2026-07-28
前端·神经网络·react.js·搜索引擎·前端框架
喜欢的名字被抢了18 小时前
长会话状态治理:当Redis失忆后,系统如何自己找回忆
数据库·redis·缓存
Highcharts.js1 天前
教程:基于 React + Highcharts 构建一个单页应用程序、按需数据拉取与图表渲染
开发语言·前端·数据结构·react.js·前端框架·highcharts·页面应用
To_OC1 天前
我被 useState 坑了两次之后,终于把它的脾气摸透了
前端·javascript·react.js