Kā izveidot pilnas kaudzes Yelp klonu ar React & GraphQL (Dune World Edition)

Es nedrīkstu baidīties. Bailes ir prāta slepkava. Bailes ir mazā nāve, kas rada pilnīgu iznīcību. Es stāšos pretī savām bailēm. Es atļaušos tam iet pāri man un caur mani. Un, kad tas būs pagājis garām, es pagriezīšu iekšējo aci, lai redzētu tās ceļu. Tur, kur bailes ir pazudušas, nekas nebūs. Palikšu tikai es.

- "Litānija pret bailēm", Frenks Herberts, Dune

Jums var rasties jautājums: "Kāds ir bailēm sakars ar lietotni React?" Pirmkārt, lietotnē React nav ko baidīties. Faktiski šajā konkrētajā lietotnē mēs aizliedza bailes. Vai tas nav jauki?

Tagad, kad esat gatavs būt bezbailīgs, apspriedīsim mūsu lietotni. Tas ir mini Yelp klons, kurā restorānu apskates vietā lietotāji pārskata planētas no klasiskās zinātniskās fantastikas sērijas Dune. (Kāpēc? Tāpēc, ka iznāk jauna kāpu filma ... bet atgriezīsimies pie galvenā.)

Lai izveidotu pilnas kaudzes lietotni, mēs izmantosim tehnoloģijas, kas atvieglo mūsu dzīvi.

  1. Reaģēt: Intuitīvs, kompozicionāls front-end ietvars, jo mūsu smadzenēm patīk sacerēt lietas.
  2. GraphQL: iespējams, esat dzirdējis daudz iemeslu, kāpēc GraphQL ir lielisks. Līdz šim vissvarīgākais ir izstrādātāju produktivitāte un laime .
  3. Hasura: izveidojiet automātiski ģenerētu GraphQL API Postgres datu bāzes augšpusē mazāk nekā 30 sekundēs.
  4. Heroku: mūsu datu bāzes mitināšanai.

Un kā GraphQL man sagādā laimi?

Es redzu, ka jūs esat skeptisks. Bet jūs, visticamāk, ieradīsities, tiklīdz pavadīsit kādu laiku kopā ar GraphiQL (GraphQL rotaļu laukumu).

Izmantojot GraphQL, front-end izstrādātājam ir brīze, salīdzinot ar vecajiem veidiem, kā REST galapunkti ir neveikli. GraphQL sniedz jums vienu gala punktu, kas uzklausa visas jūsu nepatikšanas ... es domāju vaicājumus. Tas ir tik lielisks klausītājs, ka jūs varat pateikt tieši to, ko vēlaties, un tas jums to dos, ne mazāk, ne vairāk.

Vai jūtaties psihiski par šo terapeitisko pieredzi? Ienirsim apmācībā, lai jūs to varētu izmēģināt pēc iespējas ātrāk!

?? Lūk, repo, ja vēlaties kodēt.

P art 1: S earch

S TEP 1: D eploy līdz Heroku

Katra laba ceļojuma pirmais solis ir apsēsties ar karstu tēju un mierīgi to iemalkot. Kad tas būs izdarīts, varēsim izvietot Heroku no Hasura vietnes. Tas mūs sagatavos ar visu nepieciešamo: Postgres datu bāzi, mūsu Hasura GraphQL dzinēju un dažām uzkodām ceļojumam.

black-books.png

2. solis: izveidojiet planētu tabulu

Mūsu lietotāji vēlas pārskatīt planētas. Tāpēc, izmantojot Hasura konsoli, mēs izveidojam Postgres tabulu, lai saglabātu mūsu planētas datus. Jāatzīmē ļaunā planēta Giedi Prime, kas pievērsa uzmanību ar savu netradicionālo virtuvi.

Planētu galds

Tikmēr cilnē GraphiQL: Hasura ir automātiski izveidojusi mūsu GraphQL shēmu! Spēlēties ar Explorer šeit?

GraphiQL Explorer

S TEP 3: C reate React lietotni

Mums būs nepieciešama lietotāja saskarne, tāpēc mēs izveidojam lietotni React un instalējam dažas bibliotēkas GraphQL pieprasījumiem, maršrutēšanai un stiliem. (Pārliecinieties, vai vispirms esat instalējis 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 et up Apollo Klienta

Apollo klients mums palīdzēs izpildīt GraphQL tīkla pieprasījumus un saglabāt kešatmiņu, lai mēs varētu izvairīties no visa šī rūcošā darba. Mēs arī veicam pirmo vaicājumu un uzskaitām mūsu planētas! Mūsu lietotne sāk veidoties.

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

Mēs pārbaudām GraphQL vaicājumu Hasura konsolē, pirms to kopējam un ielīmējam savā kodā.

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 saraksts

Mūsu planētu saraksts ir jauks un viss, taču tam ir nepieciešama neliela pārvērtības ar Emotion (pilnu stilu skatīt repo).

Stilēts planētu saraksts

S TEP 6: S earch formu un state

Mūsu lietotāji vēlas meklēt planētas un pasūtīt tās pēc nosaukuma. Tāpēc mēs pievienojam meklēšanas formu, kas pieprasa mūsu gala punktu ar meklēšanas virkni, un nododam rezultātus, Planetslai atjauninātu mūsu planētu sarakstu. Lai pārvaldītu lietotnes stāvokli, mēs arī izmantojam 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 TEP 7: B e lepns

Mēs jau esam ieviesuši savu planētu sarakstu un meklēšanas funkcijas! Mēs ar mīlestību skatāmies uz savu roku darbu, kopīgi uzņemam dažus pašbildes un pārejam pie atsauksmēm.

Planētu saraksts ar meklēšanu

P art 2: L IVE atsauksmes

S TEP 1: C reate viedokļi tabula

Mūsu lietotāji apmeklēs šīs planētas un rakstīs pārskatus par savu pieredzi. Caur pārskatīšanas datiem mēs izveidojam tabulu, izmantojot Hasura konsoli.

Atsauksmju tabula

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.

Ārzemju atslēgas

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.

Attiecību izsekošana

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

Pieprasot planētas pārskatus

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;
Planētas lapa ar reālām atsauksmēm

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.

Tārpu deja

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.

Ievietojiet pārskata mutāciju GraphiQL

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

Ievietot pārskata mutāciju ar mainīgajiem

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 katla kods

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

Līmējot mūsu apstrādātāja kodu 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.

Apstrādātāja 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.

Pārbaudām mūsu darbību konsolē

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.

Biznesa loģikas pārbaude

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

Pārbaudām mūsu biznesa loģiku konsolē

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ūsu darbības pārbaude, izmantojot lietotāja interfeisu

Step 7: We did it! ?

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

Dod pieci

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.

Kādas citas funkcijas jūs vēlētos redzēt šajā lietotnē? Sazinieties ar mani Twitter vietnē, un es izveidošu vairāk apmācību! Ja jūs esat iedvesmojies pats pievienot funkcijas, lūdzu, dalieties - es labprāt dzirdētu par tām :)