react-query手把手教程13-乐观更新

955 阅读4分钟

该系列其他文章可以点击查看专栏👉🏻👉🏻react-query手把手教程系列

场景

在前一篇文章中,修改github的用户名时,如果前端正在请求后端修改时,将会出加载中的提示。一般情况下,这种修改用户名的请求多数都会成功,为了减少用户烦躁的等待,你可以立即将用户修改的新用户名显示在界面上。

上面的这种行为,可以称作乐观更新。其实非常容易能够理解,你会乐观的估计后端一定可以更新成功,提前将用户修改的内容展现给他。

实践

基于评论组件演示乐观更新

还是以github为例,下面的代码是一个issue评论列表的伪代码:

// 获取当前issue评论列表内容
async function fetchIssueComments({
  org,
  repo,
  issueNumber,
}) {
  const response = await fetch(
    `https://api.github.com/repos/${org}/${repo}/issues/${issueNumber}/comments`
  );
  if (!response.ok) {
    throw new Error(
      `Could not fetch comments for issue ${issueNumber}`
    );
  }

  return response.json();
}

// 评论组件
const Comment = ({ comment }) => {
  return (
    <div>
      <div className="comment-header">
        <div className="comment-author">
          {comment.user.login}
        </div>
        <div className="comment-date">
          {new Date(comment.created_at).toLocaleDateString()}
        </div>
      </div>
      <div>{comment.body}</div>
    </div>
  );
};

// issue评论列表组件
const IssueComments = ({ org, repo, issueNumber }) => {
  const commentsQuery = useQuery(
    ["comments", org, repo, issueNumber],
    fetchIssueComments
  );

  return commentsQuery?.data?.map((comment) => (
    <Comment key={comment.id} comment={comment} />
  ));
};

可以看到上面代码的评论组件中,将会展示评论内容、当前评论人信息以及创建评论的日期。接下来的操作过程,将会对以上这三个内容进行乐观更新


常规操作

下面的代码是常规提交评论的操作伪代码:

// ①
function addComment({ org, repo, issueNumber, comment }) {
  return fetch(
    `https://api.github.com/repos/${org}/${repo}/issues/${issueNumber}/comments`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ body: comment }),
    }
  ).then((res) => res.json());
}


// ②
function CommentForm({ org, repo, issueNumber }) {
  const queryClient = useQueryClient();
  const addCommentMutation = useMutation(addComment, {
    // ③
    onSuccess: (data, variables) => {
      const { org, repo, issueNumber } = variables;


      queryClient.setQueryData(
        ["comments", org, repo, issueNumber],
        (comments) => comments.concat(data)
      );
    },
    // ④
    onSettled: (data, error, variables) => {
      const { org, repo, issueNumber } = variables;


      queryClient.invalidateQueries([
        "comments",
        org,
        repo,
        issueNumber,
      ]);
    },
  });


  return <form>{/* ... */}</form>;
}

①:实现了一个添加评论的方法,传入创建评论所需要的参数,就可以请求向github创建一条评论。

②:实现了一个提交评论表单的方法,请注意代码里面的内容,③是添加成功后向缓存中写入数据,并且不管成功失败④中都会刷新数据,保证当前展示给用户的是最新数据。


乐观更新

下面将会为大家展示,如何使用乐观更新

首先来分析一下,如果后端能够更新成功,什么数据时前端可以确定的数据:

  • 用户的评论内容comment
  • 创建时间created_at,基于发送请求的时间即可
  • 当前的评论的用户名user.login,从缓存中获取当前用户的信息

由于id并不展示给用户,其实可以通过前端生成随机数的方式,暂时用一个伪id代替,因此代码大致如下所示:

function CommentForm({ org, repo, issueNumber }) {
  const queryClient = useQueryClient();
  const username = useUser();
  const addCommentMutation = useMutation(addComment, {
    // ①
    onMutate: (variables) => {
      const comment = {
        id: Math.random().toString(),
        body: variables.comment,
        created_at: new Date(),
        user: {
          login: username,
        }
      };
      queryClient.setQueryData(
        ["comments", org, repo, issueNumber],
        (comments) => comments.concat(comment)
      );
    },
    onSettled: (data, error, variables) => {
      const { org, repo, issueNumber } = variables;
      queryClient.invalidateQueries([
        "comments",
        org,
        repo,
        issueNumber,
      ]);
    },
  });


  return <form>{/* ... */}</form>;
}

此时一旦调用addCommentMutation.mutate方法,首先会立即触发①中的回调,展示界面先将假数据展示出来。不管成功与否都会刷新当前界面,此时真数据将会替代假数据的展示。

回滚机制

假如我在onMutate里面设置了数据,但是请求失败了怎么办?虽然onSettled会重新请求数据,但是如果请求比较慢,此时假数据就会一直展示。

为了应对这种情况react-query提供了restoreCache方法,你可以在onSuccess以及onError的回调中轻松使用该方法,代码如下:

const addCommentMutation = useMutation(addComment, {
  onMutate: (variables) => {
    const savedCache = queryClient.getQueryData(
      ["comments", org, repo, issueNumber]
    );


    const comment = {
      id: Math.random().toString(),
      body: variables.comment,
      created_at: new Date(),
      user: {
        login: username,
      }
    }
    queryClient.setQueryData(
      ["comments", org, repo, issueNumber],
      (comments) => comments.concat(comment)
    );


    return () => {
      queryClient.setQueryData(
        ["comments", org, repo, issueNumber],
        savedCache
      )
    }
  },
  onSuccess: (data, variables, restoreCache) => {
    restoreCache();
    queryClient.setQueryData(
      ["comments", org, repo, issueNumber],
      (comments) => comments.concat(data)
    );
  },
  onSettled: (data, variables) => {
    queryClient.invalidateQueries(["comments", org, repo, issueNumber]);
  },
  onError: (error, variables, restoreCache) => {
    restoreCache();
  }
})

现在我们的代码就非常完美了,一旦用户请求失败就会回滚之前设置的假数据。如果请求成功会从后端请求真实的数据覆盖之前的假数据。而在这段时间的过程中,用户会先看到一个乐观更新的数据,而不是加载动画。这样可以在某种程度上提升用户体验。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情