Πώς να δημιουργήσετε έναν πλήρη κλώνο Yelp με React & GraphQL (Dune World Edition)

Δεν πρέπει να φοβάμαι. Ο ΦΟΒΟΣ ειναι ο δολοφονος του μυαλού. Ο φόβος είναι ο μικρός θάνατος που φέρνει απόλυτη εξάλειψη. Θα αντιμετωπίσω τον φόβο μου. Θα το επιτρέψω να περάσει πάνω μου και μέσα μου. Και όταν έχει περάσει, θα γυρίσω το εσωτερικό μάτι για να δω το μονοπάτι του. Όπου έχει φύγει ο φόβος δεν θα υπάρχει τίποτα. Μόνο θα παραμείνω.

- "Litany Against Fear", Frank Frank, Dune

Ίσως αναρωτιέστε, "Τι σχέση έχει ο φόβος με την εφαρμογή React;" Πρώτα απ 'όλα, δεν υπάρχει τίποτα να φοβηθείτε σε μια εφαρμογή React. Στην πραγματικότητα, σε αυτήν τη συγκεκριμένη εφαρμογή, απαγορεύσαμε τον φόβο. Δεν είναι ωραίο;

Τώρα που είστε έτοιμοι να είστε άφοβοι, ας συζητήσουμε την εφαρμογή μας. Είναι ένας μίνι κλώνος Yelp όπου, αντί να ελέγχουν τα εστιατόρια, οι χρήστες αξιολογούν πλανήτες από την κλασική σειρά επιστημονικών επιστημών, το Dune. (Γιατί; Επειδή κυκλοφορεί μια νέα ταινία Dune ... αλλά πίσω στο κύριο σημείο.)

Για να δημιουργήσουμε την εφαρμογή πλήρους στοίβας, θα χρησιμοποιήσουμε τεχνολογίες που θα κάνουν τη ζωή μας εύκολη.

  1. React: Διαισθητικό, συνθετικό πλαίσιο front-end, γιατί οι εγκέφαλοί μας θέλουν να συνθέτουν πράγματα.
  2. GraphQL: Μπορεί να έχετε ακούσει πολλούς λόγους για τους οποίους το GraphQL είναι φοβερό. Μακράν, το πιο σημαντικό είναι η παραγωγικότητα και η ευτυχία του προγραμματιστή .
  3. Hasura: Δημιουργήστε ένα GraphQL API που δημιουργείται αυτόματα πάνω από μια βάση δεδομένων Postgres σε λιγότερο από 30 δευτερόλεπτα.
  4. Heroku: Για να φιλοξενήσει τη βάση δεδομένων μας.

Και η GraphQL μου δίνει ευτυχία πώς;

Βλέπω ότι είσαι δύσπιστος. Αλλά πιθανότατα θα έρθετε αμέσως μόλις περάσετε λίγο χρόνο με το GraphiQL (την παιδική χαρά GraphQL).

Η χρήση του GraphQL είναι ένα αεράκι για τον προγραμματιστή front-end, σε σύγκριση με τους παλιούς τρόπους clunky REST endpoints Το GraphQL σάς δίνει ένα μοναδικό τελικό σημείο που ακούει όλα τα προβλήματά σας ... Εννοώ ερωτήματα. Είναι τόσο υπέροχος ακροατής που μπορείτε να του πείτε ακριβώς τι θέλετε και θα σας δώσει, τίποτα λιγότερο και τίποτα περισσότερο.

Νιώθετε ψυχικά για αυτήν τη θεραπευτική εμπειρία; Ας ρίξουμε μια ματιά στο σεμινάριο για να το δοκιμάσετε το συντομότερο δυνατόν!

?? Εδώ είναι το repo εάν θέλετε να κωδικοποιήσετε μαζί.

P art 1: S αυτί

S tep 1: D eploy να Heroku

Το πρώτο βήμα κάθε καλής διαδρομής είναι να καθίσετε με λίγο ζεστό τσάι και να το πιείτε ήρεμα. Μόλις το κάνουμε αυτό, μπορούμε να αναπτύξουμε στο Heroku από τον ιστότοπο Hasura. Αυτό θα μας προετοιμάσει με όλα όσα χρειαζόμαστε: μια βάση δεδομένων Postgres, τον κινητήρα Hasura GraphQL και μερικά σνακ για το ταξίδι.

black-books.png

Βήμα 2: Δημιουργία πίνακα πλανητών

Οι χρήστες μας θέλουν να ελέγξουν τους πλανήτες. Έτσι δημιουργούμε έναν πίνακα Postgres μέσω της κονσόλας Hasura για να αποθηκεύουμε τα δεδομένα του πλανήτη μας. Αξιοσημείωτο είναι ο κακός πλανήτης, ο Giedi Prime, ο οποίος εφιστά την προσοχή με τη μη συμβατική του κουζίνα.

Πίνακες πλανητών

Εν τω μεταξύ, στην καρτέλα GraphiQL: Η Hasura δημιούργησε αυτόματα το σχήμα GraphQL! Παίξτε με τον Explorer εδώ;

Εξερεύνηση GraphiQL

S tep 3: C reate React app

Χρειαζόμαστε μια διεπαφή χρήστη για την εφαρμογή μας, επομένως δημιουργούμε μια εφαρμογή React και εγκαθιστούμε ορισμένες βιβλιοθήκες για αιτήματα, δρομολόγηση και στυλ GraphQL. (Βεβαιωθείτε ότι έχετε εγκαταστήσει πρώτα τον κόμβο.)

> 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 επάνω Απόλλωνα πελάτη

Το Apollo Client θα μας βοηθήσει με τα αιτήματα και την προσωρινή αποθήκευση του δικτύου 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 tep 5: Λίστα λίστας

Η λίστα των πλανητών μας είναι ωραία και όλα, αλλά χρειάζεται λίγη ανανέωση με το Emotion (δείτε το repo για πλήρη στυλ)

Στυλ λίστα πλανητών

S tep 6: S & earch form & state

Οι χρήστες μας θέλουν να αναζητήσουν πλανήτες και να τους παραγγείλουν ονομαστικά. Προσθέτουμε λοιπόν μια φόρμα αναζήτησης που υποβάλλει ερωτήματα στο τελικό μας σημείο με μια συμβολοσειρά αναζήτησης και μεταδίδουμε τα αποτελέσματα για 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 tep 7: B e υπερήφανος

Έχουμε ήδη εφαρμόσει τη λίστα πλανητών και τις δυνατότητες αναζήτησης! Κοιτάζουμε με αγάπη την χειροτεχνία μας, παίρνουμε μερικές selfies μαζί και προχωρούμε σε κριτικές.

Λίστα πλανητών με αναζήτηση

P art 2: Lveve σχόλια

S tep 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_ids 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.

Εισαγωγή μετάλλαξης κριτικής σε GraphiQL

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.

Κωδικός Boilerplate για nodejs-express

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

Επικολλήστε τον κωδικό χειριστή μας στο 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 χειριστή

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?

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.

Ποιες άλλες δυνατότητες θα θέλατε να δείτε σε αυτήν την εφαρμογή; Επικοινωνήστε μαζί μου στο Twitter και θα κάνω περισσότερα μαθήματα! Εάν είστε εμπνευσμένοι να προσθέσετε μόνοι σας χαρακτηριστικά, μοιραστείτε το - Θα ήθελα πολύ να μάθω για αυτά :)