Як створити повнофункціональний клон Yelp за допомогою React & GraphQL (Dune World Edition)

Я не повинен боятися. Страх - це душегуб. Страх - це маленька смерть, яка приносить повне знищення. Я зіткнуся зі своїм страхом. Я дозволю йому пройти наді мною і крізь мене. А коли воно пройде повз, я зверну внутрішнє око, щоб побачити його шлях. Там, де страх пішов, нічого не буде. Залишуся лише я.

- "Літанія проти страху", Френк Герберт, Дюна

Можливо, вам цікаво: "Яке відношення страх має до програми React?" Перш за все, в додатку React немає чого боятися. Насправді, у цьому додатку ми заборонили страх. Хіба це не приємно?

Тепер, коли ви готові бути безстрашними, давайте обговоримо наш додаток. Це міні-клон Yelp, де замість того, щоб оглядати ресторани, користувачі оглядають планети з класичної науково-фантастичної серії Dune. (Чому? Тому що виходить новий фільм "Дюна" ... але повернемось до головного.)

Для створення нашого повнотекстового додатка ми використовуватимемо технології, які полегшують наше життя.

  1. Реагуйте: інтуїтивно зрозумілий, композиційний інтерфейс, тому що наш мозок любить складати речі.
  2. GraphQL: Ви, можливо, чули багато причин, чому GraphQL є чудовим. Безумовно, найважливішим є продуктивність та щастя розробників .
  3. Hasura: Налаштуйте автоматично згенерований API GraphQL поверх бази даних Postgres менш ніж за 30 секунд.
  4. Heroku: Для розміщення нашої бази даних.

А як GraphQL дарує мені щастя?

Я бачу, ти скептичний. Але ви, швидше за все, прийдете, як тільки проведете якийсь час із GraphiQL (ігровий майданчик GraphQL).

Використання GraphQL - це легкий для розробника інтерфейс, порівняно зі старими способами незграбних кінцевих точок REST. GraphQL дає вам єдину кінцеву точку, яка прослуховує всі ваші проблеми ... Я маю на увазі запити. Це такий чудовий слухач, що ти можеш сказати йому саме те, що хочеш, і він дасть тобі це, ні менше, ні більше.

Відчуваєте переживання цього терапевтичного досвіду? Давайте зануримось у підручник, щоб ви могли його спробувати якомога швидше!

?? Ось репо, якщо ви хочете кодувати.

Р мистецтво 1: S нитка пошук

S ТЕП 1: D eploy до Heroku

Першим кроком кожної гарної подорожі є присідання гарячого чаю та спокійне попивання. Після того, як ми це зробимо, ми можемо перейти до Heroku з веб-сайту Hasura. Це забезпечить нам усе необхідне: базу даних Postgres, наш механізм Hasura GraphQL та деякі закуски для подорожі.

black-books.png

Крок 2: Створіть таблицю планет

Наші користувачі хочуть переглянути планети. Тож ми створюємо таблицю Postgres через консоль Hasura для зберігання даних нашої планети. Заслуговує на увагу зла планета, Giedi Prime, яка привертає увагу своєю нетрадиційною кухнею.

Таблиця планет

Тим часом на вкладці GraphiQL: Hasura автоматично створила нашу схему GraphQL! Пограти з Провідником тут ??

Провідник GraphiQL

S ТЕП 3: З reate React додаток

Нам знадобиться інтерфейс для нашого додатка, тому ми створюємо додаток React та встановлюємо деякі бібліотеки для запитів GraphQL, маршрутизації та стилів. (Переконайтеся, що спочатку встановлено Node.)

> npx create-react-app melange > cd melange > npm install graphql @apollo/client react-router-dom @emotion/styled @emotion/core > npm start

S теп 4: S і ін вгору Apollo Client

Клієнт Apollo допоможе нам із нашими мережевими запитами та кешуванням GraphQL, щоб ми могли уникнути всієї цієї бурчання. Ми також робимо перший запит і перераховуємо наші планети! Наш додаток починає формуватися.

import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import Planets from "./components/Planets"; const client = new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "[YOUR HASURA GRAPHQL ENDPOINT]", }), }); const App = () => (    ); render(, document.getElementById("root"));

Ми тестуємо наш запит GraphQL в консолі Hasura перед тим, як скопіювати його в наш код.

