DevGang
Авторизоваться

Как создать базовое CRUD-приложение с компонентами NextJS, TS, React, Redux-Tookit и MUI5

Мы узнаем, как создать приложение NextJS, которое использует redux-observable для управления побочными эффектами.

В этом проекте мы узнаем, как создать базовое приложение CRUD, используя NextJS Typescript и Redux-Toolkit для управления побочными эффектами.

Технический стек:

  • NextJS - фреймворк React
  • Typescript
  • Redux-Toolkit - набор инструментов для управления состоянием redux
  • MUI5 - компоненты UI
  • Formik - управление формой
  • Yup - проверка формы
  • Prisma - база данных

Кодирование: настройка базы данных

В этом примере мы используем Prisma, облегченную серверную базу данных, в качестве нашего хранилища данных. Чтобы включить Prisma в нашем проекте, нам нужно добавить следующие зависимости:

  1. "@prisma/client": "^3.8.0"
  2. "prisma": "^3.8.0" - devDependencies

Создайте конфигурацию источника данных Prisma

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
  binaryTargets = ["native"]
}

model User {
  id        Int      @id @default(autoincrement())
  firstName String
  lastName  String
  email     String?
  birthDate DateTime?
}

И начальная схема и миграционный SQL внутри папки src/prisma.

-- CreateTable
CREATE TABLE "User" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "firstName" TEXT NOT NULL,
    "lastName" TEXT NOT NULL,
    "email" TEXT,
    "birthDate" DATETIME
);

Чтобы создать локальную базу данных, выполните:

npx prisma migrate dev --name=init

Следующий шаг заключается в том, хотите ли вы настроить хранилище redux или просмотр. Это зависит от вашего стиля программирования.

Настройка хранилища приложений

Хранилище приложений

Хранилище приложений является производным от примера NextJS. В нем сохраняются объединенные редукторы и промежуточные программы из createApi Redux-toolkit.

import {configureStore} from '@reduxjs/toolkit';
import {UsersService} from "./UserService";
import {UserSlice} from "./slices/UserSlice";

