Hogyan hozhatunk létre teljes körű Yelp klónot a React & GraphQL segítségével (Dune World Edition)

Nem szabad félnem. A félelem az elme gyilkosa. A félelem az a kis halál, amely teljes megsemmisülést hoz. Szembesülni fogok a félelmemmel. Hagyom, hogy áthaladjon rajtam és rajtam keresztül. És amikor elmúlt, megfordítom a belső szemet, hogy lássam az útját. Ahol a félelem eltűnt, nem lesz semmi. Csak én maradok.

- "Litánia a félelem ellen", Frank Herbert, Dune

Lehet, hogy kíváncsi vagy: "Mi köze a félelemnek a React alkalmazáshoz?" Először is, a React alkalmazásban nincs mitől tartani. Valójában ebben a konkrét alkalmazásban betiltottuk a félelmet. Hát nem szép?

Most, hogy készen állsz a rettentésekre, beszéljük meg az alkalmazást. Ez egy mini Yelp klón, ahol az éttermek áttekintése helyett a felhasználók a klasszikus sci-fi sorozat, a Dune bolygóit tekintik át. (Miért? Mert új Dune film jelenik meg ... de visszatérünk a fő pontra.)

A teljes kötegű alkalmazásunk felépítéséhez olyan technológiákat használunk, amelyek megkönnyítik az életünket.

  1. Reagál: Intuitív, kompozíciós front-end keret, mert agyunk szereti a dolgokat komponálni.
  2. GraphQL: Számos okot hallhatott, amiért a GraphQL fantasztikus. Messze a legfontosabb a fejlesztői termelékenység és a boldogság .
  3. Hasura: Állítson be automatikusan generált GraphQL API-t a Postgres adatbázis tetejére 30 másodperc alatt.
  4. Heroku: Adatbázisunk tárolása.

És a GraphQL hogyan nyújt boldogságot nekem?

Úgy látom, szkeptikus vagy. De nagy valószínűséggel azonnal előkerül, amint egy kis időt tölt a GraphiQL-lel (a GraphQL játszótér).

A GraphQL használata egyszerű a front-end fejlesztő számára, összehasonlítva a nehézkes REST végpontok régi módjaival. A GraphQL egyetlen végpontot ad, amely meghallgatja az összes problémáját ... mármint a lekérdezéseket. Olyan nagyszerű hallgató, hogy pontosan meg tudja mondani neki, mit akar, és megadja neked, sem kevesebbet, sem többet.

Pszichésnek érzi ezt a terápiás élményt? Merüljünk el a bemutatóban, hogy ASAP kipróbálhassa!

?? Itt van a repo, ha szeretné kódolni.

P art 1: S earch

S TEP 1: D eploy a Heroku

Minden jó út első lépése egy forró teával való leülés és nyugodt kortyolgatás. Miután ezt megtettük, a Hasura webhelyről telepíthetjük Herokuba. Ez felkészít minket mindenre, amire szükségünk van: egy Postgres adatbázissal, a Hasura GraphQL motorunkkal és néhány rágcsálnivalóval az utazáshoz.

black-books.png

2. lépés: Hozzon létre bolygótáblát

Felhasználóink ​​szeretnék áttekinteni a bolygókat. Tehát létrehozunk egy Postgres táblázatot a Hasura konzolon keresztül bolygónk adatainak tárolására. Figyelemre méltó a gonosz bolygó, a Giedi Prime, amely szokatlan konyhájával hívta fel a figyelmet.

Bolygók tábla

Eközben a GraphiQL fülön: A Hasura automatikusan létrehozta a GraphQL sémánkat! Játsszon itt az Intézővel ??

GraphiQL Explorer

S TEP 3: C reate React app

Szükségünk lesz egy felhasználói felületre az alkalmazásunkhoz, ezért létrehozunk egy React alkalmazást, és néhány könyvtárat telepítünk a GraphQL kérésekhez, útválasztáshoz és stílusokhoz. (Győződjön meg róla, hogy először telepítette a Node-ot.)

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

S TEP 4: S et fel Apolló Client

Az Apollo Client segít nekünk a GraphQL hálózati kéréseinkben és a gyorsítótárban, így elkerülhetjük ezt a morgást. Első lekérdezésünket is elvégezzük, és felsoroljuk a bolygókat! Az alkalmazásunk kezd kialakulni.

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"));

Mielőtt másoljuk-beillesztenénk a kódunkba, teszteljük a GraphQL lekérdezésünket a Hasura konzolon.

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 tep 5: S tyle list

Bolygónk listája szép és minden, de egy kis átalakításra szorul az Emotion (a teljes stílusokat lásd a repóban).

Stílusos bolygók listája

S tep 6: Keresési forma és állapot

Felhasználóink ​​bolygókat akarnak keresni és név szerint rendezni őket. Tehát hozzáadunk egy keresési űrlapot, amely lekérdezi a végpontunkat egy keresési karakterlánccal, és továbbítja az eredményeket Planetsa bolygó listánk frissítéséhez. A React Hooks segítségével is kezelhetjük az alkalmazás állapotát.

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 tep 7: B e büszke

Már megvalósítottuk bolygónk listáját és keresési funkcióit! Szeretettel tekintünk a kézimunkánkra, készítünk együtt néhány szelfit, és továbblépünk a felülvizsgálatokra.

Bolygólista kereséssel

P Art 2: L ive értékelés

S tep 1: C reate vélemény táblázat

Felhasználóink ​​meglátogatják ezeket a bolygókat, és véleményeket írnak tapasztalataikról. A Hasura konzolon keresztül létrehozunk egy táblázatot a felülvizsgálati adatokhoz.

Vélemények táblázat

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.

Idegen kulcsok

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.

Kapcsolatok követése

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

Bolygó vélemények lekérdezése

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;
Planet oldal élő véleményekkel

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.

Féregtánc

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.

Beszúrja a felülvizsgálati mutációt a GraphiQL-be

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

Beszúrja a felülvizsgálati mutációt változókkal

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.

Kazánlap kód a nodejs-expresshez

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

A kezelői kód beillesztése a Glitch-be

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

Kezelő 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.

Műveletünk tesztelése a konzolon

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.

Üzleti logika ellenőrzése

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

Üzleti logikánk tesztelése a konzolon

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.

Működésünk tesztelése a felhasználói felületen keresztül

Step 7: We did it! ?

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

Pacsi

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.

Milyen további funkciókat szeretne látni ebben az alkalmazásban? Fordulj hozzám a Twitteren, és további oktatóanyagokat készítek! Ha Ön inspirálta, hogy saját maga is hozzáadja a funkciókat, kérjük, ossza meg - szívesen hallanék róluk :)