React-企业级实践指南-三-

91 阅读39分钟

React 企业级实践指南(三)

原文:Practical Enterprise React

协议:CC BY-NC-SA 4.0

八、编写数据表、Formik 表单和 Yup 验证:第二部分

在这个由两部分组成的章节系列的第一部分中,我们开始设置产品菜单,包括使用数据表和其他样式组件的ProductListView,。第二部分将继续使用 Formik 和 Yup 输入验证构建产品菜单。

现在我们有了可以在 UI 中呈现列表产品的概念证明,如清单 8-1 所示,我们现在可以更新ProductListView.Header组件

首先,导入以下命名组件。

import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import {
  Box,
  Breadcrumbs,
  Button,
  Grid,
  Link,
  SvgIcon,
  Typography,
  makeStyles,
} from '@material-ui/core';
import {
  PlusCircle as PlusCircleIcon,
  Download as DownloadIcon,
  Upload as UploadIcon,
} from 'react-feather';

Listing 8-1Adding Import Components to Header.tsx of ProductListView

接下来,我们将在 Header 组件本身上创建类型定义和其他更改。

我们把所有东西都包装在一个<Grid/>,里面,我们还做了三个按钮:进口、**出口、新产品。**复制粘贴 ProductListView 的 Header 组件;参见清单 8-2 。

/*types definition */

type Props = {
  className?: string;
};

const Header = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <Grid
      container
      spacing={3}
      justify="space-between"
      className={clsx(classes.root, className)}
      {...rest}
    >
      <Grid item>
        <Breadcrumbs
          separator={<NavigateNextIcon fontSize="small" />}
          aria-label="breadcrumb"
        >
          <Link
            variant="body1"
            color="inherit"
            to="/dashboard"
            component={RouterLink}
          >
            Dashboard
          </Link>

          <Box>
            <Typography variant="body1" color="inherit">
              List Products
            </Typography>
          </Box>
        </Breadcrumbs>
        <Typography variant="h4" color="textPrimary">
          All Products
        </Typography>
        <Box mt={2}>
          <Button

            className={classes.action}
            startIcon={
              <SvgIcon fontSize="small">
                <UploadIcon />
              </SvgIcon>
            }
          >
            Import
          </Button>
          <Button
            className={classes.action}
            startIcon={
              <SvgIcon fontSize="small">
                <DownloadIcon />
              </SvgIcon>
            }
          >
            Export
          </Button>
        </Box>
      </Grid>

      <Grid item>

         <Button
          color="primary"
          variant="contained"
          className={classes.action}
          component={RouterLink}
          to="/dashboard/create-product"
          startIcon={
            <SvgIcon fontSize="small">
              <PlusCircleIcon />
            </SvgIcon>
          }
        >

          New Product
        </Button>
      </Grid>
    </Grid>
  );
};

Listing 8-2Updating the Header Component of ProductListView

最后,将样式边距添加到ProductListView.的标题组件中

const useStyles = makeStyles(theme => ({
  root: {},
  action: {
    marginBottom: theme.spacing(1),
    '& + &': {
      marginLeft: theme.spacing(1),
    },
  },
}));

export default Header;

Listing 8-3Adding the Styling Component to the Header Component

我们现在已经完成了 ProductListView 让我们开始构建 ProductCreateView。

更新 ProductCreateView

ProductCreateView 是我们为应用添加输入表单的地方。

打开 ProductCreateView 的 Header.tsx:

ProductCreateViewHeader.tsx.

我们将从命名的导入组件开始,如清单 8-4 所示。

import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import {
  Breadcrumbs,
  Button,
  Grid,
  Link,
  Typography,
  makeStyles,
  Box,
} from '@material-ui/core';

Listing 8-4Adding Named Components to the Header.tsx of ProductCreateView

还要创建类型定义,这只是一个 string 类型的可空类名:

type Props = {
  className?: string;
};

然后将下面的更新复制到我们的ProductCreateView的 Header 组件中,如清单 8-5 所示。

const Header = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return (
    <Grid
      className={clsx(classes.root, className)}
      container
      justify="space-between"
      spacing={3}
      {...rest}
    >
      <Grid item>
        <Breadcrumbs
          separator={<NavigateNextIcon fontSize="small" />}
          aria-label="breadcrumb"
        >
          <Link
            variant="body1"
            color="inherit"
            to="/dashboard"
            component={RouterLink}
          >
            Dashboard
          </Link>
          <Box mb={3}>
            <Typography variant="body1" color="inherit">
              Create Product
            </Typography>
          </Box>
        </Breadcrumbs>
        <Typography variant="h4" color="textPrimary">
          Create a new product
        </Typography>
      </Grid>
      <Grid item>
        <Button component={RouterLink} to="/dashboard/list-products">
          Cancel
        </Button>
      </Grid>
    </Grid>
  );
};

Listing 8-5Updating the Header.tsx of ProductCreateView

最后,让我们添加useStyle组件。

const useStyles = makeStyles(() => ({
  root: {},
}));

Listing 8-6Adding the Style Component to the Header.tsx of ProductCreateView

我们现在可以在ProductCreateViewindex.tsx中使用这个头组件。

import React from 'react';
import { Container, makeStyles } from '@material-ui/core';

import Header from './Header';
import ProductCreateForm from './ProductCreateForm';
import Page from 'app/components/page';

const ProductCreateView = () => {
  const classes = useStyles();

  return (
    <Page className={classes.root} title="Product Create">
      <Container>
        <Header />
        <ProductCreateForm />
      </Container>
    </Page>
  );
};

const useStyles = makeStyles(theme => ({
  root: {
    minHeight: '100%',
    paddingTop: theme.spacing(3),
    paddingBottom: 100,
  },
}));

export default ProductCreateView;

Listing 8-7Updating the index.tsx of ProductCreateView

所以目前来说这很好。我们出发去更新ProductCreateForm

更新 ProductCreateForm

首先,我们需要添加一些额外的 TypeScript 文件。

在文件夹ProductCreateView下,创建一个名为schema的文件夹。在schema,下,我们将为我们的 Yup 产品验证添加一个新文件。

文件路径是

ProductCreateView ➤ schema ➤ yupProductValidation.ts

清单 8-8 显示了 Yup 产品验证模式。

// the * means all

import * as Yup from 'yup';

export const yupProductValidation = Yup.object().shape({
  category: Yup.string().max(255),
  description: Yup.string().max(5000),
  images: Yup.array(),
  includesTaxes: Yup.bool().required(),
  isTaxable: Yup.bool().required(),
  name: Yup.string().max(255).required(),
  price: Yup.number().min(0).required(),
  productCode: Yup.string().max(255),
  productSku: Yup.string().max(255),
  salePrice: Yup.number().min(0),
});

Listing 8-8Creating the yupProductValidation Schema

之后,我们需要为 Formik 创建产品默认值或初始值。最好在一个单独的文件中定义,这样更干净一点。

schema下创建一个新文件,命名为productDefaultValue.ts,,如清单 8-9 所示。

文件路径是

ProductCreateView ➤ schema ➤ productDefaultValue.ts

注意,我们从 models 文件夹中导入了ProductType组件。

import { ProductType } from 'models/product-type';

export const productDefaultValue: ProductType = {
  attributes: [],
  category: '',
  createdAt: '',
  currency: '',
  id: '',
  image: '',
  inventoryType: 'in_stock',
  isAvailable: false,
  isShippable: false,
  name: '',
  quantity: 0,
  updatedAt: '',
  variants: 0,
  description: '',
  images: [],
  includesTaxes: false,
  isTaxable: false,
  productCode: '',
  productSku: '',
  salePrice: '',
  price: 0,
};

Listing 8-9Creating Initial Values in the productDefaultValue.tsx

好了,我们完成了ProductDefaultValue.tsx.现在让我们安装一个名为 Quill 的富文本编辑器库。

安装 React 管

让我们安装名为react-quill.的 React 版本。回想一下,Quill 是现代网络的开源 WYSIWYG 编辑器。React-quill 是包装 Quill.js 的 React 组件:

npm install react-quill

安装完成后,我们需要将组件导入到src目录的index.tsx中。

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'react-quill/dist/quill.snow.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import * as serviceWorker from 'serviceWorker';
import 'sanitize.css/sanitize.css';

