iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 28
4
Modern Web

Think in GraphQL系列 第 28

GraphQL 前端 (2) - 文章按讚及刪除文章

header

今天再來加入兩個新功能!

  1. 文章按讚
  2. 刪除文章

主要會介紹到如何在 Query component 中使用 Mutation component ,以及 Mutation 時的 UI 優化 。


1. 文章按讚

文章按讚部分我們希望使用者能夠進入文章裡面看完內容後才能按讚,因此會把功能放在文章頁面裡面,所以讓我們打開 src/PostPage.js

首先先加入按讚的圖示與按讚數,別忘了 GET_POST_DETAIL 裡要加上 votes field 喔。

const GET_POST_DETAIL = gql`
  query PostDetail($postId: ID!) {
    post(id: $postId) {
      id
      title
      body
      author {
        name
      }
+      votes
    }
  }
`;

...
class PostPage extends Component {
  render() {
    return (
      <Query ... >
      {({ ... }) => {
        ...
        return (
          <div>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
            <Link to="/">Back</Link>
+            <hr />
+            <div>
+              <i className="material-icons">thumb_up</i>
+              {post.votes}
+            </div>
          </div>
        )
      }}
      </Query>
    )
  }
}

如果跟下圖所示一樣就代表加入成功!

img

接下來就是加入 Mutation component ,我們要執行 `upvotePost` muttion 。

img

值得一提的是這一代的 Apollo Client 會自動幫我們做 cache 管理,所以只要按讚後等到 upvotePost mutation 執行成功並回傳新的按讚數時,只要回傳的資料的 type 與 id 在 cache 中對上, Apollo Client 就會自動幫我們更新 cache 中的那筆文章資料而不必重新整理。

不過問題來了,因為 upvotePost mutation 執行與回傳需要時間,因此會讓畫面 lag 一下才自動更新讚數。但是好的體驗應該是按下去當下讚數就要馬上更新,等到 mutation 做完後再更新一次最新的讚數 (畢竟可能中途別人也有按讚)。

所以這邊我們要在 upvaotePost 時不只加入 variables 還要加入 optimisticResponseoptimisticResponse 為一個物件,當 mutation 送出回傳資料前先用這個物件充當 response 讓快取與 UI 立即更新,待真正的 response 回來後會再更新一次快取跟 UI 。

import React, { Component } from "react";
// 引入 Mutation
import { Query, Mutation } from "react-apollo";
import { gql } from "apollo-boost";
import { Link } from "react-router";

const GET_POST_DETAIL = gql`
  query PostDetail($postId: ID!) {
    post(id: $postId) {
      id
      title
      body
      author {
        name
      }
      votes
    }
  }
`;

// 新增 mutation object
const UPVOTE_POST = gql`
  mutation UpvotePost($postId: ID!) {
    upvotePost(postId: $postId) {
      id
      title
      body
      votes
    }
  }
`;

