【问题标题】:React component and Apollo Client does not work "in real time"React 组件和 Apollo Client 不能“实时”工作
【发布时间】:2020-01-24 23:58:00
【问题描述】:

我正在尝试构建一个用户系统,但我对某些东西感到困惑,因为它不能实时工作。

我创建了一个示例Sandbox 来显示我的“问题”和我的代码。我没有添加任何类型的验证内容,仅用于示例目的。

一些问题(我明白了)是:

  1. 在第二次点击时触发按钮操作
  2. 数据在创建/删除后不会刷新。

这是<UsersPage /> 组件:

import React, { Fragment, useState, useEffect } from "react";
import { useMutation, useLazyQuery } from "@apollo/react-hooks";
import { ADD_USER, LIST_USERS, DELETE_USER } from "../../../config/constants";
import { useSnackbar } from "notistack";
import {
  Grid,
  Paper,
  TextField,
  Button,
  Typography,
  MenuItem,
  FormHelperText
} from "@material-ui/core";
import AddUserIcon from "@material-ui/icons/PersonAdd";
import { withStyles } from "@material-ui/core/styles";
import PropTypes from "prop-types";
import Table from "../../Table";

const styles = theme => ({
  grid: {
    margin: theme.spacing(3)
  },
  icon: {
    marginRight: theme.spacing(2)
  },
  form: {
    width: "100%",
    marginTop: theme.spacing(3),
    overflowX: "auto",
    padding: theme.spacing(2)
  },
  submit: {
    margin: theme.spacing(2)
  },
  container: {
    display: "flex",
    flexWrap: "wrap"
  },
  textField: {
    marginLeft: theme.spacing.unit,
    marginRight: theme.spacing.unit
  },
  root: {
    width: "100%",
    marginTop: theme.spacing(3),
    overflowX: "auto",
    padding: theme.spacing(2)
  },
  title: {
    margin: theme.spacing(2)
  },
  table: {
    minWidth: 700
  },
  noRecords: {
    textAlign: "center"
  },
  button: {
    margin: theme.spacing.unit
  }
});

const Users = props => {
  const [idState, setIdState] = useState(null);
  const [emailState, setEmailState] = useState("");
  const [passwordState, setPasswordState] = useState("");
  const [usersState, setUsersState] = useState([]);
  const [errorsState, setErrorsState] = useState({});
  const [loadingState, setLoadingState] = useState(false);
  const [addUser, addUserResponse] = useMutation(ADD_USER);
  const [loadUsers, usersResponse] = useLazyQuery(LIST_USERS);
  const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER);
  const { enqueueSnackbar } = useSnackbar();

  useEffect(() => {
    loadUsers();

    if (usersResponse.called && usersResponse.loading) {
      setLoadingState(true);
    } else if (usersResponse.called && !usersResponse.loading) {
      setLoadingState(false);
    }

    if (usersResponse.data) {
      setUsersState(usersResponse.data.getUsers);
    }
  }, [usersResponse.called, usersResponse.loading, usersResponse.data]);

  function handleSubmit(e) {
    e.preventDefault();

    if (idState) {
    } else {
      addUser({
        variables: {
          email: emailState,
          password: passwordState
        }
      });
    }

    if (addUserResponse.called && addUserResponse.loading) {
      enqueueSnackbar("Creating user");
    }

    if (addUserResponse.error) {
      addUserResponse.error.graphQLErrors.map(exception => {
        const error = exception.extensions.exception;
        const messages = Object.values(error);
        enqueueSnackbar(messages[0], { variant: "error" });
      });
    }

    if (addUserResponse.data && addUserResponse.data.addUser) {
      enqueueSnackbar("user created", { variant: "success" });
      loadUsers();
    }
  }

  function handleEdit(user) {
    setIdState(user.id);
    setEmailState(user.email);
  }

  async function handleDelete(data) {
    if (typeof data === "object") {
      data.map(id => {
        deleteUser({ variables: { id } });
        if (deleteUserResponse.data && deleteUserResponse.data.deleteUser) {
          enqueueSnackbar("User deleted", { variant: "success" });
        }
      });
    } else {
      deleteUser({ variables: { id: data } });
      if (deleteUserResponse.data && deleteUserResponse.data.deleteUser) {
        enqueueSnackbar("User deleted", { variant: "success" });
      }
    }
  }

  function resetForm() {
    setIdState(null);
    setEmailState("");
  }
  const { classes } = props;

  return (
    <Fragment>
      <Grid container spacing={8}>
        <Grid item xs={3} className={classes.grid}>
          <Paper className={classes.form}>
            <Typography variant="h6" className={classes.title}>
              {idState ? `Edit user: ${emailState}` : "Create user"}
            </Typography>
            <form className={classes.container} onSubmit={handleSubmit}>
              <input type="hidden" name="id" value={idState} />
              <TextField
                className={classes.textField}
                label="E-mail address"
                type="email"
                variant="outlined"
                margin="normal"
                autoComplete="email"
                id="email"
                name="email"
                required={!idState}
                fullWidth
                onChange={e => setEmailState(e.target.value)}
                value={emailState}
                aria-describedby="email-error"
              />
              <FormHelperText id="email-error">
                {errorsState.email}
              </FormHelperText>
              <TextField
                className={classes.textField}
                label="Password"
                variant="outlined"
                margin="normal"
                autoComplete="password"
                id="password"
                name="password"
                required={!idState}
                type="password"
                fullWidth
                onChange={e => setPasswordState(e.target.value)}
                value={passwordState}
                aria-describedby="password-error"
              />
              <FormHelperText id="password-error">
                {errorsState.password}
              </FormHelperText>

              <Button
                variant="contained"
                color="primary"
                className={classes.submit}
                size="large"
                type="submit"
              >
                <AddUserIcon className={classes.icon} /> Save
              </Button>
              <Button
                variant="contained"
                color="secondary"
                className={classes.submit}
                type="button"
                onClick={resetForm}
              >
                <AddUserIcon className={classes.icon} /> Add new
              </Button>
            </form>
          </Paper>
        </Grid>
        <Grid item xs={8} className={classes.grid}>
          <Paper className={classes.root}>
            <Table
              data={usersState}
              className={classes.table}
              columns={{
                id: "ID",
                email: "E-mail address"
              }}
              classes={classes}
              title="Users"
              handleEdit={handleEdit}
              handleDelete={handleDelete}
              filter={true}
              loading={loadingState}
            />
          </Paper>
        </Grid>
      </Grid>
    </Fragment>
  );
};