Listing 8-10Adding react-quill to the index.tsx of the src Directory

在 components 文件夹下,创建一个单独的新文件,并将其命名为quill-editor.tsx . The file path is as follows:

app ➤ components ➤ quill-editor.tsx

打开新创建的文件,导入所有必需的命名组件。

import React from 'react';
import clsx from 'clsx';
import ReactQuill from 'react-quill';
import { makeStyles } from '@material-ui/core';

Listing 8-11Importing Named Components to quill-editor.tsx

然后,我们需要类型定义和QuillEditor组件。

type Props = {
  className?: string;
  [key: string]: any;
};

const QuillEditor = ({ className, ...rest }: Props) => {
  const classes = useStyles();

  return <ReactQuill className={clsx(classes.root, className)} {...rest} />;
};

Listing 8-12Adding the QuillEditor Component

在清单 8-12 中,我们还返回了 Quill 并使用了 rest/spread 操作符,这意味着我们将QuillEditor拥有的任何东西传递给ReactQuill

接下来,我们将添加来自 Material-UI Core 的makeStyles,如清单 8-13 所示。

const useStyles = makeStyles(theme => ({
  root: {
    '& .ql-toolbar': {
      borderLeft: 'none',
      borderTop: 'none',
      borderRight: 'none',
      borderBottom: `1px solid ${theme.palette.divider}`,
      '& .ql-picker-label:hover': {
        color: theme.palette.secondary.main,
      },
      '& .ql-picker-label.ql-active': {
        color: theme.palette.secondary.main,
      },
      '& .ql-picker-item:hover': {
        color: theme.palette.secondary.main,
      },
      '& .ql-picker-item.ql-selected': {
        color: theme.palette.secondary.main,
      },
      '& button:hover': {
        color: theme.palette.secondary.main,
        '& .ql-stroke': {
          stroke: theme.palette.secondary.main,
        },
      },

      '& button:focus': {
        color: theme.palette.secondary.main,
        '& .ql-stroke': {
          stroke: theme.palette.secondary.main,
        },
      },
      '& button.ql-active': {
        '& .ql-stroke': {
          stroke: theme.palette.secondary.main,
        },

      },
      '& .ql-stroke': {
        stroke: theme.palette.text.primary,
      },
      '& .ql-picker': {
        color: theme.palette.text.primary,
      },
      '& .ql-picker-options': {
        padding: theme.spacing(2),
        backgroundColor: theme.palette.background.default,
        border: 'none',
        boxShadow: theme.shadows[10],
        borderRadius: theme.shape.borderRadius,
      },
    },
    '& .ql-container': {
      border: 'none',
      '& .ql-editor': {
        fontFamily: theme.typography.fontFamily,
        fontSize: 16,
        color: theme.palette.text.primary,
        '&.ql-blank::before': {
          color: theme.palette.text.secondary,
        },
      },
    },
  },
}));

export default QuillEditor;

Listing 8-13Adding the Styling Components to the QuillEditor Component

这就是羽毛笔编辑器。我们需要创建一个组件,将字节的值转换成人类可读的字符串。

在 utils 文件夹下,创建一个新文件,命名为bytes-to-size.ts; see列表 8-14 。

const bytesToSize = (bytes: number, decimals: number = 2) => {
  if (bytes === 0) return '0 Bytes';

  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
};

export default bytesToSize;

Listing 8-14Creating the bytes-to-size.tsx

bytes-to-size 组件检查字节值,并将其转换为用户在上传文件时易于理解的字符串(即 KB、MB、GB、TB)。

下一个任务是创建一个名为 images 的文件夹。

在文件夹public,下创建子文件夹imagesproducts;并在products文件夹下添加一个。名为add_file.svg.的 svg 文件

文件路径如下:

public ➤ images ➤ products ➤ add_file.svg

你可以在下面我的 GitHub 链接中找到第七章相同文件路径下的图片(见图 8-1 )。下载图像并将其复制或拖动到新创建的。svg 文件。

img/506956_1_En_8_Fig1_HTML.jpg

图 8-1

add_file.svg 的屏幕截图

转到github.com/webmasterdevlin/practical-enterprise-react/blob/master/chapter-7/publimg/add_file.svg

现在,让我们为拖放功能导入另一个库。

安装 React Dropzone

npm i react-dropzone

然后,让我们在文件夹组件中创建另一个文件,并将其命名为files-dropzone.tsx.

文件路径如下:

app ➤ components ➤ files-dropzone.tsx

让我们首先添加命名的导入组件,如清单 8-15 所示。

import React, { useState, useCallback } from 'react';
import clsx from 'clsx';
import { useDropzone } from 'react-dropzone';
import PerfectScrollbar from 'react-perfect-scrollbar';
import FileCopyIcon from '@material-ui/icons/FileCopy';
import MoreIcon from '@material-ui/icons/MoreVert';
import {
  Box,
  Button,
  IconButton,

  Link,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  Tooltip,
  Typography,
  makeStyles,
} from '@material-ui/core';

import bytesToSize from 'utils/bytes-to-size';

Listing 8-15Adding the Named Import Components to FilesDropzone

清单 8-15 从React Dropzone导入了useDropzone,从React Perfect Scrollbar库中导入了PerfectScrollbar

我们还包括了来自材质 UI 图标的附加图标。最后,我们导入了bytesToSize文件。

接下来,让我们定义组件的类型并创建一些本地状态。

type Props = {
  className?: string;
};

const FilesDropzone = ({ className, ...rest }: Props) => {
  const classes = useStyles();
  const [files, setFiles] = useState<any[]>([]);

  //this will be triggered when we drop a file in our component

  const handleDrop = useCallback(acceptedFiles => {
    setFiles(prevFiles => [...prevFiles].concat(acceptedFiles));
  }, []);

  const handleRemoveAll = () => {
    setFiles([]);
  };

  //useDropzone - we're deconstructing it to get the properties of the object it returns
 //we're assigning handleDrop on onDrop

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: handleDrop,
  });

  return (
    <div className={clsx(classes.root, className)} {...rest}>
      <div

        className={clsx({
          [classes.dropZone]: true,
          [classes.dragActive]: isDragActive,
        })}
        {...getRootProps()}
      >
        <input {...getInputProps()} />
        <div>
          <img
            alt="Select file"
            className={classes.image}
            srcimg/add_file.svg"        ---> here we added the svg file
          />
        </div>
        <div>
          <Typography gutterBottom variant="h5">
            Select files
          </Typography>
          <Box mt={2}>
            <Typography color="textPrimary" variant="body1">
              Drop files here or click <Link underline="always">browse</Link>{' '}
              thorough your machine
            </Typography>
          </Box>
        </div>
      </div>

      {files.length > 0 && (

          <PerfectScrollbar options={{ suppressScrollX: true }}>
            <List className={classes.list}>
              {files.map((file, i) => (
                <ListItem divider={i < files.length - 1} key={i}>
                  <ListItemIcon>
                    <FileCopyIcon />
                  </ListItemIcon>
                  <ListItemText
                    primary={file.name}
                    primaryTypographyProps={{ variant: 'h5' }}
                    secondary={bytesToSize(file.size)}
                  />

                  <Tooltip title="More options">
                    <IconButton edge="end">
                      <MoreIcon />
                    </IconButton>
                  </Tooltip>
                </ListItem>
              ))}
            </List>
          </PerfectScrollbar>
          <div className={classes.actions}>
            <Button onClick={handleRemoveAll} size="small">
              Remove all
            </Button>
            <Button color="secondary" size="small" variant="contained">
              Upload files
            </Button>
          </div>

      )}
    </div>
  );
};

Listing 8-16Creating the FilesDropzone Component

接下来,我们为FilesDropzone添加样式组件,如清单 8-17 所示。

