Kako stvoriti cjeloviti klon Yelp s React & GraphQL (izdanje Dune World)

Ne smijem se bojati. Strah je um ubica. Strah je mala smrt koja donosi potpuno uništavanje. Suočit ću se sa svojim strahom. Dopustit ću da pređe preko mene i kroz mene. A kad prođe, okrenut ću unutarnje oko da vidim njegov put. Tamo gdje je strah nestao, neće biti ništa. Ostat ću samo ja.

- "Litanije protiv straha", Frank Herbert, Dune

Možda se pitate: "Kakve veze strah ima s React aplikacijom?" Prije svega, u aplikaciji React nema čega se bojati. Zapravo, u ovoj određenoj aplikaciji zabranili smo strah. Nije li to lijepo?

Sad kad ste spremni biti neustrašivi, razgovarajmo o našoj aplikaciji. To je mini Yelp klon u kojem korisnici, umjesto da recenziraju restorane, pregledavaju planete iz klasične znanstveno-fantastične serije Dune. (Zašto? Jer izlazi novi film Dune ... ali natrag na glavnu poantu.)

Da bismo izgradili našu aplikaciju s punim hrpom, koristit ćemo tehnologije koje nam olakšavaju život.

  1. Reagirajte: Intuitivni, kompozicijski front-end okvir, jer naš mozak voli sastavljati stvari.
  2. GraphQL: Možda ste čuli mnogo razloga zašto je GraphQL nevjerojatan. Daleko najvažnija je produktivnost i sreća programera .
  3. Hasura: Postavite automatski generirani GraphQL API na vrh Postgres baze podataka u manje od 30 sekundi.
  4. Heroku: Da hostiramo našu bazu podataka.

A GraphQL mi daje sreću kako?

Vidim da ste skeptični. No, najvjerojatnije ćete se pojaviti čim provedete neko vrijeme s GraphiQL-om (igralište GraphQL).

Korištenje GraphQL-a povjetarac je za front-end programera u usporedbi sa starim načinima nezgrapnih REST krajnjih točaka. GraphQL vam daje jednu krajnju točku koja sluša sve vaše probleme ... Mislim na upite. Toliko je sjajan slušatelj da mu možete reći točno ono što želite i dat će vam ga, ni manje ni više.

Osjećate li se psihički zbog ovog terapijskog iskustva? Zaronimo u tutorial kako biste ga što prije isprobali!

?? Evo repo-a ako želite kodirati.

P art 1: Istraživanje

S TEP 1: D eploy do Heroku

Prvi korak svakog dobrog putovanja je sjedenje uz malo vrućeg čaja i mirno pijuckanje. Nakon što to učinimo, možemo se rasporediti na Heroku s web mjesta Hasura. Ovo će nam postaviti sve što nam treba: baza podataka Postgres, naš Hasura GraphQL motor i nekoliko grickalica za putovanje.

crne knjige.png

Korak 2: Stvorite tablicu planeta

Naši korisnici žele pregledati planete. Tako kreiramo Postgres tablicu putem konzole Hasura za pohranu podataka o našem planetu. Valja istaknuti zli planet, Giedi Prime, koji skreće pozornost svojom nekonvencionalnom kuhinjom.

Stol planeta

U međuvremenu, na kartici GraphiQL: Hasura je automatski generirao našu GraphQL shemu! Poigrajte se s Explorerom ovdje ??

GraphiQL Explorer

S TEP 3: C reate reagiraju aplikaciju

Trebat će nam korisničko sučelje za našu aplikaciju, pa kreiramo aplikaciju React i instaliramo neke knjižnice za GraphQL zahtjeve, usmjeravanje i stilove. (Prvo provjerite je li instaliran Node.)

> 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 i do Apollo klijent

Apollo Client će nam pomoći s našim GraphQL mrežnim zahtjevima i predmemoriranjem, tako da možemo izbjeći sav taj gunđajući posao. Također postavljamo prvi upit i popisujemo naše planete! Naša se aplikacija počinje oblikovati.

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

Testiramo naš GraphQL upit u konzoli Hasura prije kopiranja u naš kôd.

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 popis

Popis naših planeta je lijep i sve, ali treba malo preobraziti s Emotionom (pogledajte repo za cjelovite stilove).

Stilizirani popis planeta

S TEP 6: S earch kristala oblika stanje

Naši korisnici žele pretraživati ​​planete i poredati ih po imenima. Stoga dodajemo obrazac za pretraživanje koji upitom traži krajnju točku pomoću niza za pretraživanje i prosljeđujemo rezultate Planetsna ažuriranje popisa planeta. Također koristimo React Hooks za upravljanje stanjem naše aplikacije.

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 ponosni

Već smo implementirali naš popis planeta i značajke pretraživanja! S ljubavlju promatramo svoj ručni rad, napravimo nekoliko selfija i prelazimo na recenzije.

Popis planeta s pretraživanjem

Č lanak 2: Životne kritike

S TEP 1: C reate mišljenja tablica

Naši će korisnici posjetiti ove planete i pisati recenzije o svom iskustvu. Preko Hasura konzole kreiramo tablicu za naše podatke o pregledima.

Tablica s recenzijama

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.

Strani ključevi

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.

Praćenje odnosa

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

Upit za recenzije planeta

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;
Stranica planeta s recenzijama uživo

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.

Crv ples

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.

Umetnite mutaciju pregleda u GraphiQL

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

Umetnite mutaciju pregleda s varijablama

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.

Kôd tablice za nodejs-express

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

Lijepljenje koda za obradu u 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 obrađivača

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.

Testiranje naše akcije u konzoli

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.

Provjera poslovne logike za

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

Testiranje naše poslovne logike u konzoli

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.

Testiranje naše akcije putem korisničkog sučelja

Step 7: We did it! ?

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

Daj pet

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.

Koje biste još značajke željeli vidjeti u ovoj aplikaciji? Obratite mi se na Twitteru i napravit ću još vodiča! Ako ste nadahnuti da sami dodate značajke, podijelite - volio bih čuti o njima :)