export function makeStore() {
  return configureStore({
    reducer: {
      [UsersService.reducerPath]: UsersService.reducer,
      user: UserSlice.reducer
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(
            UsersService.middleware
        ),
  })
}

const store = makeStore()

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>

// Inferred type: {users: UsersState}
export type AppDispatch = typeof store.dispatch

export default store;

Пользовательский Service/API

Этот файл содержит все конечные точки API для пользователя, который использует запрос и мутацию для редактирования пользовательских данных. Он использует заголовок, определенный в базовом сервисном файле.

import {BaseService} from "./BaseService";
import {UserType} from "./types/UserType";
import {createSlice} from "@reduxjs/toolkit";

interface Pagination {
  offset: number,
  limit: number
}

export const UsersService = BaseService.injectEndpoints({
  endpoints: (build) => ({
    getUsers: build.query<UserType[], Pagination>({
      query: (param: Pagination) => `/users?offset=${param.offset}&limit=${param.limit}`,
      providesTags: [{type: "User", id: "LIST"}]
    }),
    getUser: build.query<UserType, number>({
      query: (id) => ({
        url: `/users/${id}`,
      })
    }),
    createUser: build.mutation<UserType, UserType>({
      query: (body: UserType) => ({
        url: `/users`,
        method: 'POST',
        body
      }),
      invalidatesTags: [{type: "User", id: "LIST"}]
    }),
    updateUser: build.mutation<UserType, Pick<UserType, 'id'> & Partial<UserType>>({
      query: ({id, ...body}) => ({
        url: `/users/${id}`,
        method: 'PATCH',
        body
      }),
      invalidatesTags: [{type: "User", id: "LIST"}]
    }),
    deleteUser: build.mutation<void, number>({
      query: (id) => ({
        url: `/users/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: [{type: "User", id: "LIST"}],
    }),
  }),
  overrideExisting: true,
})

export const {
  useGetUsersQuery, useGetUserQuery,
  useCreateUserMutation, useDeleteUserMutation, useUpdateUserMutation
} = UsersService;

Сторона UI

Мы внедрили традиционный список и страницы сведений с возможностью прокрутки в пользовательском интерфейсе, используя компоненты UI5.

Страница списка пользователей использует компоненты MUI 5 для отображения таблицы. При загрузке страницы, реализованной с использованием эффекта использования, он вызывает API для получения списка пользователей. Действие удаления также реализовано на этой странице.

import React, {useState} from 'react';
import {
  Alert,
  Box,
  Button,
  ButtonGroup,
  CircularProgress,
  Container,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  Snackbar,
  SwipeableDrawer,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableFooter,
  TableHead,
  TablePagination,
  TableRow
} from "@mui/material";
import moment from "moment";
import {Delete, Edit, PersonAdd} from "@mui/icons-material";
import {useAppDispatch} from 'services/hooks';

import {useRouter} from "next/router";
import {NextPage} from "next";
import {useDeleteUserMutation, useGetUsersQuery} from "../../services/UserService";
import Footer from "../../components/Footer/Footer";
import UserDetail from "./components/UserDetail";
import {UserType} from "../../services/types/UserType";
import {clearUser, setUser} from "../../services/slices/UserSlice";

const EMPTY_DIALOG = {
  open: false,
  text: '',
  title: '',
  onConfirm: () => {
  },
  onCancel: () => {
  }
}

const EMPTY_ALERT = {
  open: false,
  text: '',
};

const Users: NextPage = () => {

  const router = useRouter();
  const dispatch = useAppDispatch();
  const [offset, setOffset] = useState(0);
  const [limit, setLimit] = useState(10);
  const [dialog, setDialog] = useState(EMPTY_DIALOG);
  const [alert, setAlert] = useState(EMPTY_ALERT);

  const {
    data,
    error,
    isLoading: isUsersLoading,
    isSuccess: isUsersQueried,
    isFetching: isUsersFetching,
    isError: isUsersError
  } = useGetUsersQuery({offset: (offset * limit), limit});

  const [deleteUser, {
    data: deletedUser,
    isLoading: isUserDeleting,
    isSuccess: isUserDeleted
  }] = useDeleteUserMutation();

  const drawerBleeding = 56;
  const [openDrawer, setOpenDrawer] = React.useState(false);

  const handleChangeRowsPerPage = ({target: {value}}) => {
    setLimit(value);
  };

  const handleChangePage = (_, nextPage) => {
    setOffset(nextPage);
  };

  const handleDeleteUser = (userId: number) => async () => {
    try {
      await deleteUser(userId).unwrap();
      setAlert({
        open: true,
        text: `Successfully deleted user: ${userId}`,
      });
      resetDeleteDialog();

    } catch (error) {
      console.log(`Error: Failed deleting user with id ${userId}`);
    }
  };

  const resetDeleteDialog = () => {
    setDialog(EMPTY_DIALOG);
  }

  const openDeleteDialog = (userId: number) => () => {
    setDialog({
      open: true,
      title: 'Delete user',
      text: `Delete user: ${userId}?`,
      onConfirm: handleDeleteUser(userId),
      onCancel: () => resetDeleteDialog()
    });
  }

  const resetAlert = () => {
    setAlert(EMPTY_ALERT);
  }

  const editUser = (user: UserType) => () => {

    setOpenDrawer(true);
    dispatch(setUser(user));
  };

  const toggleEditDrawer = (newOpen: boolean) => () => {

    if (!newOpen) {
      dispatch(clearUser());
    }
    setOpenDrawer(newOpen);
  };

  const renderTable = (users: UserType[], count: number) => {
    const hasUsers = count > 0;

    return (
        <React.Fragment>
          <TableContainer>
            <Table>
              <TableHead>
                <TableRow>
                  <TableCell colSpan={6} align="right">
                    <Button variant="outlined" color="primary" onClick={toggleEditDrawer(true)}>
                      <PersonAdd/>
                    </Button>
                  </TableCell>
                </TableRow>
                <TableRow>
                  <TableCell>Id</TableCell>
                  <TableCell>First name</TableCell>
                  <TableCell>Last name</TableCell>
                  <TableCell>Email</TableCell>
                  <TableCell>Birth date</TableCell>
                  <TableCell></TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {hasUsers ? (
                    users.map((user) => (
                        <TableRow key={user.id}>
                          <TableCell>{user.id}</TableCell>
                          <TableCell>{user.firstName}</TableCell>
                          <TableCell>{user.lastName}</TableCell>
                          <TableCell>{user.email}</TableCell>
                          <TableCell>
                            {moment.utc(user.birthDate).format('MM-DD-YYYY')}
                          </TableCell>
                          <TableCell sx={{textAlign: "right"}}>
                            <ButtonGroup>
                              <Button onClick={editUser(user)}>
                                <Edit/>
                              </Button>
                              <Button onClick={openDeleteDialog(user.id)}>
                                {<Delete/>}
                              </Button>
                            </ButtonGroup>
                          </TableCell>
                        </TableRow>
                    ))
                ) : (
                    <TableRow>
                      <TableCell colSpan={6}>No users found.</TableCell>
                    </TableRow>
                )}
              </TableBody>
              <TableFooter>
                <TableRow>
                  <TablePagination
                      count={count}
                      page={offset}
                      rowsPerPage={limit}
                      onPageChange={handleChangePage}
                      onRowsPerPageChange={handleChangeRowsPerPage}
                  />
                </TableRow>
              </TableFooter>
            </Table>
          </TableContainer>
          <SwipeableDrawer
              anchor="bottom"
              open={openDrawer}
              onClose={toggleEditDrawer(false)}
              onOpen={toggleEditDrawer(true)}
              swipeAreaWidth={drawerBleeding}
              disableSwipeToOpen={false}
              ModalProps={{
                keepMounted: true,
              }}
          >
            <UserDetail toggleEditDrawer={toggleEditDrawer}></UserDetail>
          </SwipeableDrawer>
        </React.Fragment>
    );
  }

  const renderBody = () => {
    if (isUsersQueried) {
      const {users, count} = data;

      return (isUsersFetching || isUsersLoading) ?
          <Box sx={{display: 'flex'}}>
            <CircularProgress/>
          </Box> :
          renderTable(users, count)
    }
  }

  const renderError = () => {
    return isUsersError && <Alert severity="error">{JSON.stringify(error)}</Alert>;
  }

  return (
      <Container maxWidth={"md"} fixed>
        {renderError()}
        {renderBody()}
        <Footer></Footer>
        <Dialog
            open={dialog.open}
            onClose={dialog.onCancel}
            aria-labelledby="alert-dialog-title"
            aria-describedby="alert-dialog-description"
        >
          <DialogTitle id="alert-dialog-title">
            {dialog.title}
          </DialogTitle>
          <DialogContent>
            <DialogContentText id="alert-dialog-description">
              {dialog.text}
            </DialogContentText>
          </DialogContent>
          <DialogActions>
            <Button onClick={dialog.onCancel}>Disagree</Button>
            <Button onClick={dialog.onConfirm} autoFocus>
              Agree
            </Button>
          </DialogActions>
        </Dialog>
        <Snackbar
            open={alert.open}
            autoHideDuration={6000}
            onClose={resetAlert}
            message={alert.text}
        />
      </Container>
  );
}

export default Users;

А вот страница сведений о пользователе. Мы используем formik для создания формы и yup для определения правил проверки. Это срабатывает, когда мы отправляем форму.

Обратите внимание, что внутри useEffect мы устанавливаем значения формы в режиме редактирования, когда пользователь использует formic. Этот useEffect будет срабатывать только в том случае, если значение пользователя изменится, чтобы избежать слишком большого количества ошибок повторного отображения.

#React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу