react apollo_使用GraphQL和Apollo React Hooks构建餐票应用程序

react apollo

I was at a conference this year and they handed attendees meal tickets for the after-party. It was a super amazing conference and I loved meeting everyone there, but dang... I forgot my meal ticket at the hotel and was starving while everyone else ate at the after-party! They served good looking milkshakes and the best could do was stare helplessly.

我今年参加了一次会议,他们递给与会者饭后饭票。 这真是一场超棒的会议,我喜欢在那里见到每个人,但是当...我忘了在酒店的饭票,而其他人在聚会后都在吃饭,我饿得要命! 他们喝了好看的奶昔,最好的办法就是无奈地凝视。

Why am I telling you about how I almost missed out on a great after-party dinner? How is this related to React Hooks and Apollo? It’s not. However, I did end up building a meal ticket tracker and I used GraphQL Apollo React Hooks – so you get it.

为什么我要告诉您我如何在一次很棒的晚宴后错过了机会? 这与React Hooks和Apollo有什么关系? 不是。 但是,最终我确实构建了一个餐单跟踪程序,并使用了GraphQL Apollo React Hooks –这样就可以了。

A meal ticket tracker allows you to issue digital meal tickets to attendees and not rely on paper tickets. You can still give them the ticket, but also have a system that tracks if the ticket has been used or not. When an attendee is served a meal, you can invalidate the ticket and the ticket cannot be reused. This way, attendees that forget their tickets can use an ID like badge to claim their meal.

餐票跟踪器使您可以向与会者发布数字餐票,而不必依赖纸质票。 您仍然可以给他们票证,但也可以使用一个系统来跟踪票证是否已使用。 当与会者用餐时,您可以使票证失效,并且该票证不能重复使用。 这样,忘记了门票的参与者可以使用ID(例如徽章)领取餐点。

Don’t tell me this is too serious. People take their food very seriously!

不要告诉我这太严重了。 人们非常重视自己的食物!

In summary, we need our demo to:

总而言之,我们需要演示以:

  1. Generate meal tickets from attendees data

    从与会者数据生成餐单
  2. Find meal ticket based on attendee name or ID

    根据与会者姓名或ID查找餐券
  3. Invalidate meal ticket in real-time.

    无效饭票在现实 - 时间
  4. Issue ticket in real-time (maybe someone joined from wait list).

    真正的问题票- 时间 (也许有人从等待名单加入)。

At the end of the day, we want to make sure that we write a React app using only React Hooks. The stack we’ll be working with is quite simple. We’ll use React with hooks on the frontend and 8base for the backend.

最终,我们要确保仅使用React Hooks编写一个React应用程序 。 我们将使用的堆栈非常简单。 我们将在前端使用带有钩子的React,在后端使用8base

要求 ( Requirements )

To follow this tutorial, a basic understanding of React and Node.js is required. Please ensure that you have Node and npm/yarn installed before you begin.

要遵循本教程,需要对ReactNode.js有基本的了解。 在开始之前,请确保已安装Node和npm / yarn

We’ll also be making GraphQL queries in the project, so some familiarity with GraphQL is helpful.

我们还将在项目中进行GraphQL查询,因此对GraphQL有所了解会有所帮助。

设置8base ( Setting up 8base )

To get started using 8base, follow the steps listed below:

要开始使用8base,请按照以下步骤操作:

1)注册 (1) Sign Up)

If you have an existing account, visit your 8base Dashboard select an existing Workspace. If you don’t have an account, create one on 8base. Their free plan will work for what we need.

如果您已有一个帐户,请访问8base 仪表板,选择一个现有的Workspace 。 如果您没有帐户,请在8base上创建一个帐户。 他们的免费计划将满足我们的需求。

2)建立资料模型 (2) Building the Data Model)

In the workspace, navigate to the Data Builder page and click on “+Add Table” to start building the data model. Were going to create two tables with the following fields.

在工作空间中,导航到“ 数据构建器”页面,然后单击“ +添加表”以开始构建数据模型。 将使用以下字段创建两个表。

Attendees | Field | Type | Description | Options | | --- | --- | --- | --- | | name | Text | The name of the attendee | mandatory=True |

参加者 | 领域 类型 描述 选项| | --- | --- | --- | --- | | name | 文字| 与会者姓名| mandatory=True |

MealTickets | Field | Type | Description | Options | | --- | --- | --- | --- | | valid | Switch | Ascertains the ticket’s validity | format=True/False
default=True |

餐票 | 领域 类型 描述 选项| | --- | --- | --- | --- | | 有效 开关| 确定机票的有效性| format=True/False
default=True |

Once the two tables are created, we need to establish a relationship between them. This can be done by dragging one table onto the other. However, let's build the one-to-many relationship manually on the Attendees table.

创建两个表后,我们需要在它们之间建立关系。 这可以通过将一个表拖到另一个表上来完成。 但是,让我们在“ 与会者”表上手动建立一对多关系。

Attendees | Field | Type | Description | Options | | --- | --- | --- | --- | | name | Text | The name of the attendee | mandatory=True | | MealTickets | Table | An attendees meal ticket's | table=MealTickets
Relation Field Name=Owner
Allow multiple MealTickets per Attendee=True
Allow multiple Attendees per MealTicket=False |

参加者 | 领域 类型 描述 选项| | --- | --- | --- | --- | | name | 文字| 与会者姓名| mandatory=True | | MealTickets | 桌子 与会者餐券的| table=MealTickets
Relation Field Name=Owner
Allow multiple MealTickets per Attendee=True
Allow multiple Attendees per MealTicket=False |

