Sådan oprettes en Yelp-klon med fuld stak med React & GraphQL (Dune World Edition)

Jeg må ikke frygte. Frygt er mind-killer. Frygt er den lille død, der bringer total udslettelse. Jeg vil møde min frygt. Jeg vil lade det passere over mig og igennem mig. Og når det er gået forbi, vil jeg vende det indre øje for at se dets vej. Hvor frygten er gået, vil der ikke være noget. Kun jeg vil forblive.

- "Litany Against Fear", Frank Herbert, Dune

Du undrer dig måske: "Hvad har frygt at gøre med en React-app?" Først og fremmest er der intet at frygte i en React-app. Faktisk i denne særlige app forbød vi frygt. Er det ikke rart?

Nu hvor du er klar til at være frygtløs, lad os diskutere vores app. Det er en mini Yelp-klon, hvor brugere i stedet for at gennemgå restauranter gennemgår planeter fra den klassiske sci-fi-serie, Dune. (Hvorfor? Fordi der kommer en ny Dune-film ... men tilbage til hovedpunktet.)

For at opbygge vores full-stack app bruger vi teknologier, der gør vores liv lette.

  1. Reager: Intuitiv, kompositionel front-end-ramme, fordi vores hjerner kan lide at komponere ting.
  2. GraphQL: Du har måske hørt mange grunde til, at GraphQL er fantastisk. Langt det vigtigste er udviklerens produktivitet og lykke .
  3. Hasura: Opret en automatisk genereret GraphQL API oven på en Postgres-database på under 30 sekunder.
  4. Heroku: At være vært for vores database.

Og GraphQL giver mig lykke hvordan?

Jeg kan se, at du er en skeptisk. Men du kommer sandsynligvis rundt, så snart du bruger lidt tid på GraphiQL (GraphQL-legepladsen).

Brug af GraphQL er en leg for front-end-udvikleren sammenlignet med de gamle måder med klodsede REST-endepunkter. GraphQL giver dig et enkelt slutpunkt, der lytter til alle dine problemer ... Jeg mener forespørgsler. Det er en så god lytter, at du kan fortælle det nøjagtigt, hvad du vil, og det vil give dig det, intet mindre og intet mere.

Føler du dig over denne terapeutiske oplevelse? Lad os dykke ned i vejledningen, så du kan prøve det ASAP!

?? Her er repoen, hvis du vil kode sammen.

P art 1: S earch

S RIN 1: D eploy til Heroku

Det første skridt på enhver god rejse er at sidde ned med lidt varm te og nippe til den roligt. Når vi har gjort det, kan vi implementere til Heroku fra Hasura-webstedet. Dette vil give os alt, hvad vi har brug for: en Postgres-database, vores Hasura GraphQL-motor og nogle snacks til rejsen.

black-books.png

Trin 2: Opret planetabellen

Vores brugere vil gennemgå planeter. Så vi opretter en Postgres-tabel via Hasura-konsollen til at gemme vores planetdata. Vær opmærksom på den onde planet, Giedi Prime, der har trukket opmærksomhed med sit ukonventionelle køkken.

Planeter bord

I mellemtiden i fanen GraphiQL: Hasura har automatisk genereret vores GraphQL-skema! Spil rundt med Explorer her ??

GraphiQL Explorer

S RIN 3: C reate React app

Vi har brug for et brugergrænseflade til vores app, så vi opretter en React-app og installerer nogle biblioteker til GraphQL-anmodninger, routing og stilarter. (Sørg for, at du først har installeret Node.)

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

S RIN 4: S et op Apollo Client

Apollo Client hjælper os med vores GraphQL-netværksanmodninger og caching, så vi kan undgå alt det grunt arbejde. Vi laver også vores første forespørgsel og viser vores planeter! Vores app begynder at forme sig.

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

Vi tester vores GraphQL-forespørgsel i Hasura-konsollen, inden vi kopierer den ind i vores kode.

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-liste

Vores planetliste er pæn og alt, men det har brug for en lille makeover med Emotion (se repo for fulde stilarter).

Stylet liste over planeter

S RIN 6: S earch formular & tilstand

Vores brugere ønsker at søge efter planeter og bestille dem ved navn. Så vi tilføjer en søgeformular, der spørger vores slutpunkt med en søgestreng, og videresender resultaterne til for Planetsat opdatere vores planetliste. Vi bruger også React Hooks til at styre vores apptilstand.

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 RIN 7: B e stolt

Vi har allerede implementeret vores planetliste og søgefunktioner! Vi ser kærligt på vores håndværk, tager et par selfies sammen og går videre til anmeldelser.

Planetliste med søgning

P art 2: Live anmeldelser

S tep 1: Tabellen til reaktionernes anmeldelser

Vores brugere vil besøge disse planeter og skrive anmeldelser om deres oplevelse. Vi opretter en tabel via Hasura-konsollen til vores gennemgangsdata.

Tabel med anmeldelser

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.

Udenlandske nøgler

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.

Sporing af forhold

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

Forespørgsel på planetanmeldelser

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-side med live anmeldelser

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.

Ormedans

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.

Indsæt gennemgangsmutation i GraphiQL

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

Indsæt gennemgangsmutation med variabler

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.

Kedelpladekode til nodejs-express

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

Indsætter vores handler-kode i 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.

Handler 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.

Test af vores handling i konsollen

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.

Forretningslogik kontrol for

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

Test af vores forretningslogik i konsollen

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.

Test af vores handling via brugergrænsefladen

Step 7: We did it! ?

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

Giv mig fem

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.

Hvilke andre funktioner vil du se i denne app? Kontakt mig på Twitter, så laver jeg flere selvstudier! Hvis du er inspireret til selv at tilføje funktioner, skal du dele - jeg vil meget gerne høre om dem :)