const useStyles = makeStyles(theme => ({
  root: {},
  dropZone: {
    border: `1px dashed ${theme.palette.divider}`,
    padding: theme.spacing(6),
    outline: 'none',
    display: 'flex',
    justifyContent: 'center',
    flexWrap: 'wrap',
    alignItems: 'center',
    '&:hover': {
      backgroundColor: theme.palette.action.hover,
      opacity: 0.5,
      cursor: 'pointer',
    },
  },
  dragActive: {
    backgroundColor: theme.palette.action.active,
    opacity: 0.5,
  },
  image: {
    width: 130,
  },
  info: {
    marginTop: theme.spacing(1),
  },
  list: {
    maxHeight: 320,
  },
  actions: {
    marginTop: theme.spacing(2),
    display: 'flex',
    justifyContent: 'flex-end',
    '& > * + *': {
      marginLeft: theme.spacing(2),
    },
  },

}));

export default FilesDropzone;

Listing 8-17Adding the Styling Components for the FilesDropzone

重新运行浏览器,导航到“创建产品”菜单,向下滚动以上传图像。您应该会看到与图 8-2 所示相同的内容。

img/506956_1_En_8_Fig2_HTML.jpg

图 8-2

上传图片的应用用户界面

接下来,我们将导入一个通知库来显示通知消息。

安装通知库

Notistack是一个 React 通知库,可以很容易地显示通知。它还允许用户将snackbarstoasts堆叠在一起:

npm i notistack

我们需要在 app 文件夹或根组件的index.tsx中有一个notistack提供者。

添加 snackbar 提供程序并导入命名组件,如清单 8-18 所示。

import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { BrowserRouter } from 'react-router-dom';
import { SnackbarProvider } from 'notistack';
import { GlobalStyle } from 'styles/global-styles';
import MainLayout from './layouts/main-layout';
import { Routes } from './routes';

export function App() {
  return (
    <BrowserRouter>
      <SnackbarProvider dense maxSnack={3}>
        <Helmet
          titleTemplate="%s - React Boilerplate"
          defaultTitle="React Boilerplate"
        >
          <meta name="description" content="A React Boilerplate application" />
        </Helmet>
        <MainLayout>
          <Routes />
        </MainLayout>
        <GlobalStyle />
      </SnackbarProvider>
    </BrowserRouter>
  );
}

Listing 8-18Wrapping the App Component with SnackbarProvider

完成之后,让我们构建 ProductCreateForm。

更新产品创建表单

让我们通过添加命名的导入组件来开始更新 ProductCreateForm,如清单 8-19 所示。

import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import clsx from 'clsx';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import {
  Box,
  Button,
  Card,
  CardContent,
  CardHeader,
  Checkbox,
  Divider,
  FormControlLabel,
  FormHelperText,
  Grid,
  Paper,
  TextField,
  Typography,
  makeStyles,
} from '@material-ui/core';

import FilesDropzone from 'app/components/files-dropzone';
import QuillEditor from 'app/components/quill-editor';
import { postProductAxios } from 'services/productService';
import { yupProductValidation } from'./schema/yupProductValidation';
import { productDefaultValue } from './schema/productDefaultValue';

Listing 8-19Adding the Named Import Components to ProductCreateForm

你会注意到我们已经从react-router-dom添加了useHistory。我们将需要useHistory来允许用户在创建新产品后浏览产品列表。我们需要Formiknotistack.snackbar

除此之外,我们还导入了FilesDropzoneQuillEditor以及服务{postProductAxios},,这允许我们在数据库中创建或添加新产品。

我们将使用由我们的产品对象的初始值组成的productDefaultValueyupProductValidation来验证创建新产品的需求。

接下来,让我们创建类型定义和一些本地状态;参见清单 8-20 。

const categories = [
  {
    id: 'shirts',
    name: 'Shirts',
  },
  {
    id: 'phones',
    name: 'Phones',
  },
  {
    id: 'cars',
    name: 'Cars',
  },
];

type Props = {
  className?: string;
};

const ProductCreateForm = ({ className, ...rest }: Props) => {
  const classes = useStyles();
  const history = useHistory();

  //we've deconstructed the snackbar to get just the enqueueSnackbar

  const { enqueueSnackbar } = useSnackbar();
  const [error, setError] = useState('');

Listing 8-20Creating the Type Alias and Local States of ProductCreateForm

接下来,让我们使用Formik属性,即initialValues, validationSchema,onSubmit事件处理程序,如清单 8-21 所示。

return (

    /*the required attributes or properties of Formik need to be initialized.
      initialValues, validationSchema, onSubmit.
    */
    <Formik
      initialValues={productDefaultValue}
      validationSchema={yupProductValidation}

      /*   The onSubmit, you can just initially write a function without anything inside.
Usually, I'd write an alert message first to trigger it as a proof of concept. */

      onSubmit={async (values, formikHelpers) => {
        try {
          await postProductAxios(values);

          formikHelpers.setStatus({ success: true });
          formikHelpers.setSubmitting(false);
          enqueueSnackbar('Product Created', {
            variant: 'success',
          });
          history.push('/dashboard/list-products');
        } catch (err) {
          alert('Something happened. Please try again.');
          setError(err.message);
          formikHelpers.setStatus({ success: false });
          formikHelpers.setSubmitting(false);
        }
      }}
    >

      {formikProps => (
        <form
          onSubmit={formikProps.handleSubmit}
          className={clsx(classes.root, className)}
          {...rest}
        >
          <Grid container spacing={3}>
            <Grid item xs={12} lg={8}>
              <Card>
                <CardContent>
                  <TextField
                    error={Boolean(
                      formikProps.touched.name && formikProps.errors.name,
                    )}
                    fullWidth
                    helperText={
                      formikProps.touched.name && formikProps.errors.name
                    }
                    label="Product Name"
                    name="name"
                    onBlur={formikProps.handleBlur}
                    onChange={formikProps.handleChange}
                    value={formikProps.values.name}
                    variant="outlined"
                  />
                  <Box mt={3} mb={1}>
                    <Typography variant="subtitle2" color="textSecondary">
                      Description
                    </Typography>
                  </Box>
                  <Paper variant="outlined">
                    <QuillEditor
                      className={classes.editor}
                      value={formikProps.values.description}
                      onChange={(value: string) =>
                        formikProps.setFieldValue('description', value)
                      }
                    />
                  </Paper>

                  {formikProps.touched.description &&
                    formikProps.errors.description && (
                      <Box mt={2}>
                        <FormHelperText error>
                          {formikProps.errors.description}
                        </FormHelperText>
                      </Box>
                    )}
                </CardContent>
              </Card>
              <Box mt={3}>
                <Card>
                  <CardHeader title="Upload Images" />
                  <Divider />
                  <CardContent>
                    <FilesDropzone />
                  </CardContent>
                </Card>
              </Box>
              <Box mt={3}>
                <Card>
                  <CardHeader title="Prices" />
                  <Divider />

                  <CardContent>
                    <Grid container spacing={3}>
                      <Grid item xs={12} md={6}>
                        <TextField
                          error={Boolean(
                            formikProps.touched.price &&
                              formikProps.errors.price,
                          )}
                          fullWidth
                          helperText={
                            formikProps.touched.price &&
                            formikProps.errors.price
                              ? formikProps.errors.price
                              : 'If you have a sale price this will be shown as old price'
                          }
                          label="Price"
                          name="price"
                          type="number"
                          onBlur={formikProps.handleBlur}
                          onChange={formikProps.handleChange}
                          value={formikProps.values.price}
                          variant="outlined"
                        />
                      </Grid>

                      <Grid item xs={12} md={6}>
                        <TextField
                          error={Boolean(
                            formikProps.touched.salePrice &&
                              formikProps.errors.salePrice,
                          )}
                          fullWidth
                          helperText={
                            formikProps.touched.salePrice &&
                            formikProps.errors.salePrice
                          }
                          label="Sale price"
                          name="salePrice"
                          type="number"
                          onBlur={formikProps.handleBlur}
                          onChange={formikProps.handleChange}
                          value={formikProps.values.salePrice}
                          variant="outlined"
                        />
                      </Grid>
                    </Grid>
                    <Box mt={2}>
                      <FormControlLabel
                        control={
                          <Checkbox
                            checked={formikProps.values.isTaxable}
                            onChange={formikProps.handleChange}
                            value={formikProps.values.isTaxable}
                            name="isTaxable"
                          />
                        }
                        label="Product is taxable"
                      />
                    </Box>

                    <Box mt={2}>
                      <FormControlLabel
                        control={
                          <Checkbox
                            checked={formikProps.values.includesTaxes}
                            onChange={formikProps.handleChange}
                            value={formikProps.values.includesTaxes}
                            name="includesTaxes"
                          />
                        }
                        label="Price includes taxes"
                      />
                    </Box>
                  </CardContent>
                </Card>
              </Box>
            </Grid>
            <Grid item xs={12} lg={4}>
              <Card>
                <CardHeader title="Organize" />
                <Divider />
                <CardContent>
                  <TextField
                    fullWidth
                    label="Category"
                    name="category"
                    onChange={formikProps.handleChange}
                    select
                    SelectProps={{ native: true }}
                    value={formikProps.values.category}
                    variant="outlined"
                  >
                    {categories.map(category => (
                      <option key={category.id} value={category.id}>
                        {category.name}
                      </option>
                    ))}
                  </TextField>

                  <Box mt={2}>
                    <TextField
                      error={Boolean(
                        formikProps.touched.productCode &&
                          formikProps.errors.productCode,
                      )}
                      fullWidth
                      helperText={
                        formikProps.touched.productCode &&.
                        formikProps.errors.productCode
                      }
                      label="Product Code"
                      name="productCode"
                      onBlur={formikProps.handleBlur}
                      onChange={formikProps.handleChange}
                      value={formikProps.values.productCode}
                      variant="outlined"
                    />
                  </Box>
                  <Box mt={2}>
                    <TextField
                      error={Boolean(
                        formikProps.touched.productSku &&
                          formikProps.errors.productSku,
                      )}
                      fullWidth
                      helperText={
                        formikProps.touched.productSku &&
                        formikProps.errors.productSku
                      }
                      label="Product Sku"
                      name="productSku"
                      onBlur={formikProps.handleBlur}
                      onChange={formikProps.handleChange}
                      value={formikProps.values.productSku}
                      variant="outlined"
                    />
                  </Box>
                </CardContent>
              </Card>
            </Grid>
          </Grid>

          {error && (
            <Box mt={3}>
              <FormHelperText error>{error}</FormHelperText>
            </Box>
          )}
          <Box mt={2}>
            <Button
              color="primary"
              variant="contained"
              type="submit"
              disabled={formikProps.isSubmitting}
            >
              Create product
            </Button>
          </Box>
        </form>
      )}
    </Formik>
  );
};

Listing 8-21Adding the Return Component of the ProductCreateForm

好的,那么清单 8-21 中发生了什么?我们已经把一切都包在Formik.下了

因为我们这样做了,,所以我们需要使用它的默认属性,即:

initialValues:我们正在通过productDefaultValues.

validationSchema:我们正在通过yupProductValidation.

一开始,我只是写一个函数,里面什么都没有。我只是设置了一个警报来触发它,并检查它是否正在发射。

Formik:这个组件发出formikProps(您可以随意命名它,但是我更喜欢这样命名,这样它的来源就一目了然了)。

formikProps,中,我们找到了 HTML 表单的FormonSubmit,这将触发Formik.onSubmit

这里,我们绑定了对象的名字。TextField来自于 Material-UI 核心。

先简单解释一下TextField里面的必备零件。如果您再次查看它,您会看到以下属性:

formikProps.touched.name && formikProps.errors.name,

label="Product Name"
name="name"
onBlur={formikProps.handleBlur}
onChange={formikProps.handleChange}
value={formikProps.values.name}

formikProps.touched.name:此时点击TextField of name.

当出现错误时,例如,您超出了允许的字符数或将其留空。

formikProps.handleBlur:当你离开TextField时触发,例如,点击后,你离开它去另一个字段。

formikProps.handleChange:每当你在键盘上敲击或输入什么的时候,这个就会更新名字的值。这将覆盖formikProps.values.name .中现有的将是我们将在现场看到的数据。

这看起来很复杂,但实际上,如果我们自己去做,事情会更复杂。Formik 在其网站 formik.org 上的行动号召是“在无泪的 React 中构建形式”,这是有原因的

如果你已经体验过编写带有验证和绑定的表单——在这种情况下你会看到输入的变化——那么你就会知道从头开始是一件相当痛苦且不好玩的事情。这是因为我们本质上需要做的是创建一个双向绑定。

然而,React 的主要问题是它是用单向数据流设计的。React 不同于 Svelte、Vue.js 和 Angular 等其他框架,在这些框架中,双向数据绑定很容易实现。

简单地说,在双向数据绑定中,当您将模型绑定到视图时,您会知道当您更改视图中的某些内容时,它会反映在模型中。所以基本上,在模型和视图之间有一个双向的数据流。

一个合理的用例是当用户更新或编辑他们的个人资料时,我们知道已经有来自 web 服务的数据输入。

当用户在轮廓输入表单中输入时,对象模型的值也被编辑。在 React 中,不编写大量代码很难做到这一点。

这就是为什么有 React 库可以用来创建表单,比如 React Forms、Formsy 和 Redux Form,但最流行的是 Formik,因为它非常容易使用和理解。

我强烈建议您使用 Formik 或任何这些表单库。尽可能不要实现或者从头开始写。

独自做这件事的另一个缺点是很难长期维护它,包括当你不得不把项目交给新的开发人员时。

大多数时候,最好使用流行的库,因为开发者可能已经知道如何使用或熟悉这些库,或者很容易从文档中理解或从在线社区团体获得帮助。

好了,独白到此为止。让我们回到TextField中的onChange,因为我想指出这里正在发生的另一件事。

您会注意到,我们只是将formikProps.handleChange放在onChange上,更改就会被触发。这是因为formikProps.handleChange签名与onChange期望的功能相匹配。

在这种情况下,这里的onChange发出一个事件。把你的鼠标悬停在它上面,你就会看到它。

但是寻找QuillEditor.下面的onChange,它发出一个字符串。所以它是不同的,这就是为什么handleChange在这里不工作:

            <QuillEditor
                      className={classes.editor}
                      value={formikProps.values.description}
                      onChange={(value: string) =>
                   formikProps.setFieldValue('description', value)
                      }
                    />

现在的问题是,我们怎么知道它不会起作用?当我们尝试它时,我们会知道,期待它工作,并得到一个错误。 是啊,我知道。但那是我的经验,意思是 handleChange, 在这种情况下不起作用。

但是如果你遇到这种问题,你正在使用的 onChange 可能会发出不同的类型。你需要做的是使用**formik props . setfield value .**这里我们需要传递两个参数:

string:哪一个是您的属性的名称

原始类型:描述的

你可以登记你的modelsproduct-type.ts.

每当你遇到onChange,时,首先做一个控制台日志,看看它发出的是什么类型或对象,尤其是使用 JavaScript 时。对于 TypeScript,您可以将鼠标悬停在上面查看。

img/506956_1_En_8_Figb_HTML.jpgTextField中需要注意的另一件事是,我们编写它的方式有一个模式,我们可以从TextField中创建一个抽象,并把它放在一个单独的文件中,以整理我们的ProductCreateForm.

运行或刷新您的应用。创建或添加新产品,并检查它是否显示在所有产品页面上。

img/506956_1_En_8_Figc_HTML.jpg

为了你的活动

  1. 创建一个删除一个或多个产品的新函数。

  2. 使用在selectedProducts中存储一个或多个产品 id 的handleSelectOneProduct功能。

  3. 通过创建一个接受 string 类型 id 的deleteProductAxios来更新postService.ts

  4. 这个新函数应该使用一个array.map进行循环,同时向json-server.发送一个删除请求,确保不仅在服务器中删除它,而且在 UI 中也删除它。

摘要

这一章我们已经学完了,我们在这里涉及了很多内容,特别是学习如何在 React 中使用 Formik 构建输入表单,以及表单上的 Yup 验证。最后,我们谈到了如何在 Material-UI 组件和其他库(如 Quill editor)的帮助下以智能的方式创建复杂的数据表。

在下一章,我们将学习一项重要的技能:状态管理,或者说如何使用 Redux 工具包 管理我们的状态。

九、通过 Redux 工具包使用 Redux 管理状态