Before movin on, lets add some dummy records to our database. This can be done manually by clicking on a table and navigating to the data tab. However, you can also use the API Explorer. This time around, lets just run the following GraphQL mutation.

在继续之前,让我们向数据库添加一些虚拟记录。 可以通过单击表格并导航到“ data选项卡来手动完成此操作。 但是,您也可以使用API Explorer 。 这次,让我们运行以下GraphQL突变。

mutation{
  steve: attendeeCreate(data: { 
    name: "Steve Jones",
    mealTickets: {
      create: [{ valid: true }]
    }
  }) { ...attendeeResponse }

  bonnie: attendeeCreate(data: { 
    name: "Bonnie Riot",
    mealTickets: {
      create: [{ valid: true }]
    }
  }) { ...attendeeResponse }

  jack: attendeeCreate(data: { 
    name: "Jack Olark",
    mealTickets: {
      create: [{ valid: false }]
    }
  }) { ...attendeeResponse }
}

fragment attendeeResponse on Attendee {
  id 
  name
  mealTickets {
    count
  }
}

GraphQL mutations handle record creates, updates, and deletes. Using aliases (the keys named'steve', 'bonnie', etc.), we're able to run multiple operations in a single request. The data.mealTickets.create value will actually create an associated meal ticket record after the attendee record is created. That value is currently an array since attendees can have many tickets. Lastly, a fragment is simply a templates for our queries. Its contents could be written out in the query response plainly.

GraphQL突变处理记录的创建,更新和删除。 使用别名(键名为“ steve”,“ bonnie”等),我们可以在单个请求中运行多个操作。 创建与会者记录后, data.mealTickets.create值实际上将创建关联的餐券记录。 该值当前是一个数组,因为与会者可以拥有许多票证。 最后, fragment只是我们查询的模板。 它的内容可以清楚地写在查询响应中。

3)角色和权限 (3) Roles and Permissions)

To allow app users to securely access the API with appropriate permissions, were going to create a custom role. Navigate to Settings > Roles and create new role with the name "Meal Ticketer". Once created, click the role and lets update its permissions.

为了允许应用程序用户使用适当的权限安全地访问API,我们将创建一个自定义角色。 导航到Settings >“ Roles并使用名称“ Meal Ticketer”创建新角色。 创建角色后,单击角色,然后更新其权限。

Here we can update the Meal Ticketer's (a person using the app) permissions. For example, they should be able to do things like create attendees or mealTickets, and update mealTickets but not delete them. Let check the appropriate boxes and select the needed options.

在这里,我们可以更新Meal Ticketer(使用该应用程序的人)的权限。 例如,他们应该能够执行诸如创建attendeesmealTickets ,并更新mealTickets但不能删除它们。 让我们选中相应的框,然后选择所需的选项。

Meal Ticketer | Table | Create | Read | Update | Delete | Fields | | --- | --- | --- | --- | --- | --- | | Attendees | True | All Records | All Records | False | *Defaults | | MealTickets | True | All Records | All Records | False | *Defaults |

餐票 | 桌子 创建| 阅读| 更新| 删除| 领域| | --- | --- | --- | --- | --- | --- | | 参加者| 是的 所有记录| 所有记录| 错误| *默认| | 餐票| 是的 所有记录| 所有记录| 错误| *默认|

Now, all unauthenticated users who call the workspace API endpoint and have the Meal Ticketer role can permform these actions.

现在,所有调用工作区API端点并具有Meal Ticketer Ticketer角色的未经身份验证的用户都可以执行这些操作。

4)认证配置文件 (4) Authentication Profile)

Setting up authentication will allow users to sign-up, log-in, and log-out of the app. Users should be authenticated to view the list of attendees and to perform tasks like allocating and invalidating tickets. We’ll configure 8base to handle authentication.

设置身份验证将允许用户注册,登录和注销应用程序。 应该对用户进行身份验证,以查看与会者列表并执行诸如分配和取消票证之类的任务。 我们将配置8base来处理身份验证。

Navigate to the Authentication page to begin the setup. We’ll need to create an authentication profile that contains roles, allowed urls, etc.

导航到“ 身份验证”页面以开始设置。 我们需要创建一个包含角色,允许的url等的身份验证配置文件。

To create a new authentication profile, click the button with a plus-sign button and specify the following values:

要创建新的身份验证配置文件,请单击带有加号按钮的按钮并指定以下值:

OptionValueNotes
Name"Default Guest Auth"Choose any descriptive name
Type8base authenticationFind more auth info in the docs
Self SignupOpen to allLeave Off if using a free workspace
RolesMeal TicketerMultiple roles can be assigned to user on sign up
选项 笔记
Name “默认访客身份验证” 选择任何描述性名称
Type 8base认证 文档中查找更多身份验证信息
Self Signup 向所有人开放 如果使用空闲的工作空间,请Off
Roles 餐票 可以在注册时为用户分配多个角色

Add the new authentication profile. The information that’s now displayed is useful when connecting the client application to the authentication profile. Note the Authentication Profile Id, the Client ID and the Domain; these values will come in handy later in the article.

添加新的身份验证配置文件。 将客户端应用程序连接到身份验证配置文件时,现在显示的信息非常有用。 注意身份验证配置文件IdClient IDDomain ; 这些值将在本文的稍后部分派上用场。

Next, we’ll set the custom domains. Scroll down to where you see Custom Domains. This is where you can provide routes that’ll be used during authentication. Update your URLs to be similar to the screenshot below.

接下来,我们将设置自定义域。 向下滚动到看到“ Custom Domains 。 您可以在此处提供将在身份验证期间使用的路由。 更新您的URL,使其类似于下面的屏幕截图。

