React 技术栈 (表格,表单处理最优方案)

React 问世已有一段时间,如今依旧表现强劲。 相信你也会在项目中经常遇见他

核心:React + TypeScript

元框架:Next.js

Next.js[3] 堪称 React 开发的 “瑞士军刀”,功能齐全且表现出色。目前最新版本(Next.js 15)[4]已经全面支持 React 19,集成了路由和 API 管理功能,还内置了性能优化机制。

不过,它不是唯一的选择。对于全栈应用而言,Remix[5] 依旧很棒;还有个崭露头角的 Tanstack Start[6],也在做出一些有趣的成果,大家不妨关注一下。要是你只需要路由功能,可以试试 React Router[7]。

样式设计:Tailwind CSS+shadcn/ui

你也许会对 Tailwind CSS[8] 持怀疑态度,不过实际使用后可能会改变你的看法。Tailwind CSS 与 shadcn/ui[9] 搭配,就能打造出强大的样式设计组合。AI 工具能生成精准的 Tailwind 类,shadcn/ui 提供开箱即用的无障碍组件,同时还能优化代码包体积。这样一来,你可以在保持设计一致性的同时,快速进行原型设计和迭代。

客户端状态管理:Zustand

服务器状态管理:TanStack Query

TanStack Query[11] 能处理服务器状态中所有令人头疼的问题。它可以自动刷新数据,智能缓存十分有效,轻松处理实时更新,乐观更新功能更是神奇,而且其开发工具会让你不禁感叹,要是没有它该怎么开发。

动画效果:Motion

在 React 中实现动画效果,Motion[12] 是最佳选择。它支持声明式动画,易于理解,对各种手势的支持也很出色,还具备共享布局动画等高级功能,无论是简单的过渡效果,还是复杂的动画设计,它都能完美胜任。

测试工具

测试环节也不能马虎。Vitest、React Testing Library 和 Playwright 这三款工具堪称黄金组合:Vitest[13] 比 Jest[14] 速度更快,并且原生支持 ES 模块;React Testing Library[15] 依旧是组件测试的得力助手,能帮你发现可访问性问题,让测试过程更贴近用户使用场景;而 Playwright[16] 在端到端测试方面表现卓越,能支持多种浏览器、进行视觉测试、处理网络相关事务,还能模拟移动设备,并且测试结果稳定可靠。

表格处理:

1.TanStack Table

如果涉及表格相关开发,TanStack Table[17] 必不可少。它提供类型安全的表格,对于大量数据支持虚拟滚动,排序和筛选功能易用,列设置灵活,即便处理海量数据集,性能依旧出色。

2.vxetable

vue中用过的比较好用的table处理框架,支持虚拟滚动,拖拽,排序,分页,列控制,动态打印,导出,多语言等

3.material-react-table

支持虚拟滚动,拖拽,排序,分页,列控制,请求动画,动态列,动态头,多头等配置

