讓我來接著來練習如何使用加入 mutation ,來實作一個簡單的部落格發文系統吧!
今天目標:
這邊一樣使用 create-react-app
來建立專案,以及安裝套件。
~ $ npx create-react-app react-graphql-blog
~ $ cd react-graphql-blog
~/react-graphql-blog $ npm install --save apollo-boost react-apollo graphql
因為我們這篇會用到 materialize 套件來幫助我們做畫面的美化以及 icon 支援,因此請打開 public/index.html
,在 head
裡面新增這兩行:
<head>
...
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/css/materialize.min.css"
/>
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
</head>
接著我們來到 src/App.js
,設定這次的 project 。同樣在這邊我會提供我寫好的 Apollo LaunchPad 範例 ,使用時直接將 url
帶入 https://j9qzxq4pjp.lp.gql.zone/graphql 。
import React, { Component } from 'react';
import ApolloClient, { InMemoryCache } from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
const client = new ApolloClient({
uri: 'https://j9qzxq4pjp.lp.gql.zone/graphql',
});
class App extends Component {
render() {
return (
<ApolloProvider client={client}>
<div>
<h2>My first Apollo app ?</h2>
</div>
</ApolloProvider>
);
}
}
npm start
後就會出現下圖的畫面 (這邊我用 codeSandbox ,若在本地端打開 localhost:3000 即可):
有了一個 React + Apollo Client 專案後,我們先試著把 Post 列表印出來! 新建一個檔案 src/PostList.js
,在裡面加入之前提過的 Query component 。
// src/PostList.js
import React from "react";
import { Query, Mutation } from "react-apollo";
import { gql } from "apollo-boost";
const GET_POSTS_FOR_AUTHOR = gql`
query PostsForAuthor {
posts {
id
title
body
}
}
`;
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">
{ title }
</li>
);
});
return (
<div>
<ul className="collection">{postList}</ul>
</div>
);
}}
</Query>
);
export default Posts;
接著將 src/PostList
加進 src/App.js
,然後在 src/App.js
用 Root
component 包起來,讓接下來的 component 都能夠自動置中間。
...
const App = ({ children }) => {
return <div className="container">{children}</div>;
};
class Root extends Component {
render() {
return (
<ApolloProvider client={client}>
<div>
<App>
<h2>My Blog ?</h2>
<PostList />
</App>
</div>
</ApolloProvider>
);
}
}
export default Root;
就會出現如下圖的效果:
一個 Post 列表就出現囉!
在開始開發其他功能之前,讓我們先安裝個 react-router
幫助我們導入 Route 來實作新功能!首先一樣先安裝,不過這邊的 demo 我是安裝第三版的 react-router
。
~/react-graphql-blog $ npm install --save react-router@^3.2.1
安裝成功後,打開 src/App.js
讓我們來看如何引入以及使用,因為這邊主要是介紹 GraphQL ,所以 react-router
的細節就不贅述了。
...
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
...
const App = ({ children }) => {
return (
<div className="container">
<h2>My Blog ?</h2>
{children}
</div>
);
};
class Root extends Component {
render() {
return (
<ApolloProvider client={client}>
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRoute component={PostList} />
</Route>
</Router>
</ApolloProvider>
);
}
}
再次打開 localhost:3000 後,如果照常顯示就代表成功設定好 react-router
了!
對於一個部落格,我們的首頁有了文章標題列表,但重點是每一篇文章裡的內容,因此讓我們來實作一個新的 Route 來顯示一個完整的文章頁面。
這邊我們分三步驟:
/posts/:id
route/posts/:id
route新增 route 前先建立 src/PostPage.js
並在裡面新增一個展示用 component。
import React from "react";
const PostPage = (props) => (<div> Post Page ({props.params.id})</div>);
export default PostPage;
接著打開 src/App.js
,引入 PostPage
component 並加入新增的 Route
component ,而路徑則是用 post/:id
。
// src/App.js
...
+ import PostPage from "./PostPage";
...
class Root extends Component {
render() {
return (
<ApolloProvider client={client}>
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRoute component={PostList} />
+ <Route path="posts/:id" component={PostPage} />
</Route>
</Router>
</ApolloProvider>
);
}
}
打開 src/PostList.js
,加上 react-router
的 Link
component ,它可以自動幫我們管理路徑以及紀錄瀏覽歷程。
// src/PostList.js
+ import { Link } from "react-router";
...
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>
+ </li>
);
});
return (
<div>
<ul className="collection">{postList}</ul>
</div>
);
}}
</Query>
);
結果會如下圖:
接著可以試試看點擊其中一個標題看看是否能成功進入頁面。
第三步讓我們回到新建立的 src/PostPage.js
並添加功能。
另外因為這次的 query 需要參數,因此要在 Query
component 加入 variables
prop ,而 postId
的值則由跟著網址所傳入的 this.props.id
來提供。
import React, { Component } from "react";
import { Query } 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
}
}
}
`;
class PostPage extends Component {
render() {
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>;
return (
<div>
<h3>{data.post.title}</h3>
<p>{data.post.body}</p>
<Link to="/">Back</Link>
</div>
);
}}
</Query>
);
}
}
export default PostPage;
完成後回到首頁,點擊第一個標題,就會進入文章頁面囉 ! 再點擊 Back 連結就能夠回到首頁。
接著新增文章功能也是類似的流程:
/posts/new
route/posts/new
route新增 route 前一樣先建立 src/PostAdd.js
並在裡面新增一個展示用 component。
import React from "react";
const PostAdd = () => (<div> Add Post Page</div>);
export default PostAdd;
接著打開 src/App.js
,引入 PostAdd
component 並加入新增的 Route
component ,務必注意要加在 post/:id
上方才不會被 :id
的字串比對給取代掉。
// src/App.js
...
+ import PostAdd from "./PostAdd";
...
class Root extends Component {
render() {
return (
<ApolloProvider client={client}>
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRoute component={PostList} />
+ <Route path="posts/new" component={PostAdd} />
<Route path="posts/:id" component={PostPage} />
</Route>
</Router>
</ApolloProvider>
);
}
}
接著在首頁網址後面加上 posts/new
看看新頁面有沒有被成功加入。
打開 src/PostList.js
新增「增加文章」按鈕。這邊一樣使用 Link
component 。
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>
</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>
);
結果如圖:
最後我們會在 src/AddPost.js
用到 Mutation
component 來處理 GraphQL mutation 。
使用 Mutation
component 時至少要將 gql 物件傳入 Mutation
component 的 mutation
prop 中,並且在 Mutation
prop 裡面提供一個 render function React 處理。
而這個 render function 會提供兩個參數:
variables
參數作為 Mutation variables 。需注意只有使用到 mutation function時才會觸發喔。
loading
(觸發 mutation 後執行中), data
(觸發後值行完成回傳的資料) 以及 error
(觸發後執行失敗) 資料。因此一個 Mutation
component 會如此呈現:
const MUTATE = gql`mutation ($id: ID!) {
mutate(id: $id) {
result
}
}`;
return (
<Mutation mutation={MUTATE}>
{(mutate, { data, loading, error }) => {
if (loading) return "mutating...";
if (error) return alert('Error');
const id = /* data processing... */
return <button onClick={() => mutate( { variables: { id } })} >Go!</button>
}}
</Mutation>
);
更多的用法請參考: Apollo Client - Mutation
回到 src/AddPost.js
,這裡我們會在 Mutation
component 裡面置入一個 form (表單) 幫助我們處理輸入資訊以及送出。資料送出後,呼叫 hashHistory.push("/")
回到首頁。
import React from "react";
import gql from "graphql-tag";
import { Link, hashHistory } from "react-router";
import { Mutation } from "react-apollo";
const ADD_POST = gql`
mutation AddPost($title: String!, $body: String) {
addPost(title: $title, body: $body) {
id
title
author {
id
name
}
votes
}
}
`;
const PostAdd = () => {
let titleInput;
let bodyInput;
return (
<Mutation mutation={ADD_POST} >
{(addPost, { data }) => (
<div>
<Link to="/">Back</Link>
<form
onSubmit={e => {
e.preventDefault();
addPost({
variables: { title: titleInput.value, body: bodyInput.value }
}).then(() => hashHistory.push("/"));
titleInput.value = "";
bodyInput.value = "";
}}
>
<label>Post Title:</label>
<input
ref={node => {
titleInput = node;
}}
/>
<label>Post Body:</label>
<input
ref={node => {
bodyInput = node;
}}
/>
<button type="submit">Add Post</button>
</form>
</div>
)}
</Mutation>
);
};
export default PostAdd;
完成後,如果你可以從首頁按 Add 按鈕進來並看到這個畫面就算成功囉!
![https://i.imgur.com/jbNTt0p.png]
不過這邊還有一個問題,那就是 Apollo Client 的 cache 會幫我們紀錄資料的 state ,因此新增完文章回首頁時, Apollo Client 會自動帶入之前 Qeury 取得的舊文章列表而非含有新的文章的列表,此時你還需要重整頁面新的文章才會浮現出來。
這當然不是我們想要的效果,我們想要新增完文章回首頁時,能夠立即看到新的文章列表,所以我們可以使用 Mutation
component 的 refetchQueries
prop 來幫助我們重新抓取文章列表。
這個 refetchQueries
prop 會在 mutation 結束後觸發一個 function (mutationResult) => Array<{ query, variables }> | string
,參數傳入的 mutationResult
裡面含有同樣的 data
, loading
, error
,而回傳部分規定要是一個 Array ,裡面要有你所希望重新發出的 query 。
我們希望重新發出的 query 就是文章列表的 query ,因此我們直接把 src/PostList.js
的 query object 複製過來 (之後會把 query object 都集中整理),並且放入 refetchQueries
prop 的 function 中。
+ const GET_POSTS_FOR_AUTHOR = gql`
+ query PostsForAuthor {
+ posts {
+ id
+ title
+ body
+ }
+ }
+ `;
const PostAdd = () => {
let titleInput;
let bodyInput;
return (
<Mutation
mutation={ADD_POST}
+ refetchQueries={() => {
+ return [{ query: GET_POSTS_FOR_AUTHOR }];
+ }}
>
...
</Mutation>
);
}
...
完成後就可以來測試看看囉~隨便輸入一些資料:
送出後回到首頁,第一眼雖然還是舊的文章列表,但約等個一下下 (文章列表 query 重新索取中),最新的文章標題就跟著一起出現囉~
如果你連那一下下都不能容忍,可以直接在 Mutation
component 加入 awaitRefetchQueries
prop 並設為 true
,這樣一來,等到 refetchQueries 完成後才會跳回首頁。
<Mutation
mutation={ADD_POST}
refetchQueries={() => {
return [{ query: GET_POSTS_FOR_AUTHOR }];
}}
+ awaitRefetchQueries={true}
>
...
</Mutation>
今天將簡單講解了 Mutation
component 的使用,可以看到加入 Mutation 後整個程式複雜了許多,光是處理資料呈現的同步問題就令人傷腦筋。但其實 Apollo Client 的 cache 功能可以另外設定,甚至可以做到某個物件更新時, Apollo Client 會跟著比對 type 與 id 來自動更新 cache 裡面的資料,讓使用體驗更加順暢、開發更容易!
不過明天我們要先繼續新增其他功能如「刪除文章」、「更新文章」、「文章按讚」等功能,之後會再花篇幅介紹如何設定 cache 。
最後附上本篇 project 的程式碼:
請問RequireAuth這個HOC判斷使用者登入的部分,可以用localStorage.getItem("x-token")取代嗎?