Note: make sure the \_localhost:port__ number matches that which your React app will run on_!

注意:确保\ _ localhost:port __号与您的React应用将在on_上运行的号相匹配!

5)获取Workspace API端点 (5) Getting the Workspace API Endpoint)

Lastly, let’s copy our workspace’s API endpoint. This endpoint is unique to our workspace, and is where we will send our GraphQL queries URL.

最后,让我们复制工作区的API端点。 该终结点对于我们的工作空间是唯一的,并且是我们将发送GraphQL查询URL的地方。

There are a few ways to obtain the endpoint. However, just navigate to the workspace Home page and you’ll find the endpoint in the bottom left. 

有几种获取端点的方法。 但是,只需导航到工作区主页 ,您将在左下角找到端点。

开发React挂钩 ( Developing React Hooks )

I created a starter project so that setup is easy and to ensure the tutorial focuses on getting started with 8base and GraphQL. The skeleton of the application has already been set up, this includes styling and project structure.

我创建了一个入门项目,以便于设置,并确保本教程侧重于8base和GraphQL入门。 应用程序的框架已经设置好,其中包括样式和项目结构。

克隆应用 (Cloning the App)

Run the following command to clone the repository:

运行以下命令来克隆存储库:

git clone https://github.com/christiannwamba/meal-tickets-tutorial-starter

Move into the folder and install the project’s dependencies by running the following command:

进入文件夹并通过运行以下命令安装项目的依赖项:

cd meal-tickets && yarn install

Now let’s start the React app server by running yarn start in the root folder of the project.

现在,通过在项目的根文件夹中运行yarn start来启动React应用服务器。

认证方式 (Authentication)

Dive into the codebase and open the src/authClient.js file. We’re going to replace the placeholder values with those that were created in the Authentication Profile; the AUTH_CLIENT_ID and AUTH_PROFILE_ID. Also, take a minute to read the in-code documentation.

深入代码库并打开src/authClient.js文件。 我们将用Authentication Profile中创建的占位符值替换掉; AUTH_CLIENT_IDAUTH_PROFILE_ID 。 另外,请花一点时间阅读代码文档。

/_ src/authClient.js _/

/_*
 _ Creating an Authentication Profile in 8base will provide 
 _ you with a AUTH_CLIENT_ID and AUTH_PROFILE_ID.
 _ 
 */

const AUTH0_CLIENT_ID = 'AUTH_CLIENT_ID';
const AUTH_PROFILE_ID = 'AUTH_PROFILE_ID';

In the src/index.js file, we import the authClient and supply it to the 8base App Provider. The App Provider component wraps our application and uses Apollo Client to help manage the authentication flow. In this file, lets update the URI with our workspace API endpoint.

src/index.js文件中,我们导入authClient并将其提供给8base App Provider。 App Provider组件包装了我们的应用程序,并使用Apollo Client帮助管理身份验证流程。 在此文件中,让我们使用工作空间API端点更新URI

const URI = '<WORKSPACE_API_ENDPOINT>';

登录 ( Login )

We’ll now implement login in the src/pages/Index.js component. The 8base-react-sdk exports an AuthContext function that checks whether the user is authorized before render. Wrapping AuthContext with the useContext hook gives access to some variables, including isAuthorized. When not authorized, the authClient is used to authenticate the user.

现在,我们将在src/pages/Index.js组件中实现登录。 8base-react-sdk导出AuthContext函数,该函数在渲染之前检查用户是否被授权。 使用useContext挂钩包装AuthContext可以访问某些变量,包括isAuthorized未经授权时, authClient用于验证用户。

Open the src/pages/index.js and add the following imports to the top of the file:

打开src/pages/index.js并将以下导入添加到文件顶部:

import React, { useContext } from 'react';
import { AuthContext } from '@8base/react-sdk';
import LoginImage from '../login.svg';

Let's read to in-code documentation to get a better grasp of what's happening. In-particular, pay attention to how we are wrapping AuthContext in use context.

让我们阅读编码文档,以更好地了解正在发生的事情。 特别是,请注意我们如何在使用上下文中包装AuthContext。

/_*
 _ Wrap AuthContext in useContext and unpack
 _ isAuthorized and authClient
 _ 
 */

export default function Index() {
  const date = new Date().toDateString();
  const { isAuthorized, authClient } = useContext(AuthContext);
  return (
    <>
      <div className="page-info">
        <img src="images/meal-ticket.svg" alt="" />
        <h1 className="img-title">meal ticket</h1>
        <p className="date">{date}</p>
      </div>
      {!isAuthorized ? (
        <div className="login-container">
          <img
            src={LoginImage}
            className="login-image"
            alt="Login to use the app"
          />
          <div>
            <button
              className="login-button"
              onClick={() => authClient.authorize()}
            >
              Login
            </button>
          </div>
        </div>
      ) : (
        <ul className="options">
          ...
        </ul>
      )}
    </>
  );
}

登出 ( Logout )

What goes up must come down, and what logs in must logout. Lets head over to the src/App.js file and make some changes to allow this.

上升的事物必须下降,而登录的事物必须注销。 让我们转到src/App.js文件并进行一些更改以允许这样做。

We spoke about the AuthContext import in the previous section. Now, we're importing an additional wrapper function called withApollo. It injects the Apollo Client into components passed as an argument. We need access to the Apollo Client to clear the store during logout.

在上一节中,我们谈到了AuthContext导入。 现在,我们将导入一个名为withApollo的附加包装函数。 它将Apollo客户程序注入作为参数传递的组件中。 我们需要访问Apollo客户端以在注销期间清除商店。

Update the App function to look like the snippet below:

更新App功能,使其看起来像下面的代码片段:

/** 
 **_ Wrap AppRouter with withApollo 
 _**
 **_
 _**/
const RouterApp = withApollo(AppRouter);

// ...
/**
 _ Use RouterApp as application router.
 _ 
 _
 _/
 <RouterApp></RouterApp>

Now, the AppRouter function has access to Apollo Client. So let's look at the AppRouter function that displays a button to trigger the authClient.logout() function, and then clears the store. Update the file to look like the snippet below:

现在, AppRouter函数可以访问Apollo Client。 因此,让我们看一下AppRouter函数,该函数显示一个按钮来触发authClient.logout()函数,然后清除存储。 更新文件,使其看起来像下面的代码片段:

/_*
  _ On logout, clear the store using 
  _ client and logout via authClient.
  _ 
  */

function AppRouter({ client }) {
  const { isAuthorized, authClient } = useContext(AuthContext);

  const logout = async () => {
    await client.clearStore();
    authClient.logout();
  };

  return (
    <div>
        <>
          {isAuthorized && (
            <div className="logout-container">
              <button className="logout-button" onClick={logout}>
                <p>
                  Logout <span></span>
                </p>
              </button>
            </div>
          )}
          <Route path="/" exact component={Index} />
          <Route path="/auth/callback" component={Auth} />
          ...
        </>
    </div>
  );
}

Now after initiating and completing the login flow, you should see a Logout button at the top of the page.

现在,启动并完成登录流程后,您应该在页面顶部看到一个Logout按钮。

Nice work! We’ve successfully implemented Authentication in the app. In the next section, we’ll start fetching and displaying records from our GraphQL API. Let’s get to it.

干得好! 我们已经在应用程序中成功实现了身份验证。 在下一节中,我们将开始从GraphQL API获取和显示记录。 让我们开始吧。

显示与会者列表 ( Display Attendees List )

We're going to first fetch a list of attendees from 8base. To do that, we need a GraphQL query to fetch the records, as well as drill the returned data down the component tree as a prop.

我们将首先从8base获取与会者列表。 为此,我们需要一个GraphQL查询来获取记录,以及将返回的数据向下钻入组件树作为道具。

Open the src/App.js file and add the following imports to the file:

打开src/App.js文件,并将以下导入添加到该文件:

import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

The gql function is for writing GraphQL queries and mutations with introspection and useQuery is a hook for fetching data from the 8base endpoint.

gql函数用于编写具有自省功能的GraphQL查询和变异,而useQuery是用于从8base端点获取数据的钩子。

Next, we'll look at adding the query that fetches the attendees list, along with their meal tickets. This query gets passed as an argument to the useQuery Apollo-hook. In the same file, update the ATTENDEES_LIST to look like the snippet below:

接下来,我们将看一下添加查询的列表,该查询将获取与会者列表以及他们的餐券。 该查询作为参数传递给useQuery Apollo-hook。 在同一文件中,更新ATTENDEES_LIST以使其类似于下面的代码片段:

const ATTENDEES_LIST = gql`{
    attendeesList {
      count
      items {
        name
        id
        mealTickets {
          items {
            id
            valid
          }
        }
      }
    }
  }`;

The useQuery function exposes a loading variable that can be used when awaiting a response; we'll use it to ensure the request is completed before accessing the data variable. This check is in the return statement of the App.js component .

useQuery函数公开了一个loading变量,可以在等待响应时使用它。 我们将使用它来确保在访问data变量之前完成请求。 此检查位于App.js组件的return语句中。

/_*
 _ Unpack loading and data from useQuery hook.
 _  
 _/
const { loading, data } = useQuery(ATTENDEES_LIST);

{/_ Render loading message when running query _/}
return (
    <div>
      {loading ? ( // add check to ensure the data is fully loaded before displaying.
        <p>Loading...</p>
      ) : (
        <>
          {isAuthorized && (
            <div className="logout-container">
              ...
            </div>
          )}
          <Route path="/" exact component={Index} />
          <Route
            path="/generate/"
            component={() => (
              <Generate
                attendees={data.attendeesList.items} // Pass the list of attendees as props to the component
              />
            )}
          />
          <Route
            path="/tickets/"
            component={() => (
              <Tickets
                attendees={data.attendeesList.items} // Pass the list of attendees as props to the component
              />
            )}
          />
        </>
      )}
    </div>
);

At the start of the return statement, there is a check for when the query request is loading and then we display a loading indicator. When loading is done, the application routes are then rendered and the attendees list is passed as props to the Ticket and Generate components. Within those components, we’ll filter the list.

在return语句的开头,将检查查询请求何时loading ,然后显示一个加载指示符。 加载完成后,将呈现应用程序路线,并将与会者列表作为道具传递给TicketGenerate组件。 在这些组件中,我们将过滤列表。

In the Generate component, let's filter out attendees with valid tickets before doing the opposite in the Tickets component.

在“ Generate组件中,让我们在“ Tickets组件中执行相反操作之前,先过滤出具有有效票证的与会者。

Open the src/pages/Generate.js file and add the function below to the bottom of the file. The function filters the attendees list of and only returns those without valid tickets.

打开src/pages/Generate.js文件,并将下面的函数添加到文件底部。 该功能过滤与会者列表,仅返回没有有效票证的与会者。

function hasInvalidTicket(attendees) {
  return attendees.filter(({ mealTickets: { items: mealTickets = [] } }) => {
    const hasInvalidTicket = mealTickets.every((ticket) => !ticket.valid);
    return hasInvalidTicket;
  });
}

The function runs through the list of attendees, filtering out those with valid tickets, leaving attendees that need fresh tickets. Update the rest of the component to look the following way:

