So erstellen Sie einen Full-Stack-Yelp-Klon mit React & GraphQL (Dune World Edition)

Ich darf mich nicht fürchten. Angst ist der Geisteskiller. Angst ist der kleine Tod, der völlige Auslöschung bringt. Ich werde meiner Angst ins Auge blicken. Ich werde zulassen, dass es über mich und durch mich geht. Und wenn es vorbei ist, werde ich das innere Auge drehen, um seinen Weg zu sehen. Wo die Angst hingegangen ist, wird nichts sein. Nur ich werde bleiben.

- "Litanei gegen Angst", Frank Herbert, Dune

Sie fragen sich vielleicht: "Was hat Angst mit einer React-App zu tun?" Zunächst einmal gibt es in einer React-App nichts zu befürchten. Tatsächlich haben wir in dieser speziellen App die Angst verboten. Ist das nicht schön

Jetzt, da Sie bereit sind, furchtlos zu sein, lassen Sie uns unsere App diskutieren. Es ist ein Mini-Yelp-Klon, bei dem Benutzer anstelle von Restaurants Planeten aus der klassischen Science-Fiction-Serie Dune überprüfen. (Warum? Weil ein neuer Dune-Film herauskommt ... aber zurück zum Hauptpunkt.)

Um unsere Full-Stack-App zu erstellen, verwenden wir Technologien, die unser Leben einfacher machen.

  1. Reaktion: Intuitives, kompositorisches Front-End-Framework, weil unser Gehirn gerne Dinge komponiert.
  2. GraphQL: Sie haben vielleicht viele Gründe gehört, warum GraphQL großartig ist. Das mit Abstand wichtigste ist die Produktivität und Zufriedenheit der Entwickler .
  3. Hasura: Richten Sie in weniger als 30 Sekunden eine automatisch generierte GraphQL-API auf einer Postgres-Datenbank ein.
  4. Heroku: Um unsere Datenbank zu hosten.

Und GraphQL macht mich glücklich, wie?

Ich sehe, du bist skeptisch. Aber Sie werden höchstwahrscheinlich vorbeikommen, sobald Sie einige Zeit mit GraphiQL (dem GraphQL-Spielplatz) verbringen.

Die Verwendung von GraphQL ist für den Front-End-Entwickler im Vergleich zu den alten Methoden klobiger REST-Endpunkte ein Kinderspiel. GraphQL bietet Ihnen einen einzigen Endpunkt, der alle Ihre Probleme abhört ... ich meine Abfragen. Es ist so ein großartiger Zuhörer, dass Sie ihm genau sagen können, was Sie wollen, und es wird es Ihnen geben, nicht weniger und nicht mehr.

Fühlen Sie sich über diese therapeutische Erfahrung aufgeregt? Lassen Sie uns in das Tutorial eintauchen, damit Sie es so schnell wie möglich ausprobieren können!

?? Hier ist das Repo, wenn Sie mitcodieren möchten .

P art 1: Suche

S TEP 1: D eploy bis Heroku

Der erste Schritt jeder guten Reise besteht darin, sich mit heißem Tee hinzusetzen und ruhig daran zu nippen. Sobald wir das getan haben, können wir Heroku von der Hasura-Website bereitstellen. Damit haben wir alles, was wir brauchen: eine Postgres-Datenbank, unsere Hasura GraphQL-Engine und einige Snacks für die Reise.

black-books.png

Schritt 2: Erstellen Sie eine Planetentabelle

Unsere Benutzer möchten Planeten überprüfen. Also erstellen wir eine Postgres-Tabelle über die Hasura-Konsole, um unsere Planetendaten zu speichern. Bemerkenswert ist der böse Planet Giedi Prime, der mit seiner unkonventionellen Küche auf sich aufmerksam gemacht hat.

Planetentabelle

In der Zwischenzeit auf der Registerkarte GraphiQL: Hasura hat unser GraphQL-Schema automatisch generiert! Hier mit dem Explorer herumspielen ??

GraphiQL Explorer

S TEP 3: C Reate app React

Wir benötigen eine Benutzeroberfläche für unsere App, erstellen also eine React-App und installieren einige Bibliotheken für GraphQL-Anforderungen, -Routing und -Stile. (Stellen Sie sicher, dass Sie zuerst Node installiert haben.)

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

Apollo Client hilft uns bei unseren GraphQL-Netzwerkanforderungen und beim Caching, damit wir all diese Grunzarbeiten vermeiden können. Wir machen auch unsere erste Abfrage und listen unsere Planeten auf! Unsere App beginnt sich zu formen.

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

Wir testen unsere GraphQL-Abfrage in der Hasura-Konsole, bevor wir sie in unseren Code kopieren.

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

Unsere Planetenliste ist nett und alles, aber sie muss mit Emotion ein wenig überarbeitet werden (siehe Repo für vollständige Styles).

Gestaltete Liste der Planeten

S tep 6: S uche Form & Zustand

Unsere Benutzer möchten nach Planeten suchen und diese nach Namen sortieren. Also fügen wir ein Suchformular hinzu, das unseren Endpunkt mit einer Suchzeichenfolge abfragt, und übergeben die Ergebnisse an Planets, um unsere Planetenliste zu aktualisieren. Wir verwenden auch React Hooks, um unseren App-Status zu verwalten.

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 stolz

Wir haben unsere Planetenliste und Suchfunktionen bereits implementiert! Wir schauen liebevoll auf unsere Handarbeit, machen ein paar Selfies zusammen und fahren mit den Bewertungen fort.

Planetenliste mit Suche

P art 2: Lebendige Rezensionen

S chritt 1: C reate Bewertungen Tisch

Unsere Benutzer werden diese Planeten besuchen und Bewertungen über ihre Erfahrungen schreiben. Über die Hasura-Konsole erstellen wir eine Tabelle für unsere Überprüfungsdaten.

Bewertungen Tabelle

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.

Fremde Schlüssel

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.

Beziehungen verfolgen

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

Planet Reviews abfragen

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;
Planetenseite mit Live-Rezensionen

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.

Wurmtanz

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.

Fügen Sie eine Review-Mutation in GraphiQL ein

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

Überprüfungsmutation mit Variablen einfügen

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-Code für nodejs-express

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

Einfügen unseres Handler-Codes in 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.

Testen Sie unsere Aktion in der Konsole

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.

Überprüfung der Geschäftslogik auf

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

Testen unserer Geschäftslogik in der Konsole

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.

Testen Sie unsere Aktion über die Benutzeroberfläche

Step 7: We did it! ?

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

Gib mir fünf

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.

Welche anderen Funktionen möchten Sie in dieser App sehen? Kontaktieren Sie mich auf Twitter und ich werde weitere Tutorials erstellen! Wenn Sie inspiriert sind, selbst Funktionen hinzuzufügen, teilen Sie diese bitte mit - ich würde gerne davon hören :)