Users.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(styles)(Users);

如果您需要更多代码或编辑:

前端沙箱:App / Code

后端沙箱:App / Code

任何cmets、建议或任何东西都将不胜感激。

【问题讨论】:

    标签: javascript reactjs graphql material-ui react-apollo


    【解决方案1】:
    1. 在第二次点击时触发按钮操作

    因为你打电话

    if (addUserResponse.called && addUserResponse.loading) {
      enqueueSnackbar("Creating user");
    }
    

    在您致电addUser 之后。检查if (addUserResponse.called &amp;&amp; addUserResponse.loading)时状态没有改变,状态与调用addUser之前的状态相同。

    当你第二次点击时,你有第一次点击后的状态,如果

    if (addUserResponse.data && addUserResponse.data.addUser) {
      enqueueSnackbar("user created", { variant: "success" });
      loadUsers();
    }
    

    是真的。

    解决方案:

    创建一个useEffect 来处理addUser 状态并从handleSubmit 中删除if 子句

     useEffect(() => {
        if (!addUserResponse.called) {
          return;
        }
    
        if (addUserResponse.loading) {
          enqueueSnackbar("Creating user");
          return;
        }
    
        if (addUserResponse.error) {
          addUserResponse.error.graphQLErrors.map(exception => {
            const error = exception.extensions.exception;
            const messages = Object.values(error);
            enqueueSnackbar(messages[0], { variant: "error" });
          });
          return;
        }
    
        enqueueSnackbar("user created", { variant: "success" });
      }, [addUserResponse.called, addUserResponse.loading]);
    
     function handleSubmit(e) {
        e.preventDefault();
    
        if (idState) {
        } else {
          addUser({
            variables: {
              email: emailState,
              password: passwordState
            }
          });
        }
      }
    
    1. 数据在创建/删除后不会刷新。

    你应该update your cache after mutation,因为当你调用突变时,Apollo 不知道你是在添加还是删除。

    解决方案:

     const [addUser, addUserResponse] = useMutation(ADD_USER, {
        update: (cache, { data: { addUser } }) => {
          // get current data cache
          const cachedUsers = cache.readQuery({ query: LIST_USERS });
    
          // create new users
          const newUsers = [addUser, ...cachedUsers.getUsers];
    
          // save newUsers on cache
          cache.writeQuery({
            query: LIST_USERS,
            data: {
              getUsers: newUsers
            }
          });
        }
      });
    

    删除用户也是如此,预计newUsers 将过滤当前用户:

    const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER, {
        update: (cache, { data: { deleteUser } }) => {
          const cachedUsers = cache.readQuery({ query: LIST_USERS });
    
          // NOTE: this didn't work because deleteUser return true instead user.
          // I'd suggest change your backend and deleteUser return user id to
          // be able to perform this filter.
          const newUsers = cachedUsers.getUsers.filter(
            ({ id }) => id !== deleteUser.id
          );
    
          cache.writeQuery({
            query: LIST_USERS,
            data: {
              getUsers: newUsers
            }
          });
        }
      });
    

    注 1:

    您无需多次致电loadUsers。因为您在执行突变时更新缓存,所以您的数据将始终是最新的。因此,我会这样称呼loadUsers

     useEffect(() => {
        loadUsers();
      }, []);
    
      useEffect(() => {
        if (usersResponse.called && usersResponse.loading) {
          setLoadingState(true);
        } else if (usersResponse.called && !usersResponse.loading) {
          setLoadingState(false);
        }
      }, [usersResponse.called, usersResponse.loading]);
    

    注2

    您不需要为用户创建状态,您已经拥有来自usersResponse.data.getUsers 的状态,但这是您的偏好。就我而言,我删除了const [usersState, setUsersState] = useState([]); 并添加了

    const users =
        usersResponse.data && usersResponse.data.getUsers
          ? usersResponse.data.getUsers
          : [];
    

    传递给桌子。


    2019 年 10 月 10 日编辑

    我所做的主要更改是创建了一个名为 batchDeleteUsers 的突变,它可以在一次调用中删除多个用户。

    更新服务器

    我对服务器进行了一些更改以使应用程序正常运行。首先,deleteUser 返回User,我创建了一个名为batchDeleteUsers 的突变。

    我当前的突变模式:

    type Mutation {
      addUser(email: String!, password: String!): User
      deleteUser(id: String!): User
      batchDeleteUsers(ids: [String!]!): [User]
    }
    

    我目前的解析器:

    deleteUser: (root, { id }, context) => {
      const user = USERSDB.find(user => user.id === id);
      USERSDB = USERSDB.filter(user => user.id !== id);
      return user;
    },
      batchDeleteUsers: (root, { ids }, context) => {
      const users = USERSDB.filter(user => ids.includes(user.id));
      USERSDB = USERSDB.filter(user => !ids.includes(user.id));
      return users;
    }
    

    更新应用 1

    我使用的是useQuery,而不是使用useLazyQuery 并在useEffect 中调用它。这样我们就不需要在useEffect里面进行查询了,在组件初始化的时候就触发了。

    const usersResponse = useQuery(LIST_USERS);
    

    更新应用 2

    以下是我如何创建 deleteUserbatchDeleteUsers 突变。

    const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER, {
      update: (cache, { data: { deleteUser } }) => {
        const cachedUsers = cache.readQuery({ query: LIST_USERS });
    
        const newUsers = cachedUsers.getUsers.filter(
          ({ id }) => id !== deleteUser.id
        );
    
        cache.writeQuery({
          query: LIST_USERS,
          data: {
            getUsers: newUsers
          }
        });
      }
    });
    const [batchDeleteUsers, batchDeleteUsersResponse] = useMutation(
      BATCH_DELETE_USERS,
      {
        update: (cache, { data: { batchDeleteUsers } }) => {
          const cachedUsers = cache.readQuery({ query: LIST_USERS });
    
          const newUsers = cachedUsers.getUsers.filter(({ id }) => {
            return !batchDeleteUsers.map(({ id }) => id).includes(id);
          });
    
          cache.writeQuery({
            query: LIST_USERS,
            data: {
              getUsers: newUsers
            }
          });
        }
      }
    );
    

    更新应用程序 3

    这就是我处理删除用户突变生命周期的方式。

    useEffect(() => {
      if (!deleteUserResponse.called) {
        return;
      }
    
      if (deleteUserResponse.loading) {
        enqueueSnackbar("Deleting user");
        return;
      }
    
      if (deleteUserResponse.error) {
        deleteUserResponse.error.graphQLErrors.map(exception => {
          const error = exception.extensions.exception;
          const messages = Object.values(error);
          enqueueSnackbar(messages[0], { variant: "error" });
        });
        return;
      }
    
      enqueueSnackbar("user deleted", { variant: "success" });
    }, [deleteUserResponse.called, deleteUserResponse.loading]);
    
    useEffect(() => {
      if (!batchDeleteUsersResponse.called) {
        return;
      }
    
      if (batchDeleteUsersResponse.loading) {
        enqueueSnackbar("Deleting users");
        return;
      }
    
      if (batchDeleteUsersResponse.error) {
        batchDeleteUsersResponse.error.graphQLErrors.map(exception => {
          const error = exception.extensions.exception;
          const messages = Object.values(error);
          enqueueSnackbar(messages[0], { variant: "error" });
        });
        return;
      }
    
      enqueueSnackbar("users deleted", { variant: "success" });
    }, [batchDeleteUsersResponse.called, batchDeleteUsersResponse.loading]);
    

    更新应用程序 4

    最后,这就是我处理删除用户的方式。

    function handleDelete(data) {
      if (typeof data === "object") {
        batchDeleteUsers({ variables: { ids: data } });
      } else {
        deleteUser({ variables: { id: data } });
      }
    }
    

    我的服务器沙箱:code

    我的前端沙箱:code

    【讨论】:

    • 删除不起作用,只是为了评论。它真的改变了我的代码。谢谢。
    • 太棒了。当用户被删除时,您是否可以更改后端以返回用户 ID?然后我可以更新我的代码来删除用户
    • 有新网址吗?
    • 不,它没有。这是same link
    • @Maramal 我已经更新了应用程序。我认为它完全有效。
    猜你喜欢
    • 2019-02-08
    • 2019-09-17
    • 2019-03-30
    • 2021-01-15
    • 2021-08-11
    • 2020-02-14
    • 2019-12-10
    • 2017-07-21
    • 2018-07-03
    相关资源
    最近更新 更多