在前一章中,我们构建了一个产品仪表板,并使用 Formik 作为输入表单,使用 Yup 库来验证用户的输入。创建和验证表单是任何前端开发人员都必须具备的基本和常见技能,但是现在,我们将继续学习更复杂的开发人员技能,即使用 Redux with Redux 工具包 管理应用的全局状态。

React 和 Redux 是状态管理的绝佳组合,尤其是在构建企业级应用时。但是配置 Redux 的复杂过程成了很多开发者的绊脚石。许多开发人员讨厌在 React 应用中设置 Redux 的复杂性。

于是,Redux 工具包 就诞生了。

正如其网站 redux-toolkit.js.org 所定义的,Redux 工具包 是“用于高效 Redux 开发的官方的、固执己见的、包含电池的工具集”以前称为 Redux Starter Kit,Redux 工具包 附带了有用的库,使 React 开发人员的生活更加轻松。

简而言之,Redux 工具包 现在是推荐使用 Redux 的方式。

在我们继续学习如何使用 Redux 工具包 所需的基本概念之前,我们先来谈谈 Redux。我们还将看看快速使用 Redux 工具包 所需的一些重要的 Redux 术语。最后,我们将通过使用 CodeSandbox 的快速 Redux 实现来完成本章。

Redux 概述

根据其官方网站,Redux 是“JavaScript 应用的可预测状态容器。”它主要用于管理单个不可变状态树(对象)中整个应用的状态。状态的任何变化都会创建一个新对象(使用操作和减速器)。我们将在本章后面详细讨论核心概念。

https://redux.js.org/ 的 Redux 网站如图 9-1 所示。

img/506956_1_En_9_Fig1_HTML.jpg

图 9-1

Redux 网站位于 redux. js. org/

如果我们正在构建一个大的 React 项目,我们通常会使用 Redux。对于较小的应用,我不认为你需要 Redux。现在让我们讨论一下为什么我们要在应用中使用 Redux。

为什么要用 Redux?

首先,我们的应用将有一个单独的状态存储。想象一下,商店拥有我们的组件需要到达的所有数据或状态。这非常方便,特别是对于大型应用,因为我们可以将大部分——不一定是全部——数据保存在一个位置,而不必担心必须将 props 发送到组件树的多个层次。

将数据从 React 组件树中的一个组件传递到另一个组件的过程通常有多层深,这种过程称为适当的钻取。是的,对于许多开发人员来说,这可能是一个相当头疼的问题。

这是一个常见的问题,您需要将数据从一个 React 组件传递到另一个组件,但是您必须通过许多其他不需要数据的组件才能到达您想要的目的地或组件。

是的,有大量的数据传递给各种组件,这些组件并不真正需要呈现数据,而只是将数据传递给下一个组件,直到数据到达需要它的组件。

假设您有这个 web 应用,并且有一个大的<div>或组件。

这个组件是许多组件的父组件,这些组件是子组件。这在图 9-2 中进行了说明,其中我们将容器作为父组件,并将两个组件(仪表板和顶栏)作为下一级组件。

img/506956_1_En_9_Fig2_HTML.jpg

图 9-2

React 中的适当钻孔

在仪表板下,我们有以下组件:

SidebarMenuComponent X.

在顶栏下,我们按级别深度顺序排列了以下组件:

Component Y ➤ ComponentComponent

同步仪表板和顶栏组件可以很快完成——您只需要传递或发出一个事件,例如,从仪表板到容器,然后从容器到顶栏,反之亦然。

图 9-2 是 React 中支柱钻孔的图示。

公平地说,如果我们只是向下传递两层甚至三层数据,适当的钻探并不是那么糟糕。追踪数据流是容易做到的。但是,如果我们已经钻得太频繁,达到十层或更多层,问题就可能出现。

案例场景 :

如果您有一个四层深度的组件,并且您需要将数据共享或传递给另一个三层或四层深度的元素,该怎么办?

要解决的问题 : 你需要向组件 X 和组件 y 渲染或传递相同类型的数据,换句话说,生成的数据应该总是相同或同步的。如果组件 X 中有变化,它们也应该反映在组件 y 中。

这有点复杂,因为组件 X 和 Y 与它们的父组件(容器)是不同级别的。此外,随着应用的增长,还有一个可维护性的问题,因为最终,跟踪我们的应用发生的事情会变得更加困难。

在我们的应用中有一个解决这类问题或需求的方法。通常,这是状态管理的工作。

状态管理库通常允许组件访问存储。存储是状态的内存存储,任何组件都可以全局访问它。

存储还允许数据对其他组件进行 React。假设这个州有任何变化。在这种情况下,任何使用 状态的组件都会重新呈现 DOM 差异,或者无论组件的级别有多深,状态发生了什么变化都会反映在 UI 中,如图 9-3 所示,一个 React 式状态管理库。

img/506956_1_En_9_Fig3_HTML.jpg

图 9-3

React 状态管理存储

每个组件 都可以 直接访问商店;无需担心向上传递或返回到父组件并再次向下钻取以将数据或状态分配给另一个更深层次的组件。

可以将存储看作是内存中的本地存储,您可以使用它来访问状态。

状态管理本质上是一个大型 JavaScript 对象存储,是大型企业和现代 JavaScript 应用(如 Angular、Vue.js、React 和其他 JavaScript 框架)的流行解决方案,用于快速呈现不同组件中的状态。

为了更好地理解 Redux 中的状态管理,让我们看看 Redux 中使用的术语。

Redux的核心部件:

调度:触发动作的调度。对于接收方来说,接收一个动作,不能只是用一个普通的函数,然后把动作发送给 reducer。

您需要一个 dispatcher 函数将动作发送给 reducer。想象一下,调度员是你友好的 UPS 或 FedEx 快递员,负责给你送包裹。另一种思考调度员的方式是枪,而行动是子弹。枪需要击发,子弹才能释放到它的目标,也就是减速器。

Reducers : Reducers 的工作是修改店铺,也是唯一可以修改店铺的人。React-Redux 应用中可以有任意多的 reducers 来更新商店的状态。

存储:同步不同组件中所有状态的应用的全局状态。它是 React 式的;这就是为什么它可以同步各种组件中的所有状态。

选择器:选择器取一段状态呈现在 UI 中。本质上,选择器是从存储中获取状态的一部分或从存储中访问状态的函数。您不能只将整个商店导入组件中;您需要使用选择器从存储中获取状态。

现在我们已经对 Redux 有了一个大概的了解,也知道了为什么要在我们的应用中使用它,让我们从 Redux 工具包 (RTK)开始。

Redux 工具包

Redux 工具包 是一种自以为是的编写 Redux 的方式,因为社区意识到开发人员都有自己的 React-Redux 应用实现。

在创建 Redux 工具包(简称 RTK)之前,没有在 React 应用中实现或配置 Redux 的标准指南。它已经预装了有用的库,如 Redux、Immer、Redux Thunk 和 Reselect。

以下是每个库的简短描述:

Immer :处理店内不变性。

Redux :用于状态管理。

Redux Thunk :处理异步动作的中间件。RTK 提供默认选项,但如果您愿意,也可以使用 Redux-Saga。

重新选择:简化减速器功能。让我们能从全球商店中分得一杯羹。

图 9-4 如果想详细了解 RTK 的更多信息,是 Redux 工具包 的网站。

img/506956_1_En_9_Fig4_HTML.jpg

图 9-4

Redux 工具包网站

动作类型:避免我们的动作名称出现打字错误。

动作:携带修改店铺的说明。一个动作给 reducer 带来了关于如何处理存储内部状态的指令。

两种类型的操作:

  • 非异步动作、同步动作或没有副作用的动作:一个很好的例子是,如果你想在商店中保存一组使用复选框选择的项目。此操作不需要 HTTP 请求,因为您要保存在存储中的数据来自复选框的详细信息。

  • 异步动作或者有副作用的动作:这种动作通常需要一个 axios 函数来发送 HTTP 请求,例如,将 web 服务的响应保存在存储中,然后在 UI 中呈现。

  • 副作用:是响应一个 Redux 动作可能发生也可能不发生的过程。把它想象成一个行动,在你的行动得到回应之前,你不太确定接下来会发生什么。