class PostPage extends Component {
  render() {
    console.log(this.props.params.id);
    return (
      <Query
        query={GET_POST_DETAIL}
        variables={{ postId: this.props.params.id }}
      >
        {({ loading, error, data }) => {
          if (loading) return <p>Loading...</p>;
          if (error) return <p>Error :(</p>;
          const { post } = data;

          return (
            <div>
              <h3>{post.title}</h3>
              <p>{post.body}</p>
              <Link to="/">Back</Link>
              <hr />
              <Mutation mutation={UPVOTE_POST}>
                {(upvote, { data, loading }) => {
                  return (
                    <div
                      onClick={() =>
                        upvote({
                          variables: { postId: post.id },
                          optimisticResponse: {
                            __typename: "Mutation",
                            upvotePost: {
                              id: post.id,
                              __typename: "Post",
                              ...post,
                              votes: post.votes + 1
                            }
                          }
                        })
                      }
                    >
                      <i className="material-icons">thumb_up</i>
                      {post.votes}
                    </div>
                  );
                }}
              </Mutation>
            </div>
          );
        }}
      </Query>
    );
  }
}

這邊需注意 optimisticResponse 裡面的每一層都要加上 __typename 並且一定要回傳物件的 id ,這樣 Apollo Clietn 才可以依照 type 及 id 來動態更新快取。

完成後可以自己試一試有加 optimisticResponse 與沒加的區別,另外也可以打開兩個視窗展示同一個頁面,一邊按讚後另一邊再按讚,看第二次按讚的視窗的讚數會不會更新到正確的讚數。

2. 刪除文章

刪除文章部分,我們希望可以在文章列表執行,這樣可以快速刪除自己不想要的文章。打開 src/PostList.js ,先加入刪除的圖示。

const Posts = () => (
  <Query query={GET_POSTS}>
    {({ loading, error, data, refetch }) => {
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error :(</p>;
      const postList = data.posts.map(({ id, title }) => {
        return (
          <li key={id} className="collection-item">
            <Link to={`/posts/${id}`}>{title}</Link>
+            <i className="material-icons">delete</i>
          </li>
        );
      });
      ...

為了增加美觀,我們在 src/index.css 裡面加入這行:

.collection-item {
  display: flex;
  justify-content: space-between;
}

就會出現如下圖的圖案:

img

接著來加入 Mutation component 來執行 deletePost mutation 。

img

這邊同樣地我們要加入 optimisticResponse 幫助我們在按下刪除的瞬間就讓貼文消失,此外當 deletePost mutation 結束後我們也希望看到最新的文章列表,包含在中途被新增的或被刪除的結果都要一併顯示。

不過問題又來了, Apollo Clietn 的 optimisticResponse 雖然會幫助我們自動更新 UI ,但是只限於已經存在的資料,對於新增或是刪除等動作就需要 update 參數的幫忙。

import React from "react";
// 引入 Mutation
import { Query, Mutation } from "react-apollo";
import { gql } from "apollo-boost";
import { Link } from "react-router";

const GET_POSTS_FOR_AUTHOR = gql`
  query PostsForAuthor {
    posts {
      id
      title
      body
    }
  }
`;

// 新增 mutation object
const DELETE_POST = gql`
  mutation DeletePost($postId: ID!) {
    deletePost(postId: $postId) {
      id
    }
  }
`;

const Posts = () => (
  <Query query={GET_POSTS_FOR_AUTHOR}>
    {({ loading, error, data, refetch }) => {
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error :(</p>;
      const postList = data.posts.map(({ id, title }) => {
        return (
          <li key={id} className="collection-item">
            <Link to={`/posts/${id}`}>{title}</Link>
            <Mutation mutation={DELETE_POST} key={id}>
              {(deletePost, { data, loading, error }) => {
                if (!loading && !error) {
                  return (
                    <i
                      className="material-icons"
                      onClick={() =>
                        deletePost({
                          variables: { postId: id },
                          optimisticResponse: {
                            __typename: "Mutation",
                            deletePost: {
                              id,
                              __typename: "Post"
                            }
                          },
                          update: (proxy, { data: { deletePost } }) => {
                            const data = proxy.readQuery({
                              query: GET_POSTS_FOR_AUTHOR
                            });
                            const index = data.posts.findIndex(
                              post => post.id === deletePost.id
                            );
                            data.posts.splice(index, 1);
                            proxy.writeQuery({
                              query: GET_POSTS_FOR_AUTHOR,
                              data
                            });
                          }
                        }).then(() => refetch())
                      }
                    >
                      delete
                    </i>
                  );
                }
                return "";
              }}
            </Mutation>
          </li>
        );
      });

      return (
        <div>
          <ul className="collection">{postList}</ul>
          <Link to="/posts/new" className="btn-floating btn-large red right">
            <i className="material-icons">add</i>
          </Link>
        </div>
      );
    }}
  </Query>
);
export default Posts;

update 中,我們使用 proxy.readQuery 來讀取 cache 資料和使用 proxy.writeQuery 去更新 cache ,把指定資料刪除。

看到這邊大家可能會有疑問,好像 optimisticResponseupdate 之間好像沒有關係,但如果拿掉 optimisticResponse 就沒有立即刪除的效果。這是因為 update 會在 mutation response 回來時被觸發,而 optimisticRespons 就是搶在 mutation 回來前假造一個 response ,這時候會先觸發 update 一次,讓資料先從 cache 中消失,待真正的 mutation response 回來時 cache 中其實早就沒有那筆文章了。

另外做完 deletePost 後記得加上 .then(() => refetch()) 去重新整理整個文章列表 (畢竟刪除文章只有更新單筆資料),而這個 refetch 是上面的 Query component 讓我們傳進來的,與 data, loading, error 屬於同一個參數內。

可以試試看刪除當下資料會不會立即被刪掉,等待 mutation 做完後又會顯示最新的結果~


明天會繼續借紹其他功能~~

Edit React-GraphQL-Blog


Reference


上一篇
GraphQL 前端 (1) - 使用 React + Apollo Client 設計一個部落格系統
下一篇
GraphQL 前端 (3) - Authentication 與 Authorization
系列文
Think in GraphQL30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言