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 数组

相关推荐
sure28214 小时前
React Native中创建自定义渐变色
前端·react native
墨狂之逸才15 小时前
React Native 物理按键扫码监听终极方案:从冲突到完美共存
react native
Lupino16 小时前
烧掉 10 刀 API 费,我才明白小程序虚拟列表根本不用“库”!
react.js·微信小程序
嚴寒21 小时前
前端配环境配到崩溃?这个一键脚手架让我少掉了一把头发
前端·react.js·架构
古茗前端团队1 天前
嗯…微信小程序主包又双叒叕不够用了!!!
react.js
寅时码2 天前
React 正在演变为一场不可逆的赛博瘟疫:AI 投毒、编译器迷信与装死的官方
前端·react.js·设计模式
学高数就犯困2 天前
React:一个例子讲清楚 useEffect 和 useReducer
react.js
Wect2 天前
JSX & ReactElement 核心解析
前端·react.js·面试
codingWhat3 天前
手撸一个「能打」的 React Table 组件
前端·javascript·react.js
程序员ys3 天前
前端权限控制设计
前端·vue.js·react.js