例如,当你向 web 服务器发送一个请求时,你还不知道你将得到一个 200 OK 还是一个 404 还是一个错误 500。这种“走出去”并等待行动回应的过程被称为副作用,因为它本质上是我们无法“控制”或直到我们得到它才知道的东西。

为了更好或更全面地了解 Redux 状态管理是如何工作的,让我们看一下图 9-5——React 应用中 Redux 状态管理的流程。

冗余状态管理流程

img/506956_1_En_9_Fig5_HTML.jpg

图 9-5

React 应用内部的 Redux 状态管理流程

我们有从商店获取数据的组件。商店在初始设置时有一些默认属性,每个值都必须初始化,以免得到未定义的返回。

选择器:您会注意到选择器正在从存储中获取或选择一个状态。它是否是一个空的初始值并不重要,因为一旦存储加载了数据或状态,我们的组件就会被重新呈现,我们会在 UI 中看到新的值。

Dispatcher——异步和同步动作:组件可以通过 Dispatcher 发送一个动作。动作可以是异步的,也可以是同步的。

同步:有时也叫 非异步或非异步 ,意为同时发生。

异步:不同时发生。简单来说,两者的主要区别在于的等待时间。 在同步(非异步)代码过程中,有一个分步操作。

同时,异步代码通常是我们自己不处理的操作,比如发出 API 请求。我们提出一个请求;我们等待回应。但是在等待回复的同时,我们可以做另一个代码。当我们最终得到 API 响应时,就是我们处理它的时候。

减速器:同步或“非异步”动作将直接作用于减速器。缩减器然后将基于它们的动作修改或改变存储。

改变或修改可以意味着存储对象内的名字空间或模块的状态的改变。

那么什么是名称空间或者模块呢? 这些是彼此逻辑分离或状态分组。例如,您有一个配置文件的状态、一个付款状态、另一组对象的状态等。当我们开始实现 Redux 时,我们将在应用本身中讨论这一点。

异步:那么调度器发送一个异步动作呢?然后这个异步动作直接进入副作用。副作用会向服务发送请求。

副作用从 web 服务获得成功(例如,200 OK)或失败响应(例如,4xx 或 5xx)。无论我们从服务中得到什么样的 React 或行动,副作用都会把它发送给减少者。缩减者将再次决定如何处理他们收到的动作。

另一件要注意的事情是,副作用的动作可以根据服务的响应而改变。在操作中,我们需要使用 try-catch 块。例如,在 try 中,如果是 200,就这样做,如果是 400,就这样做,等等。

在 CodeSandbox 中使用 RTK

如果你想在应用中实现 RTK 之前先体验一下,请访问这个神奇的网站 https://codesandbox.io/s/redux-toolkit-matchers-example-e765q 来看看 Redux 的快速实现。

图 9-6 是 CodeSandbox 网站上 Redux 工具包 Matchers 示例的截图。

img/506956_1_En_9_Fig6_HTML.jpg

图 9-6

codesandbox.io/redux-toolkit-matchers-example-e765q 截图

在侧边栏菜单中,注意两个文件夹:应用和功能。这些是 Redux 实现,根据 Redux 工具包 的创建者,这就是我们应该如何构建 React-Redux 应用。

例如,创建放置名称空间的文件夹功能,并在 app 文件夹中设置您的商店。商店和减压器不同,但是在商店里你可以找到所有的减压器。

img/506956_1_En_9_Fig7_HTML.jpg

图 9-7

codesandbox.io 上的存储和 reducers 的屏幕截图

每个名称空间或模块都有它的缩减器。在流程的末端,也就是商店,是你组合所有减压器的地方。

在下一章,我们将使用 Redux DevTools。您将看到 React 以及 Angular 开发人员喜欢 Redux 的一些原因:

  • 时间旅行调试工具

  • 状态是可预测的,所以很容易测试

  • 集中式状态和逻辑

  • 灵活的用户界面适应性

顺便说一下,Angular 中还有一个 Redux 实现,叫做 NgRx。是 Redux 加 RxJS。还有一些其他的 Angular 应用的状态管理库,但我相信 NgRx 是目前最流行的一个。

摘要

在本章中,我们讨论了使用 Redux 和 Redux 工具包 进行状态管理。我们了解到 Redux 工具包 是为了简化 Redux 的设置而开发的,尤其是在大型 React 应用中。

我们还了解到,当我们需要将数据传递给几层深的组件和另一个组件树中的组件时,Redux 可能是一种有效而方便的方法来解决我们的正确钻取问题。最后,我们展示了 https://codesandbox.io/ 网站,在那里我们可以进行快速的网络开发,并在学习 RTK 时获得即时反馈。

在下一章中,我们将开始在项目应用中实现我们在这里讨论的内容。

十、设置 Redux 工具包并调度一个异步动作

在前一章中,我们学习了使用 Redux 工具包 管理状态的概念。我们讨论了 React 应用中的 prop drilling,并展示了在 React 中编写 Redux 时的模式。

现在,正如承诺的那样,在这一章中,我们在这里开始变脏:

  • 设置 Redux 工具包

  • 向缩减器分派异步动作

  • 将商店的状态呈现给我们的用户界面,特别是日历视图

创建日历视图组件

为此,我们现在将创建日历视图组件。

打开仪表板目录,我们将创建两个文件夹,calendarCalendarView,以及index.tsx文件:

dashboard ➤ calendar ➤ CalendarView ➤ index.tsx

打开index.tsx file,现在只添加一个 h1 标签,如清单 10-1 所示。

import React from 'react';

const Index = () => {
  return (
    <div>
      <h1>Calendar Works!</h1>
    </div>
  );
};

export default Index;

Listing 10-1Creating index.tsx of CalendarView

我们的下一个练习是更新路线,因为我们需要在 routes.tsx 中注册日历组件。

更新路线

转到routes.tsx,并注册CalendarView.我们可以把它放在ProductCreateView之后,如清单 10-2 所示。

<Route exact path={path + '/calendar'}
                  component={lazy(
                  () => import('./views/dashboard/calendar/CalendarView'),
                  )} />

Listing 10-2Registering the CalendarView in routes.tsx

更新仪表板边栏导航

在 routes 文件夹中注册日历后,我们将向仪表板侧栏导航添加一个日历图标。

转到dashboard-sidebar-navigation进行更新。首先,从 React Feather 添加日历图标。同样,我们将其重命名为CalendarIcon.

import { PieChart as PieChartIcon,
        ShoppingCart as ShoppingCartIcon,
        ChevronUp as ChevronUpIcon,
        ChevronDown as ChevronDownIcon,
        Calendar as CalendarIcon,
        List as ListIcon,
        FilePlus as FilePlusIcon,
        LogOut as LogOutIcon,
} from 'react-feather';

Listing 10-3Importing the Calendar Component to the dashboard-sidebar-navigation

既然我们已经将它添加到了DashboardSidebarNavigation组件中,让我们将另一个菜单放在 Create Product 下面,如清单 10-4 所示。

<ListSubheader>Applications</ListSubheader>
              <Link className={classes.link} to={`${url}/calendar`}>
              <ListItem button>
                <ListItemIcon>
                  <CalendarIcon/>
                </ListItemIcon>
                <ListItemText primary={'Calendar'} />
              </ListItem>
              </Link>

Listing 10-4Creating a Calendar Icon Menu in the dashboard-sidebar-navigation

刷新浏览器看到如图 10-1 所示的日历菜单。

img/506956_1_En_10_Fig1_HTML.jpg

图 10-1

在用户界面中显示日历

既然我们已经看到它正在工作,让我们为我们的日历建立模型。在 models 文件夹中,添加一个文件并将其命名为calendar-type.ts.我们将创建 CalendarView 的形状或模型类型,如清单 10-5 所示。

export type EventType = {
  id: string;
  allDay: boolean;
  color?: string;
  description: string;
  end: Date;
  start: Date;
  title: string;
};

//union type 

export type ViewType =
  | 'dayGridMonth'
  | 'timeGridWeek'
  | 'timeGridDay'
  | 'listWeek';

Listing 10-5Creating the Shape or Model Type of the CalendarView

好了,是时候让减压器进入商店了。记住 Redux 中的 reducers 是我们用来管理应用状态的。

还原剂

我们将首先进行一些重构,但我们将确保不会丢失 Redux 的任何核心功能。

打开reducers.tsx并用清单 10-6 所示的代码替换它。插入的注释是对每个问题的简要解释。

/* Combine all reducers in this file and export the combined reducers.
combineReducers - turns an object whose values are different reducer functions into a single reducer function. */

import { combineReducers } from '@reduxjs/toolkit';

/*  injectedReducers - an easier way of registering a reducer */
const injectedReducers = {
  //reducers here to be added one by one.
};

/* combineReducers requires an object.we're using the spread operator (...injectedReducers) to spread out all the Reducers */

const rootReducer = combineReducers({
  ...injectedReducers,
});

/* RooState is the type or shape of the combinedReducer easier way of getting all the types from this rootReduder instead of mapping it one by one. RootState - we can use the Selector to give us intelli-sense in building our components. */

export type RootState = ReturnType<typeof rootReducer>;
export const createReducer = () => rootReducer;

Listing 10-6Refactoring the reducers.ts

接下来,我们还需要更新商店并简化它。目前有 Saga 实现,但我们不需要它。我们将使用一个更简单的副作用 Thunk。

打开configureStore.ts并用下面的代码重构,如清单 10-7 所示。

/*Create the store with dynamic reducers */

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { forceReducerReload } from 'redux-injectors';

import { createReducer } from './reducers';

export function configureAppStore() {
  const store = configureStore({

    /*reducer is required. middleware, devTools, and the rest are optional */
    reducer: createReducer(),
    middleware: [
      ...getDefaultMiddleware({
        serializableCheck: false,
      }),
    ],
    devTools: process.env.NODE_ENV !== 'production',
  });

  /* Make reducers hot reloadable, see http://mxs.is/googmo istanbul ignore next */

  if (module.hot) {
    module.hot.accept('./reducers', () => {
      forceReducerReload(store);
    });
  }

  return store;
}

Listing 10-7Refactoring the configureStore.ts

让我们进一步检查清单 10-8 中发生了什么。

在商店设置中,我们使用来自Redux 工具包configureStoregetDefaultMiddleware

如果您将光标悬停在 getDefaultMiddleware **,**上,您将看到这条消息:“它返回一个包含 ConfigureStore()安装的默认中间件的数组。如果您希望使用自定义中间件阵列配置您的商店,但仍保持默认设置,这将非常有用。”

来自 redux-injectors 的是我们的热重装。

从 rootReducer 返回 combinedReducers 的函数。

是一组插件或中间件。

我们需要通过一个提供者将它注入到我们的组件中。

在那之后,我们去

 src ➤ index.tsx

在 React 中,如果您看到一个名称提供者作为后缀的组件,这意味着您必须将它包装在根组件中。

提供者组件提供对整个应用的访问。在清单 10-8 中,我们将根组件(index.tsx)包装在提供者组件中。

/*wrapping the root component inside a provider gives all the component an access
 to the provider component or the whole application */

const ConnectedApp = ({ Component }: Props) => (
  <Provider store={store}>
    <HelmetProvider>
      <Component />
    </HelmetProvider>
  </Provider>
);

Listing 10-8Wrapping the Root Component (index.tsx) Inside a Provider Component

该提供程序是从 React-Redux 派生的。这是样板文件为我们设置的。

注意,提供者有一个必需的属性商店,我们将在configureStore.ts.中创建的商店传递给它,这就是为什么我们从store/configureStore.中导入了configureAppStore

这使得store成为事实的单一来源——可用于我们应用中的所有组件。

接下来,我们需要更新根组件的 index.tsx,如清单 10-9 所示。请记住,这个 index.tsx 是应用的入口文件——仅用于设置和样板代码。

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'react-quill/dist/quill.snow.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import * as serviceWorker from 'serviceWorker';
import 'sanitize.css/sanitize.css';

// Import root app
import { App } from 'app';
import { HelmetProvider } from 'react-helmet-async';
import { configureAppStore } from 'store/configureStore';

// Initialize languages
import './locales/i18n';

const store = configureAppStore();
const MOUNT_NODE = document.getElementById('root') as HTMLElement;

interface Props {
  Component: typeof App;
}

/*wrapping the root component inside a provider gives all the component an access
 to the provider component or the whole application */

const ConnectedApp = ({ Component }: Props) => (
  <Provider store={store}>
    <HelmetProvider>
      <Component />
    </HelmetProvider>
  </Provider>
);
const render = (Component: typeof App) => {
  ReactDOM.render(<ConnectedApp Component={Component} />, MOUNT_NODE);
};

if (module.hot) {

  // Hot reloadable translation json files and app
  // modules.hot.accept does not accept dynamic dependencies,
  // have to be constants at compile-time

  module.hot.accept(['./app', './locales/i18n'], () => {
    ReactDOM.unmountComponentAtNode(MOUNT_NODE);
    const App = require('./app').App;
    render(App);
  });
}

render(App);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 10-9Updating the index.tsx of the Root Component

在这之后,让我们只是做一点清理。

清理时间

删除存储文件夹中的文件夹_tests_。我们还将取出types文件夹,因为我们已经有了一个RootState.

img/506956_1_En_10_Fig2_HTML.jpg

图 10-2

删除存储和类型文件夹中的 tests 文件夹

接下来,找到 utils 文件夹并删除除了bytes-to-size.ts文件之外的所有内容。

img/506956_1_En_10_Fig3_HTML.jpg

图 10-3

删除 utils 文件夹

更新 Axios

就这么定了。我们现在去 axios 更新端点,如清单 10-10 所示。

Open src ➤ api ➤ axios.ts

export default api;

export const EndPoints = {
  sales: 'sales',
  products: 'products',
  events: 'event',
};

Listing 10-10Updating the Endpoints in axios.ts

然后让我们添加另一组假数据,在db.json.产品后添加以下事件数据。事件数组包含七个事件对象。

复制清单 10-11 中的代码,并将其添加到 db.json 文件中。

"events": [
    {
      "id": "5e8882e440f6322fa399eeb8",
      "allDay": false,
      "color": "green",
      "description": "Inform about new contract",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Call Samantha"
    },
    {
      "id": "5e8882eb5f8ec686220ff131",
      "allDay": false,
      "color": null,
      "description": "Discuss about new partnership",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Meet with IBM"
    },

    {
      "id": "5e8882f1f0c9216396e05a9b",
      "allDay": false,
      "color": null,
      "description": "Prepare docs",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "SCRUM Planning"
    },
    {
      "id": "5e8882f6daf81eccfa40dee2",
      "allDay": true,
      "color": null,
      "description": "Meet with team to discuss",
      "end": "2020-12-12T12:30:00-05:00",
      "start": "2020-11-11T12:00:27.87+00:20",
      "title": "Begin SEM"
    },

    {
      "id": "5e8882fcd525e076b3c1542c",
      "allDay": false,
      "color": "green",
      "description": "Sorry, John!",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Fire John"
    },
    {
      "id": "5e888302e62149e4b49aa609",
      "allDay": false,
      "color": null,
      "description": "Discuss about the new project",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Call Alex"
    },

    {
      "id": "5e88830672d089c53c46ece3",
      "allDay": false,
      "color": "green",
      "description": "Get a new quote for the payment processor",
      "end": "2021-01-01T12:00:27.87+00:20",
      "start": "2021-01-01T12:00:27.87+00:20",
      "title": "Visit Samantha"
    }
  ]

Listing 10-11Adding the Events Object in db.json

实现 Redux 工具包

好了,现在让我们来做实现 Redux 工具包的有趣部分。

我们将在这个应用中使用两种实现,这样您将了解两者是如何工作的,并且您可以更容易地使用现有的 React–Redux 工具包 项目。

在您很快会遇到的许多不同的项目中,实现几乎是相同的;有时,这只是文件夹结构和创建的文件数量的问题。

在这里,我们将把所有的动作和 reducers 写在一个文件中,我们将把它命名为 calendarSlice.ts。

src目录中,创建一个新文件夹,并将其命名为features; this,这是我们将实现 Redux 的地方。

features,里面新建一个文件夹并命名为calendar.calendar里面新建一个文件名为calendarSlice.ts.