type ArgInstance = {
  data?: any[];
  api?: any;
  columns: MRT_ColumnDef<any>[];
  loading?: boolean;
  enablePager?: boolean;
  searchParam?: any;
};
type Setting = {
  enablePager?: boolean;
  searchParam?: any;
};
type State = {
  loading?: boolean;
};
type TableInstance = {
  data: ArgInstance,
  settings?: Setting;
  state?: State;
};
const loadingErrorMessage = 'Loading Error';
const MaterialStaticTable = (props: TableInstance) => {
  //should be memoized or stable
  const { data, columns, } = props.data;
  const [tableData, setTableData] = useState<any[]>(data || []);
  const showRowsPage: any = [10, 20, 30, 50];
  const [frontPager, setFrontPager] = useState(true);
  const [enableColumnOrdering, setEnableColumnOrdering] = useState(true);
  const [pagination, setPagination] = useState({
    pageIndex: frontPager ? 0 : 1, // 对于支持后端分页的接口0页代表查所有
    pageSize: 10,
  });
  const [columnsData, setColumnsData] = useState<any>(columns || []);
  const [enableRowVirtual, setEnableRowVirtual] = useState(true);
  const [isError, setIsError] = useState(false);
  const [isLoading, setIsLoading] = useState(props.state?.loading || false);
  const [isFetching, setIsFetching] = useState(false);
  const [globalFilter, setGlobalFilter] = useState('');
  const [isEnablePager, setIsEnablePager] = useState(props.data.enablePager);
  const [rowCount, setRowCount] = useState(0);
  const [totalPage, setTotalPage] = useState(0);
  const [searchCondition, setSearchCondition] = useState('');

  const [tablesReqInfo, setTablesReqInfo] = useState<any>(null);
  const [tablesData, setTablesData] = useState(data || []);
  const [filterString, setFilterString] = useState<any>('');

  const refreshTable = () => {
    // Refresh table logic here
  };
  const table = useMaterialReactTable({
    columns: columns,
    data: tableData,

    enablePagination: isEnablePager,
    enableRowVirtualization: !enableRowVirtual,

    muiCircularProgressProps: {
      color: 'warning',
      thickness: 5,
      size: 80,
    },
    muiSkeletonProps: {
      animation: 'pulse',
      height: 28,
    },
    // enableColumnVirtualization: enableColumnVirtualization,
    initialState: {
      // pagination,
      // density: 'compact', // 表格紧密 compact comfortable
      // showGlobalFilter: !isPrint,
      // ...customState,
    },


    manualPagination: !frontPager, // 是否前端分页(接口请求走后端分页)
    rowCount: rowCount, // 数据总数
    pageCount: pagination.pageSize,
    state: {
      pagination,
      // pagination: pagination,
      isLoading: isLoading,
      showAlertBanner: isError,
      showProgressBars: isFetching,
    },

    // muiPaginationProps: {
    //   color: 'primary',
    //   showRowsPerPage: false,
    //   rowsPerPageOptions: showRowsPage, //showRowsPage,
    //   shape: 'rounded',
    //   variant: 'outlined',
    //   size: 'small',
    //   defaultPage: frontPager ? 0 : 1,
    //   count: totalPage,
    //   page: (frontPager ? 1 : 0) + pagination.pageIndex,
    // onChange: (event, page) => {
    //   // fix default page init to 0 cause two request
    //   // console.log('onchange', (frontPager ? -1 : 0) + page, page);
    //   setPagination({ ...pagination, ...{ pageIndex: (frontPager ? -1 : 0) + page } });
    // },
    // }, 
    // defaultColumn: {
    //   minSize: 1,
    //   maxSize: 100,
    //   // size: document.body.clientWidth / columns.length - 3,
    // },

    paginationDisplayMode: 'pages',
    // enableStickyHeader: true,
    enableStickyFooter: true,
    enableColumnOrdering: enableColumnOrdering,
    enableColumnResizing: true, //false为自动分布行
    layoutMode: 'grid',
    enableFullScreenToggle: false, // 全屏

    enableColumnDragging: true, // 列拖拽
    enableDensityToggle: false, // 密度
    enableColumnActions: false, // 列操作
    enableFilters: true, // 控制全局过滤以及每一列过滤(开启会导致表格右上角出现过滤框)
    manualFiltering: true,
    // enableColumnVirtualization: true,//虚拟滚动

    enableGlobalFilter: false, // /searchFromEnd,
    positionGlobalFilter: 'right', // "left",
    ///
    onGlobalFilterChange: setGlobalFilter,
    onColumnFiltersChange: (filterData: any) => { },
    onShowColumnFiltersChange: (filterData: any) => { },
    enableColumnFilterModes: true,// 开启列过滤模式
    ///
    // columnFilterDisplayMode: "custom",
    // enableRowNumbers: true,
    // enableRowDragging: true,
    enableTableHead: true,
    enableMultiSort: true,
    manualSorting: false, // 手动排序
    enableEditing: true,
    editDisplayMode: 'row', //default
    onEditingRowSave: ({ table, values }) => {
      //validate data
      //save data to api
      console.log('onEditingRowSave', table, values);
      table.setEditingRow(null); //exit editing mode
    },
    muiEditTextFieldProps: ({ cell, row, table }) => ({
      onBlur: (event) => {
        //validate data
        //save data to api and/or rerender table
        // table.setEditingCell(null) is called automatically onBlur internally
      },
    }),
    muiTableBodyCellProps: ({ cell, column, table }) => ({
      onClick: () => {
        table.setEditingCell(cell); //set editing cell
        //optionally, focus the text field
        queueMicrotask(() => {
          const textField = table.refs.editInputRefs.current[column.id];
          if (textField) {
            textField.focus();
            textField.select?.();
          }
        });
      },
    }),
    onEditingRowCancel: () => {
      //clear any validation errors
    },
    enableRowActions: true,
    enableRowSelection: true,// 允许行选择
    enableGrouping: false,
    enableHiding: true,
    enableSorting: true,

    // defaultColumn: {
    //   minSize: 40,
    //   maxSize: 1000,
    //   size: 180,
    // },

    // muiTableContainerProps: {
    //   sx: {
    //   },
    // },

    muiTableBodyProps: {
      sx: {
        maxHeight: isEnablePager ? '700px' || '600px' : 'unset',
        overflowY: 'auto',
        minHeight: '600px',
      },
    },
    muiTableBodyRowProps: ({ row }: any) => {
      let { original } = row;
      let custStyle: any = {};
      // if (original.moreLabRecordFound === true) {
      //   custStyle.background = '#F7EAC1';
      // }
      // if (original.moreLabRecordFound === false) {
      //   custStyle.background = '#FFC4C4';
      // }
      return {
        sx: { ...custStyle },
        className: '',
      };
    },
    // muiTableBodyRowProps: muiTableBodyRowProps,
    muiTableBodyCellProps: ({ column }: any) => {
      return {}
    },
    // 
    renderTopToolbarCustomActions: () => (
      <Box className="m-r-10 table_top_left">
        <Box className="flex flex-1">
          {/* {newFormFn && (
            <span className="m-r-10 visibleNotPrinter">
              <Button
                onClick={newFormFn}
                size="small"
                variant="contained"
                color="primary"
                disableRipple
              >
                {newFormFnText}
              </Button>
            </span>
          )}
          {leftForm} */}
        </Box>
      </Box>
    ),
    renderToolbarInternalActions: ({ table }) => {
      return (
        <>
          <Box className="visibleNotPrinter">
            {/* <TextField
              name="searchCondition"
              onChange={(e: any) => {
                setGlobalFilter(e.target.value || '');
              }}
              value={globalFilter}
              autoComplete="off"
              InputProps={{
                startAdornment: (
                  <InputAdornment position="start">
                    <Search
                      onClick={() => {
                        setLoadTime(new Date());
                      }}
                      sx={{ cursor: 'pointer' }}
                    />
                  </InputAdornment>
                ),
                endAdornment: (
                  <InputAdornment position="start">
                    <Clear
                      onClick={() => {
                        setGlobalFilter('');
                      }}
                      sx={{ cursor: 'pointer' }}
                    />
                  </InputAdornment>
                ),
              }}
            /> */}
          </Box>
        </>
      );
    },
    // muiBottomToolbarProps: {
    //   sx: {
    //     // display: "inline-block",
    //     display: "flex",
    //     width: "100%",
    //   },
    // },

    muiTableFooterCellProps: {
      sx: {
        display: 'flex',
        width: '45%',
        float: 'right',
      },
    },

    // renderBottomToolbarCustomActions: () => (
    //   <Box className="f-r f-s-14">
    //     <span className="total_counts">Total {rowCount} Records</span>
    //   </Box>
    // ),

    renderBottomToolbar: () => (
      <Box className="flex flex-1" sx={{ justifyContent: 'space-between' }}>
        {/* <Box>
          {' '}
          <span className="total_counts">Total {rowCount} Records</span>{' '}
        </Box>
        <Box className="visibleNotPrinter">
          <Box>
            <span className="total_counts">Rows Per Page:</span>
            <Select
              disabled={isLoading}
              value={pagination.pageSize}
              onChange={(event: any) => {
                setPagination({
                  ...pagination,
                  pageSize: event?.target?.value || 10,
                  pageIndex: frontPager ? 0 : 1
                });
                const pageCount = tablesData.length > 0
                  ? Math.ceil(tablesData.length / event?.target?.value || 10)
                  : 0;
                setTotalPage(pageCount);
              }}
            >
              {showRowsPage.map((pageItem: any, indexItem: number) => (
                <MenuItem key={indexItem} value={pageItem}>
                  {pageItem}
                </MenuItem>
              ))}
            </Select>
          </Box>
          <Pagination
            disabled={isLoading}
            showFirstButton
            showLastButton
            shape="rounded"
            page={pagination.pageIndex + (frontPager ? 1 : 0)}
            count={totalPage}
            onChange={(event: any, page: number) => {
              setPagination({
                ...pagination,
                pageIndex: (frontPager ? -1 : 0) + page
              });
            }}
          />
        </Box> */}
      </Box>
    ),

    muiToolbarAlertBannerProps: isError
      ? {
        color: 'error',
        children: loadingErrorMessage,
      }
      : undefined,
  });

  return <MaterialReactTable table={table} />;
};

表单处理:

1.React Hook Form

过去,在 React 中处理表单让人头疼,但有了 React Hook Form[18] 就不一样了。它专为速度而生,搭配 Zod[19] 进行表单验证易如反掌,与 TypeScript 配合默契,代码包小,API 设计直观易懂。

import * as React from "react"
import { useForm } from "react-hook-form"


type FormInputs = {
  username: string
  firstName: string
}


const App = () => {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors },
  } = useForm<FormInputs>()


  const onSubmit = (data: FormInputs) => {
    console.log(data)
  }


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>Username</label>
      <input {...register("username")} />
      {errors.username && <p>{errors.username.message}</p>}
      <label>First Name</label>
      <input {...register("firstName")} />
      {errors.firstName && <p>{errors.firstName.message}</p>}
      <button
        type="button"
        onClick={() => {
          const inputs = [
            {
              type: "manual",
              name: "username",
              message: "Double Check This",
            },
            {
              type: "manual",
              name: "firstName",
              message: "Triple Check This",
            },
          ]


          inputs.forEach(({ name, type, message }) => {
            setError(name, { type, message })
          })
        }}
      >
        Trigger Name Errors
      </button>
      <input type="submit" />
    </form>
  )
}

2.Formik

[Formik](Validation | Formik)

import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

const SignupSchema = Yup.object().shape({
  firstName: Yup.string()
    .min(2, 'Too Short!')
    .max(50, 'Too Long!')
    .required('Required'),
  lastName: Yup.string()
    .min(2, 'Too Short!')
    .max(50, 'Too Long!')
    .required('Required'),
  email: Yup.string().email('Invalid email').required('Required'),
});

export const ValidationSchemaExample = () => (
  <div>
    <h1>Signup</h1>
    <Formik
      initialValues={{
        firstName: '',
        lastName: '',
        email: '',
      }}
      validationSchema={SignupSchema}
      onSubmit={values => {
        // same shape as initial values
        console.log(values);
      }}
    >
      {({ errors, touched }) => (
        <Form>
          <Field name="firstName" />
          {errors.firstName && touched.firstName ? (
            <div>{errors.firstName}</div>
          ) : null}
          <Field name="lastName" />
          {errors.lastName && touched.lastName ? (
            <div>{errors.lastName}</div>
          ) : null}
          <Field name="email" type="email" />
          {errors.email && touched.email ? <div>{errors.email}</div> : null}
          <button type="submit">Submit</button>
        </Form>
      )}
    </Formik>
  </div>
);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值