该功能遍历与会者列表,过滤出具有有效票证的与会者,留下需要新票证的与会者。 更新组件的其余部分,以使其看起来像以下方式:

export default function Generate({ attendees }) {
  const attendeesWithNoOrInvalidTickets = hasInvalidTicket(attendees);

  return (
    <>
      <BackButton />
      <div className="search-wrapper-wp">
        ...
      </div>
      <div className="main-wrapper">
        <h2 className="page-title">SEARCH RESULT</h2>
        <ul className="search-result">
          {/_ Loop through attendees list and display their name _/ }
          {attendeesWithNoOrInvalidTickets.map((attendee) => (
            <Ticket key={attendee.id} attendee={attendee}>
              <GenerateButton/>
            </Ticket>
          ))}
        </ul>
      </div>
    </>
  );
}

Head over to the src/pages/Tickets.js file and do the opposite for the component. In this component, we’ll filter out users with invalid tickets and leave those with valid tickets. Add the following function at the bottom of the page:

转到src/pages/Tickets.js文件,然后对该组件执行相反的操作。 在此组件中,我们将过滤出具有无效票证的用户,并保留具有有效票证的用户。 在页面底部添加以下功能:

function hasValidTicket(attendees) {
  return attendees.filter((attendee) => {
    const mealTickets = attendee.mealTickets.items;
    const validTickets = mealTickets.filter((ticket) => ticket.valid);
    return validTickets.length > 0;
  });
}

The function checks the list for attendees with active tickets and filters out those without. Update the component to display the attendees returned from the function:

该功能检查列表中是否有活动票证的参与者,并过滤掉那些没有活动票证的参与者。 更新组件以显示从函数返回的与会者:

export default function Tickets({ attendees }) {
  const attendeesWithValidTickets = hasValidTicket(attendees);

  return (
    <>
      <BackButton />
      <div className="search-wrapper-wp">
        ...
      </div>
      <div className="main-wrapper">
        <h2 className="page-title">SEARCH RESULT</h2>
        <ul className="search-result">
          {attendeesWithValidTickets.map((attendee) => (
            <Ticket attendee={attendee} key={attendee.id}>
              <InvalidateButton/>
            </Ticket>
          ))}
        </ul>
      </div>
    </>
  );
}

You can find the complete file for the Generate page and the Tickets page on Github.

您可以在Github上找到Generate页面和Tickets页面的完整文件。

We're getting there! Next, we’ll work on generating tickets for an attendee.

我们到了! 接下来,我们将为与会者生成票证。

为与会者生成票证 ( Generating tickets for attendees )

To handle generating tickets, we’ll use the useMutation hook from Apollo. We'll use a mutation to create meal tickets, and add the ticket data using a variable. In the src/pages/Generate.js file, add the following imports and mutation string to the file:

为了处理生成票证,我们将使用Apollo的useMutation挂钩。 我们将使用一种变异来创建餐单,并使用变量添加餐单数据。 在src/pages/Generate.js文件中,将以下导入和突变字符串添加到文件中:

import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';

const GENERATE_TICKET = gql`mutation GenerateTicket($data: MealTicketCreateInput!) {
    mealTicketCreate(data: $data) {
      id
    }
  }`;

The GENERATE_TICKET value will be passed to the useMutation hook an argument. Calling the hook with the string will return a function for running mutation. On a side note, it's practice for the return function to share a similar name with the mutation operation.

GENERATE_TICKET值将传递给useMutation钩子参数。 用字符串调用钩子将返回一个用于运行突变的函数。 附带说明一下,惯例是return函数与mutation操作共享相似的名称。

const [mealTicketCreate] = useMutation(GENERATE_TICKET);

Now, lets generate a ticket! Update the src/pages/Generate.js file with the following function:

现在,让我们生成一张票! 使用以下功能更新src/pages/Generate.js文件:

/_ src/pages/Generate.js _/

const onGenerateClick = (attendeeId, generateTicket) => {
    const data = {
    variables: {
        data: {
        valid: true,
        owner: {
            connect: {
            id: attendeeId,
            },
        },
        },
    },
    };
    generateTicket(data);
};

export default function Generate({ attendees }) {
    ...
}

The onGenerateClick function takes two arguments, the attendeeId and a generateTicket mutation function for running mutations. Within the function, we curate the body of the mutation.

onGenerateClick函数采用两个参数,即attendeeId onGenerateClick和用于运行突变的generateTicket突变函数。 在功能内,我们管理突变体。

The data object for the intended ticket has two properties:

预期票证的data对象具有两个属性:

  • valid: the current state of the ticket. The validity state will be set to false after the ticket is used.

    valid :票证的当前状态。 使用票证后,有效性状态将设置为false。
  • owner: the attendee will be connected to the ticket using the attendeeId.

    owner :与会者将连接到使用的车票attendeeId

The function is ready, let's put it to use. Pass the function to the onClick prop of the Generate component. Follow the snippet below:

该函数已准备就绪,让我们使用它。 将该函数传递给Generate组件的onClick属性。 请按照以下代码段进行操作:

// src/pages/Generate.js
...
export default function Generate({ attendees }) {
  const [mealTicketCreate, { data }] = useMutation(GENERATE_TICKET);
  const attendeesWithNoOrInvalidTickets = hasInvalidTicket(attendees);
  return (
    <>
      <BackButton />
      <div className="search-wrapper-wp">
        ...
      </div>
      <div className="main-wrapper">
        <h2 className="page-title">SEARCH RESULT</h2>
        <ul className="search-result">
          {attendeesWithNoOrInvalidTickets.map((attendee) => (
            <Ticket key={attendee.id} attendee={attendee}>
              <GenerateButton
                onClick={() => onGenerateClick(attendee.id, mealTicketCreate)} // --- call the function with the attendeeId and the mutation function
              ></GenerateButton>
            </Ticket>
          ))}
        </ul>
      </div>
    </>
  );
}
...

Call the onGenerateClick function with the attendee.id value and the mutation function mealTicketCreate. After this update, you should be able to successfully create tickets for an attendee.

调用onGenerateClick与功能attendee.id值和突变功能mealTicketCreate 。 更新之后,您应该能够成功为与会者创建票证。

After the ticket is generated, the attendee should move from the Generate page to the Tickets page. We'll add the ability to see this in real-time using subscriptions later in the article. We can generate tickets, how do we invalidate them? Let's figure it out together in the next section.

生成票证后,与会者应从“ 生成”页面移至“ 票证”页面。 我们将在本文后面的部分中添加使用订阅实时查看此功能的功能。 我们可以生成票证,如何使它们无效? 让我们在下一节中一起弄清楚。

无效的与会者票 ( Invalidating attendee tickets )

After an attendee fetches a meal, it's only right to invalidate their ticket to prevent them from coming over for a second or even a third round. To achieve this, we'll run on the assumption that a user will only have one valid ticket at a time. So when invalidating, we find the only active ticket and make it invalid. Doing this will move the attendee back to the Generate page.

与会者取餐后,唯一有效的方法是使他们的门票无效,以防止他们进入第二轮甚至第三轮。 为此,我们将假设用户一次只能拥有一张有效票证。 因此,当失效时,我们会找到唯一的活动故障单并使之无效。 这样做会将与会者移回“ 生成”页面。

Enough talk, let's put code on editor; add the following imports to the src/pages/Tickets.js page.

聊够了,让我们把代码放到编辑器上。 将以下导入添加到src/pages/Tickets.js页面。

import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';

Let's add a mutation string for invalidating tickets. Add the following in the file:

让我们添加一个用于使票证无效的突变字符串。 在文件中添加以下内容:

const INVALIDATE_TICKET = gql`mutation InvalidateTicket($data: MealTicketUpdateInput!) {
    mealTicketUpdate(data: $data) {
      id
    }
  }`;

Pass the string to the useMutation hook, the return function will be used in the click handler to run mutations:

将字符串传递给useMutation挂钩,在点击处理程序中将使用return函数来运行突变:

export default function Tickets({ attendees }) {
  const [mealTicketUpdate] = useMutation(INVALIDATE_TICKET);
  const attendeesWithValidTickets = hasValidTicket(attendees);
  return <>...</>;
}

The return statement has been omitted for brevity. No changes were made to that section of the component

为简洁起见,已省略了return语句。 组件的该部分未做任何更改

We can make use oof the mutation function in the event handler, add the following function to the src/pages/Tickets.js file:

我们可以在事件处理程序中使用oof突变函数,将以下函数添加到src/pages/Tickets.js文件中:

import React, { useEffect } from 'react';
    ...
    const INVALIDATE_TICKET = gql`...`;
    const getValidTicket = (tickets) => {
      return tickets.find((ticket) => ticket.valid);
    };
    const onInvalidateClick = (tickets, inValidateTicket) => {
      const validTicket = getValidTicket(tickets);
      const data = {
        variables: {
          data: {
            id: validTicket.id,
            valid: false,
          },
        },
      };
      inValidateTicket(data);
    };
    export default function Tickets({ attendees, search, searchTerm }) {
      ...
    }

The onInvalidateClick function takes two arguments, the tickets array containing all the tickets belonging to an attendee and the mutation function invalidateTicket. Within the function, we call the getValidTicket function to get the valid ticket of the attendee. With that, we curate the body of the mutation using the id of the ticket and setting it the valid state to false thus invalidating it.

onInvalidateClick函数onInvalidateClick两个参数, tickets数组包含属于与会者的所有票证,而变异函数invalidateTicket 。 在该函数内,我们调用getValidTicket函数以获取与会者的有效票证。 这样,我们使用票证的id来管理突变体,并将其valid状态设置为false从而使其无效。

Pass the function to the Invalidate component's onClick prop. After the change the function should look like the snippet below:

将函数传递给Invalidate组件的onClick属性。 更改后,该功能应类似于以下代码段:

// src/pages/Tickets.js

import React, { useEffect } from 'react';
...

const INVALIDATE_TICKET = gql`...`;

const getValidTicket = (tickets) => {
  return tickets.find((ticket) => ticket.valid);
};

const onInvalidateClick = (tickets, inValidateTicket) => {
  ...
};

export default function Tickets({ attendees }) {
  const [mealTicketUpdate] = useMutation(INVALIDATE_TICKET);
  const attendeesWithValidTickets = hasValidTicket(attendees);
  return (
    <>
      <BackButton />
      <div className="search-wrapper-wp">...</div>
      <div className="main-wrapper">
        <h2 className="page-title">SEARCH RESULT</h2>
        <ul className="search-result">
          {attendeesWithValidTickets.map((attendee) => (
            <Ticket attendee={attendee} key={attendee.id}>
              <InvalidateButton
                onClick={() =>
                  onInvalidateClick(
                    attendee.mealTickets.items,
                    mealTicketUpdate
                  )
                }
              ></InvalidateButton>
            </Ticket>
          ))}
        </ul>
      </div>
    </>
  );
}

Call the onInvalidateClick function with the list of tickets attendee.mealTickets.items value and the mutation function mealTicketUpdate. After this update, you should be able to invalidate a ticket, moving the user back to the next Generate page.

调用onInvalidateClick与门票的列表功能attendee.mealTickets.items值和突变功能mealTicketUpdate 。 更新之后,您应该能够使票证无效,将用户移回到下一个“ 生成”页面。

Let's consider scrolling through a long list of attendees trying to find an attendee to generate a ticket for or invalidate an existing ticket. It would be a lot easier to search through the list to find an attendee. We have the search bar already, so let's add functionality to search as you type.

让我们考虑滚动浏览一长串与会者,尝试找到一个与会者来生成票证或使现有票证失效。 搜索列表以查找与会者会容易得多。 我们已经有了搜索栏,因此让我们在键入时添加搜索功能。

正在搜寻 ( Searching )

To implement the search feature, we'll make use of the graphql filter object. The name field of the attendee will be checked if it contains the string entered in the search field. Doing this will involve updating the query string in the App component for fetching attendees and passing a variables object to the useQuery hook.

为了实现搜索功能,我们将使用graphql filter对象。 如果与会者的name字段contains在搜索字段中输入的字符串,则将对其进行检查。 这样做将涉及更新App组件中的查询字符串以获取与会者并将variables对象传递给useQuery挂钩。

Open the src/App.js file and update the React import to include the useState hook like the snippet below:

打开src/App.js文件并更新React导入以包括useState挂钩,如下面的代码片段所示:

// src/App.js
import React, { useState } from 'react';

And then update the GET_ATTENDEES string to look like the snippet below:

然后更新GET_ATTENDEES字符串,使其看起来像下面的代码片段:

// src/App.js
const GET_ATTENDEES = gql`query Attendees($searchTerm: String!) {
    attendeesList(filter: { name: { contains: $searchTerm } }) {
      count
      items {
        name
        id
        mealTickets {
          items {
            id
            valid
          }
        }
      }
    }
  }`;

Then, we'll update the line calling the useQuery hook to include the variables object and create a state value using the useState hook.

然后,我们将更新调用useQuery挂钩的行以包含variables对象,并使用useState挂钩创建状态值。

// src/App.js

import React, { useState } from 'react';
...

const GET_ATTENDEES = gql`...`;

function App() {
 ...
}

function AppRouter() {
  const [searchTerm, setSearchTerm] = useState('');
  const { isAuthorized, authClient } = useContext(AuthContext);
  const { loading, data } = useQuery(GET_ATTENDEES, {
    variables: { searchTerm },
  });

  const logout = async () => {
    ...
  };
  return <div>...</div>;
}

...

The searchTerm and setSearchTerm will be you used to manage the update from the search field. Within the AppRouter component, pass the searchTerm and the setSearchTerm values to the Generate and Tickets components.

searchTermsetSearchTerm将是您用于管理从搜索领域的更新。 内AppRouter组件,通过searchTermsetSearchTerm值的GenerateTickets部件。

// src/App.js

function AppRouter() {
  ...

  return (
    <div>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <>
          {isAuthorized && (
            ...
          )}
          <Route path="/" exact component={Index} />
          <Route
            path="/generate/"
            component={() => (
              <Generate
                attendees={data.attendeesList.items}
                search={setSearchTerm}
                searchTerm={searchTerm}
              />
            )}
          />
          <Route
            path="/tickets/"
            component={() => (
              <Tickets
                attendees={data.attendeesList.items}
                search={setSearchTerm}
                searchTerm={searchTerm}
              />
            )}
          />
        </>
      )}
    </div>
  );
}

Within the src/pages/Generate.js file, pass the searchTerm and search props to the search input.

src/pages/Generate.js文件中,将searchTermsearch props传递给搜索输入。

// src/pages/Generate.js

export default function Generate({ attendees, search, searchTerm }) {
  const [mealTicketCreate, { data }] = useMutation(GENERATE_TICKET);
  const attendeesWithNoOrInvalidTickets = hasInvalidTicket(attendees);
  return (
    <>
      <div className="search-wrapper-wp">
        <div className="search-wrapper">
          <a href="#search">
            <svg className="search">
              <use href="images/icons.svg#search"></use>
            </svg>
          </a>
          <input
            id="search"
            type="search"
            value={searchTerm}
            onChange={(e) => search(e.target.value)}
          />
        </div>
      </div>
      <div className="main-wrapper">
        ...
      </div>
    </>
  );
}

And within the src/pages/Tickets.js do same, similar to the screenshot shown below:

并在src/pages/Tickets.js执行相同的操作,类似于以下所示的屏幕截图:

// src/pages/Tickets.js

export default function Tickets({ attendees, search, searchTerm }) {
  const [mealTicketUpdate, { data }] = useMutation(INVALIDATE_TICKET);
  const attendeesWithValidTickets = hasValidTicket(attendees);
  return (
    <>
      <div className="search-wrapper-wp">
        <div className="search-wrapper">
          <a href="#search">
            <svg className="search">
              <use href="images/icons.svg#search"></use>
            </svg>
          </a>
          <input
            id="search"
            type="search"
            value={searchTerm}
            onChange={(e) => search(e.target.value)}
          />
        </div>
      </div>
      <div className="main-wrapper">
        ...
      </div>
    </>
  );
}

Now you can easily search for an attendee

Searching should function properly now if you try. Visit http://localhost:3000/tickets or http://localhost:3000/generate to test it out. In the next section, we’ll set up the app to receive real-time updates using GraphQL subscriptions.

如果您尝试进行搜索,现在应该可以正常运行了。 访问http:// localhost:3000 / ticketshttp:// localhost:3000 / generate进行测试。 在下一节中,我们将设置应用程序以使用GraphQL订阅接收实时更新。

