React 组件和 Apollo Client 不工作 "in real time"

React component and Apollo Client does not work "in real time"

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

我创建了一个示例 Sandbox 来展示我的 "issue" 和我的代码。我没有添加任何类型的验证内容,这只是为了示例目的。

一些问题(我看到)是:

  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

如有任何意见、建议或任何内容,我们将不胜感激。

  1. Button actions trigger on second click

因为你打电话给

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

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

之前的状态相同

第二次点击的时候是第一次点击后的状态,这个if

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. Data won't refresh after created / deleted.

你应该 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
      : [];

传递给table。


编辑 2019 年 10 月 10 日

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

正在更新服务器

我对服务器进行了一些更改以使应用程序正常运行。首先,deleteUser returns 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

而不是使用 useLazyQuery 并在 useEffect 中调用它,我使用 useQuery。这样我们就不需要在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