Redux 工具包 建议在名称空间中添加后缀 Slice。

img/506956_1_En_10_Figa_HTML.jpg

打开calendarSlice文件,让我们添加一些命名的导入(清单 10-12 )。

/*PayloadAction is for typings  */
import {
  createSlice,
  ThunkAction,
  Action,
  PayloadAction,
} from '@reduxjs/toolkit';

import { RootState } from 'store/reducers';
import { EventType } from 'models/calendar-type';
import axios, { EndPoints } from 'api/axios';

Listing 10-12Adding the Named Import Components in calendarSlice

接下来,让我们在 calendarSlice 中进行键入,如清单 10-13 所示。

/*typings for the Thunk actions to give us intlelli-sense */
export type AppThunk = ThunkAction<void, RootState, null, Action<string>>;

/*Shape or types of our CalendarState  */

interface CalendarState {
  events: EventType[];
  isModalOpen: boolean;
  selectedEventId?: string;     //nullable
  selectedRange?: {                       //nullable
    start: number;
    end: number;
  };

  loading: boolean;  //useful for showing spinner or loading screen

  error: string;
}

Listing 10-13Creating the Typings/Shapes in calendarSlice

仍然在我们的 calendarSlice 文件中,我们将在 initialState 中初始化一些值,如清单 10-14 所示。

/*initialState is type-safe, and it must be of a calendar state type.
  It also means that you can't add any other types here that are not part of the calendar state we’ve already defined.  */

const initialState: CalendarState = {
  events: [],
  isModalOpen: false,
  selectedEventId: null,
  selectedRange: null,
  loading: false,
  error: '',
};

Listing 10-14Adding the Default Values of the initialState

然后,我们继续创建namespacecreateSlice,,如清单 10-15 所示。我们将命名空间和 createSlice 添加到 calendarSlice。

const calendarNamespace = 'calendar';

/*Single-File implementation of Redux-Toolkit*/

const slice = createSlice({

  /*namespace for separating related states. Namespaces are like modules*/
  name: calendarNamespace,

  /*initialState is the default value of this namespace/module and it is required.*/

  initialState, // same as initialState: initialState

  /*reducers --  for non asynchronous actions. It does not require Axios.*/
  /* the state here refers to the CalendarState */

  reducers: {
    setLoading(state, action: PayloadAction<boolean>) {
      state.loading = action.payload;
    },
    setError(state, action: PayloadAction<string>) {
      state.error = action.payload;
    },
    getEvents(state, action: PayloadAction<EventType[]>) {
      state.events = action.payload;
    },
  },
});

/* Asynchronous actions. Actions that require Axios (HTTP client)
 or any APIs of a library or function that returns a promise. */

export const getEvents = (): AppThunk => async dispatch => {
  dispatch(slice.actions.setLoading(true));
  dispatch(slice.actions.setError(''));
  try {
    const response = await axios.get<EventType[]>(EndPoints.events);
    dispatch(slice.actions.getEvents(response.data));
  } catch (error) {
    console.log(error.message);
    dispatch(slice.actions.setError(error.message));
  } finally {
    dispatch(slice.actions.setLoading(false));
  }
};

export default slice.reducer;

Listing 10-15Adding the Namespace and createSlice

createSlice是一个大对象,要求我们在nameinitialStatereducers中放置一些东西。

这里的reducers是不需要 axios 或者不基于承诺的非异步动作(也称为同步动作)的对象。

非异步动作/同步动作

让我们检查一下我们在 calendarSlice 中的非异步操作或同步操作中写了什么:

有两个参数(状态和动作),但是你只需要传递一个布尔值 PayloadAction。

setError in reducers:同第一个参数状态;不需要传递任何东西,因为 Thunk 会在引擎盖下处理它。我们只需要传递一些东西或者更新一个字符串PayloadAction,

getEvents in reducers:payload action 是 EventType 的数组。

异步操作

下面是我们的异步操作:

getEvents :返回 AppThunk 的函数和调度函数。

dispatch(slice.actions.setLoading(true)) :将加载从默认假更新为真。

我们传递的只是一个空字符串,所以基本上,每当我们有一个成功的请求时,我们就将这里的错误重置为空。

在 try-catch 块中,我们使用了一个axios.get,,它从Endpoints.events.返回一个数组EventType

我们得到的response.data将被发送到商店,以便更新状态。

在创建了calendarSlice之后,我们现在将更新根 reducers。

更新根缩减器

再次打开reducers.ts文件,更新injectedReducers.

首先,我们需要从 features/calendar/calendar slice 导入 calendarReducer,如清单 10-16 所示。

import { combineReducers } from '@reduxjs/toolkit';
import calendarReducer from 'features/calendar/calendarSlice'

Listing 10-16Adding the Named Component in reducers.ts

然后,在同一个文件中,注入我们的第一个缩减器,如清单 10-17 所示。

const injectedReducers = {
  calendar: calendarReducer,
};

Listing 10-17Injecting the calendarReducer in injectedReducers

我们现在可以使用这个名称空间calendar从这个日历中获取所需的状态。但是我们稍后会在组件中这样做。

现在,我们准备在日历视图或页面的 UI 组件中编写我们的selectorsdispatchers

更新日历视图

但是首先,让我们通过进入日历视图组件来测试dispatch。打开CalendarViewindex.tsx进入.

首先,我们将更新 CalendarView 的 index.tsx,如清单 10-18 所示。

import React, { useEffect } from 'react';
import { getEvents } from 'features/calendar/calendarSlice';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'store/reducers';

const CalendarView = () => {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getEvents());
  }, []);

Listing 10-18Updating index.tsx of CalendarView

现在,我们将在控制台中检查getEventsuseDispatch以查看我们是否成功获取了数据。

确保您的服务器正在运行http://localhost:5000/events,并单击浏览器http://localhost:3000/dashboard/calendar中的刷新按钮。

打开Chrome DevToolsNetworkResponse查看事件数据,如图 10-4 所示。

img/506956_1_En_10_Fig4_HTML.jpg

图 10-4

在 Chrome DevTools 上显示了事件数据的截图

我们的 Redux 正在工作的概念证明!状态在浏览器中,我们可以使用它。让我们回到我们的CalendarView组件,我们将添加useSelector.

useSelector需要一个带有发送和返回RootState的签名的函数,现在我们可以访问reducer。现在,我们只能访问或获取日历,因为这是我们到目前为止添加的内容,如图 10-5 所示。

img/506956_1_En_10_Fig5_HTML.jpg

图 10-5

通过根状态演示智能感知

我们通过使用RootState获得智能感知。如果您使用的是 JavaScript 而不是 TypeScript,那么您必须猜测或搜索您的 reducer 文件。想象一下,如果您有一个包含几十甚至几百个文件的大型应用。寻找它会很快变得令人厌倦。

这个智能特性是 TypeScript 的亮点之一。你可以只输入点(。),然后它会显示您可以使用的所有可用减速器。

好的,现在让我们在CalendarView.做一些映射

return (
    <div>
      <h1>Calendar Works!</h1>

      {loading && <h2>Loading... </h2>}
      {error && <h2>Something happened </h2>}
      <ul>

                 /*conditional nullable chain */

        {events?.map(e => (
          <li key={e.id}>{e.title} </li>
        ))}
      </ul>
    </div>
  );
};

export default CalendarView;

Listing 10-19Mapping the CalendarView in the UI

好了,让我们检查一下我们在清单 10-19 中做了什么。

loading &&:如果条件为真,& &之后的元素运行;否则,如果状态为 false,则忽略它。同样的逻辑也适用于error &&

刷新浏览器,检查是否可以在数据呈现之前看到加载。

img/506956_1_En_10_Fig6_HTML.jpg

图 10-6

在 UI 中呈现 CalendarView

摘要

在这一章中,我希望你已经对 React 应用中的 Redux 实现流程有了更好的理解,包括如何将一个异步动作分派给 reducer,以及如何将状态从存储渲染到 UI。

我们还使用了状态管理库 Redux 工具包,并实现了它的助手函数 createSlice。我们还扩展了样式组件,以包括 Material-UI 中的日历视图组件。

在下一章,我们将继续我们的 Redux 课程,使用 Redux 创建、删除和更新事件。