订阅实时更新 ( Real-time updates with subscriptions )

To get started with subscriptions, we’ll have to create a GraphQL subscription string to manage fetching of updates. We’ll write a string to subscribe to the record of the ticket, open the src/App.js file and make the following changes.

要开始订阅,我们必须创建一个GraphQL订阅字符串来管理更新的获取。 我们将编写一个字符串来订阅票证记录,打开src/App.js文件并进行以下更改。

Include the useSubscription hook as one of the named imports from apollo-hooks and the useEffect hook from React:

包括useSubscription钩子作为apollo-hooks命名的导入之一,并包括React的useEffect钩子:

import React, { useState, useContext, useEffect } from 'react';
import { useQuery, useSubscription } from '@apollo/react-hooks';

Add the following string next to get real-time updates from the Tickets record:

添加以下字符串以从“ 票证”记录中获取实时更新:

const TICKETS_SUB = gql`subscription AttendeeSub {
    MealTickets {
      node {
        owner {
          id
        }
        id
        valid
      }
      mutation
    }
  }`;

This should go below the GET_ATTENDEES variable. Next, we'll pass the string to the useSubscription hook:

它应该位于GET_ATTENDEES变量下面。 接下来,我们将字符串传递给useSubscription钩子:

// src/App.js

...

function AppRouter({ client }) {
  const [searchTerm, setSearchTerm] = useState('');
  ...
  const [attendees, setAttendees] = useState([]); // Add a state variable for storing the attendees array.
  const subscription = useSubscription(ATTENDEES_SUB);

  const logout = async () => {
    ...
  };
  useEffect(() => {
    if (!loading) {
      setAttendees(data.attendeesList.items);
      updateAttendeeRecord(subscription, attendees, setAttendees);
    }
  }, [data, subscription.data]);
  return (
    <div>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <>
          ...
          <Route
            path="/generate/"
            component={() => (
              <Generate
                attendees={attendees} //update prop to use the state variable
                search={setSearchTerm}
                searchTerm={searchTerm}
              />
            )}
          />
          <Route
            path="/tickets/"
            component={() => (
              <Tickets
                attendees={attendees} //update prop to use the state variable
                search={setSearchTerm}
                searchTerm={searchTerm}
              />
            )}
          />
        </>
      )}
    </div>
  );
}
...

In the snippet above, we created a state variable for storing the list the of attendees returned from the useQuery hook. Below that, we call the useSubscription passing TICKETS_SUB as an argument. When there are updates to the Tickets collection, the data will be available in the subscription variable.

在上面的代码段中,我们创建了一个状态变量,用于存储从useQuery挂钩返回的attendees列表。 在此之下,我们调用useSubscription并传递TICKETS_SUB作为参数。 当Tickets集合有更新时,数据将在subscription变量中可用。

Within the useEffect hook, we check if request to fetch the attendees list is still processing using the loading variable. If loading is false, we’ll set the data returned to the state as attendees. Below that, we have a function we haven’t yet defined, the function will use the updates from the subscription to update the list of attendees. We also updated the attendees prop passed to the Tickets and Generate components, from data.attendeeList.items to attendees.

useEffect挂钩中,我们使用loading变量检查是否仍在处理获取参与者列表的请求。 如果loadingfalse ,我们将返回的数据设置为attendees 。 在此之下,我们还有一个尚未定义的功能,该功能将使用订阅中的更新来更新与会者列表。 我们还更新了传递到TicketsGenerate组件的attendees道具,从data.attendeeList.itemsattendees

Add the following function to the bottom of the file:

在文件底部添加以下功能:

// src/App.js

const updateAttendeeRecord = (subRes, attendees, setAttendees) => {
  if (subRes.data) {
    const { node, mutation } = subRes.data.MealTickets;
    const updatedAttendees = attendees.map((attendee) => {
      if (node.owner.id === attendee.id) {
        const tickets = attendee.mealTickets.items;
        if (mutation === 'create') {
          attendee.mealTickets = {
            items: attendee.mealTickets.items.concat(node),
          };
          return attendee;
        } else if (mutation === 'update') {
          const updatedTickets = attendee.mealTickets.items.map((item) => {
            if (item.id === node.id) {
              return {
                ...item,
                ...node,
              };
            }
            return item;
          });
          attendee.mealTickets = {
            items: updatedTickets,
          };
          return attendee;
        }
      } else {
        return attendee;
      }
    });
    setAttendees(updatedAttendees);
  }
};

In the function, we check if there’s data returned from the subscription, then we loop through the attendees list and check for an attendee with an id matching the ticket owner id.

在该函数中,我们检查是否有从订阅中返回的数据,然后遍历attendees列表,并检查id与票证所有者id匹配的与会者。

In the next execution, we get the mutation type and add the new ticket to the ticket list of the attendee if the mutation type is create and if the type is update we find the ticket and replace the ticket fields with that returned from the subscription update.

在接下来的执行,我们得到的mutation类型和新的票证添加到与会者的门票清单,如果mutation类型是create ,如果类型是update我们找到了车票,代之以从签约更新返回的车票领域。

In the end, we set the updatedAttendees to state. Now, you can open two browser tabs and try generating/invalidating tickets to see attendees move between the lists in real-time.

最后,我们将updatedAttendees设置为state。 现在,您可以打开两个浏览器选项卡,并尝试生成/禁用票证以查看与会者实时在列表之间移动。

You can find the code for this article on GitHub. Happy coding

您可以在GitHub上找到本文的代码。 快乐编码

翻译自: https://scotch.io/tutorials/build-a-meal-ticketing-app-with-graphql-and-apollo-react-hooks

react apollo

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值