Я не повинен боятися. Страх - це душегуб. Страх - це маленька смерть, яка приносить повне знищення. Я зіткнуся зі своїм страхом. Я дозволю йому пройти наді мною і крізь мене. А коли воно пройде повз, я зверну внутрішнє око, щоб побачити його шлях. Там, де страх пішов, нічого не буде. Залишуся лише я.
- "Літанія проти страху", Френк Герберт, Дюна
Можливо, вам цікаво: "Яке відношення страх має до програми React?" Перш за все, в додатку React немає чого боятися. Насправді, у цьому додатку ми заборонили страх. Хіба це не приємно?
Тепер, коли ви готові бути безстрашними, давайте обговоримо наш додаток. Це міні-клон Yelp, де замість того, щоб оглядати ресторани, користувачі оглядають планети з класичної науково-фантастичної серії Dune. (Чому? Тому що виходить новий фільм "Дюна" ... але повернемось до головного.)
Для створення нашого повнотекстового додатка ми використовуватимемо технології, які полегшують наше життя.
- Реагуйте: інтуїтивно зрозумілий, композиційний інтерфейс, тому що наш мозок любить складати речі.
- GraphQL: Ви, можливо, чули багато причин, чому GraphQL є чудовим. Безумовно, найважливішим є продуктивність та щастя розробників .
- Hasura: Налаштуйте автоматично згенерований API GraphQL поверх бази даних Postgres менш ніж за 30 секунд.
- Heroku: Для розміщення нашої бази даних.
А як GraphQL дарує мені щастя?
Я бачу, ти скептичний. Але ви, швидше за все, прийдете, як тільки проведете якийсь час із GraphiQL (ігровий майданчик GraphQL).
Використання GraphQL - це легкий для розробника інтерфейс, порівняно зі старими способами незграбних кінцевих точок REST. GraphQL дає вам єдину кінцеву точку, яка прослуховує всі ваші проблеми ... Я маю на увазі запити. Це такий чудовий слухач, що ти можеш сказати йому саме те, що хочеш, і він дасть тобі це, ні менше, ні більше.
Відчуваєте переживання цього терапевтичного досвіду? Давайте зануримось у підручник, щоб ви могли його спробувати якомога швидше!
?? Ось репо, якщо ви хочете кодувати.
Р мистецтво 1: S нитка пошук
S ТЕП 1: D eploy до Heroku
Першим кроком кожної гарної подорожі є присідання гарячого чаю та спокійне попивання. Після того, як ми це зробимо, ми можемо перейти до Heroku з веб-сайту Hasura. Це забезпечить нам усе необхідне: базу даних Postgres, наш механізм Hasura GraphQL та деякі закуски для подорожі.

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

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

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_id
s 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.

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.

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

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

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?

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, і я зроблю більше підручників! Якщо вас надихнуло додавати функції самостійно, поділіться - я хотів би про них почути :)