import React from "react"; import { useQuery, gql } from "@apollo/client"; const PLANETS = gql` { planets { id name cuisine } } `; const Planets = ({ newPlanets }) => { const { loading, error, data } = useQuery(PLANETS); if (loading) return 

Loading ...

; if (error) return

Error :(

; return data.planets.map(({id, name, cuisine}) => (

{name} | {cuisine}

)); }; export default Planets;

S теп 5: S мозоль список

Наш список планет гарний, і все, але йому потрібен невеликий макіяж з Emotion (див. Репо для повних стилів).

Стилізований список планет

S ТЕП 6: S нитка пошук форми & стан

Наші користувачі хочуть шукати планети та упорядковувати їх за іменами. Тож ми додаємо форму пошуку, яка запитує нашу кінцеву точку за допомогою рядка пошуку, і передаємо результати, Planetsщоб оновити наш список планет. Ми також використовуємо React Hooks для управління нашим додатком.

import React, { useState } from "react"; import { useLazyQuery, gql } from "@apollo/client"; import Search from "./Search"; import Planets from "./Planets"; const SEARCH = gql` query Search($match: String) { planets(order_by: { name: asc }, where: { name: { _ilike: $match } }) { name cuisine id } } `; const PlanetSearch = () => { const [inputVal, setInputVal] = useState(""); const [search, { loading, error, data }] = useLazyQuery(SEARCH); return ( setInputVal(e.target.value)} onSearch={() => search({ variables: { match: `%${inputVal}%` } })} /> ); }; export default PlanetSearch;
import React from "react"; import { useQuery, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANETS = gql` { planets { id name cuisine } } `; const Planets = ({ newPlanets }) => { const { loading, error, data } = useQuery(PLANETS); const renderPlanets = (planets) => { return planets.map(({ id, name, cuisine }) => (  {name} {cuisine}  )); }; if (loading) return 

Loading ...

; if (error) return

Error :(

; return renderPlanets(newPlanets ; }; export default Planets;
import React from "react"; import styled from "@emotion/styled"; import { Input, Button } from "./shared/Form"; const SearchForm = styled.div` display: flex; align-items: center; > button { margin-left: 1rem; } `; const Search = ({ inputVal, onChange, onSearch }) => { return (   Search  ); }; export default Search;
import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import PlanetSearch from "./components/PlanetSearch"; import Logo from "./components/shared/Logo"; import "./index.css"; const client = new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "[YOUR HASURA GRAPHQL ENDPOINT]", }), }); const App = () => (     ); render(, document.getElementById("root"));

S теп 7: B е пишатися

Ми вже застосували наш список планет та функції пошуку! Ми з любов’ю дивимося на наші справи, робимо кілька селфі разом і переходимо до оглядів.

Список планет з пошуком

P мистецтво 2: L IVE відгуки

S теп 1: C відгуки reate стіл

Наші користувачі будуть відвідувати ці планети та писати відгуки про свій досвід. Ми створюємо таблицю через консоль Hasura для наших даних огляду.

Таблиця відгуків

We add a foreign key from the planet_id column to the id column in the planets table, to indicate that planet_ids of reviews have to match id's of planets.

Зовнішні ключі

Step 2: Track relationships

Each planet has multiple reviews, while each review has one planet: a one-to-many relationship. We create and track this relationship via the Hasura console, so it can be exposed in our GraphQL schema.

Відстеження стосунків

Now we can query reviews for each planet in the Explorer!

Запит на відгуки про планети

Step 3: Set up routing

We want to be able to click on a planet and view its reviews on a separate page. We set up routing with React Router, and list reviews on the planet page.

import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import { BrowserRouter, Switch, Route } from "react-router-dom"; import PlanetSearch from "./components/PlanetSearch"; import Planet from "./components/Planet"; import Logo from "./components/shared/Logo"; import "./index.css"; const client = new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "[YOUR HASURA GRAPHQL ENDPOINT]", }), }); const App = () => (          ); render(, document.getElementById("root"));
import React from "react"; import { useQuery, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANET = gql` query Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews { id body } } } `; const Planet = ({ match: { params: { id }, }, }) => { const { loading, error, data } = useQuery(PLANET, { variables: { id }, }); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

{reviews.map((review) => ( {review.body} ))} ); }; export default Planet;
import React from "react"; import { useQuery, gql } from "@apollo/client"; import { Link } from "react-router-dom"; import { List, ListItemWithLink } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANETS = gql` { planets { id name cuisine } } `; const Planets = ({ newPlanets }) => { const { loading, error, data } = useQuery(PLANETS); const renderPlanets = (planets) => { return planets.map(({ id, name, cuisine }) => (   {name} {cuisine}   )); }; if (loading) return 

Loading ...

; if (error) return

Error :(

; return ; }; export default Planets;

Step 4: Set up subscriptions

We install new libraries and set up Apollo Client to support subscriptions. Then, we change our reviews query to a subscription so it can show live updates.

> npm install @apollo/link-ws subscriptions-transport-ws
import React from "react"; import { render } from "react-dom"; import { ApolloProvider, ApolloClient, HttpLink, InMemoryCache, split, } from "@apollo/client"; import { getMainDefinition } from "@apollo/client/utilities"; import { WebSocketLink } from "@apollo/link-ws"; import { BrowserRouter, Switch, Route } from "react-router-dom"; import PlanetSearch from "./components/PlanetSearch"; import Planet from "./components/Planet"; import Logo from "./components/shared/Logo"; import "./index.css"; const GRAPHQL_ENDPOINT = "[YOUR HASURA GRAPHQL ENDPOINT]"; const httpLink = new HttpLink({ uri: `//${GRAPHQL_ENDPOINT}`, }); const wsLink = new WebSocketLink({ uri: `ws://${GRAPHQL_ENDPOINT}`, options: { reconnect: true, }, }); const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === "OperationDefinition" && definition.operation === "subscription" ); }, wsLink, httpLink ); const client = new ApolloClient({ cache: new InMemoryCache(), link: splitLink, }); const App = () => (          ); render(, document.getElementById("root"));
import React from "react"; import { useSubscription, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; const PLANET = gql` subscription Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews { id body } } } `; const Planet = ({ match: { params: { id }, }, }) => { const { loading, error, data } = useSubscription(PLANET, { variables: { id }, }); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

{reviews.map((review) => ( {review.body} ))} ); }; export default Planet;
Сторінка планети з оглядами в прямому ефірі

Step 5: Do a sandworm dance

We've implemented planets with live reviews! Do a little dance to celebrate before getting down to serious business.

Танець глистів

Part 3: Business logic

Step 1: Add input form

We want a way to submit reviews through our UI. We rename our search form to be a generic InputForm and add it above the review list.

import React, { useState } from "react"; import { useSubscription, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; import InputForm from "./shared/InputForm"; const PLANET = gql` subscription Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews(order_by: { created_at: desc }) { id body created_at } } } `; const Planet = ({ match: { params: { id }, }, }) => { const [inputVal, setInputVal] = useState(""); const { loading, error, data } = useSubscription(PLANET, { variables: { id }, }); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

setInputVal(e.target.value)} onSubmit={() => {}} buttonText="Submit" /> {reviews.map((review) => ( {review.body} ))} ); }; export default Planet;

Step 2: Test review mutation

We'll use a mutation to add new reviews. We test our mutation with GraphiQL in the Hasura console.

Вставте огляд мутації в GraphiQL

And convert it to accept variables so we can use it in our code.

Вставте мутацію огляду зі змінними

Step 3: Create action

The Bene Gesserit have requested us to not allow (cough censor cough) the word "fear" in the reviews. We create an action for the business logic that will check for this word whenever a user submits a review.

Inside our freshly minted action, we go to the "Codegen" tab.

We select the nodejs-express option, and copy the handler boilerplate code below.

Код шаблону для nodejs-express

We click "Try on Glitch," which takes us to a barebones express app, where we can paste our handler code.

Вставлення коду обробника в Glitch

Back inside our action, we set our handler URL to the one from our Glitch app, with the correct route from our handler code.

URL-адреса обробника

We can now test our action in the console. It runs like a regular mutation, because we don't have any business logic checking for the word "fear" yet.

Перевірка нашої дії в консолі

Step 4: Add business logic

In our handler, we add business logic that checks for "fear" inside the body of the review. If it's fearless, we run the mutation as usual. If not, we return an ominous error.

Перевірка ділової логіки

If we run the action with "fear" now, we get the error in the response:

Тестування нашої бізнес-логіки в консолі

Step 5: Order reviews

Our review order is currently topsy turvy. We add a created_at column to the reviews table so we can order by newest first.

reviews(order_by: { created_at: desc })

Step 6: Add review mutation

Finally, we update our action syntax with variables, and copy paste it into our code as a mutation. We update our code to run this mutation when a user submits a new review, so that our business logic can check it for compliance (ahem obedience ahem) before updating our database.

import React, { useState } from "react"; import { useSubscription, useMutation, gql } from "@apollo/client"; import { List, ListItem } from "./shared/List"; import { Badge } from "./shared/Badge"; import InputForm from "./shared/InputForm"; const PLANET = gql` subscription Planet($id: uuid!) { planets_by_pk(id: $id) { id name cuisine reviews(order_by: { created_at: desc }) { id body created_at } } } `; const ADD_REVIEW = gql` mutation($body: String!, $id: uuid!) { AddFearlessReview(body: $body, id: $id) { affected_rows } } `; const Planet = ({ match: { params: { id }, }, }) => { const [inputVal, setInputVal] = useState(""); const { loading, error, data } = useSubscription(PLANET, { variables: { id }, }); const [addReview] = useMutation(ADD_REVIEW); if (loading) return 

Loading ...

; if (error) return

Error :(

; const { name, cuisine, reviews } = data.planets_by_pk; return (

{name} {cuisine}

setInputVal(e.target.value)} onSubmit={() => { addReview({ variables: { id, body: inputVal } }) .then(() => setInputVal("")) .catch((e) => { setInputVal(e.message); }); }} buttonText="Submit" /> {reviews.map((review) => ( {review.body} ))} ); }; export default Planet;

If we submit a new review that includes "fear" now, we get our ominous error, which we display in the input field.

Тестування нашої дії за допомогою інтерфейсу користувача

Step 7: We did it! ?

Congrats on building a full-stack React & GraphQL app!

Дай п'ять

What does the future hold?

spice_must_flow.jpg

If only we had some spice melange, we would know. But we built so many features in so little time! We covered GraphQL queries, mutations, subscriptions, routing, searching, and even custom business logic with Hasura actions! I hope you had fun coding along.

Які ще функції ви хотіли б бачити в цьому додатку? Зв’яжіться зі мною у Twitter, і я зроблю більше підручників! Якщо вас надихнуло додавати функції самостійно, поділіться - я хотів би про них почути :)