Cómo crear un clon de Yelp completo con React & GraphQL (Dune World Edition)

No debo temer. El miedo es el asesino de la mente. El miedo es la pequeña muerte que conduce a la destrucción total. Me enfrentaré a mi miedo. Permitiré que pase sobre mí y a través de mí. Y cuando haya pasado, giraré el ojo interior para ver su camino. Donde el miedo se ha ido no habrá nada. Solo yo me quedaré.

- "Letanía contra el miedo", Frank Herbert, Dune

Quizás se pregunte, "¿Qué tiene que ver el miedo con una aplicación React?" En primer lugar, no hay nada que temer en una aplicación React. De hecho, en esta aplicación en particular, prohibimos el miedo. ¿No es agradable?

Ahora que está listo para ser valiente, hablemos de nuestra aplicación. Es un mini clon de Yelp donde en lugar de revisar restaurantes, los usuarios revisan planetas de la clásica serie de ciencia ficción, Dune. (¿Por qué? Porque está saliendo una nueva película de Dune ... pero volviendo al punto principal).

Para crear nuestra aplicación de pila completa, usaremos tecnologías que nos facilitan la vida.

  1. Reaccionar: marco de interfaz intuitivo y compositivo, porque a nuestro cerebro le gusta componer cosas.
  2. GraphQL: es posible que haya escuchado muchas razones por las que GraphQL es increíble. De lejos, el más importante es la productividad y la felicidad de los desarrolladores .
  3. Hasura: configure una API GraphQL generada automáticamente sobre una base de datos de Postgres en menos de 30 segundos.
  4. Heroku: Para alojar nuestra base de datos.

Y GraphQL me da felicidad, ¿cómo?

Veo que eres escéptico. Pero lo más probable es que se recupere tan pronto como pase algún tiempo con GraphiQL (el campo de juego de GraphQL).

El uso de GraphQL es muy sencillo para el desarrollador de aplicaciones para el usuario, en comparación con las viejas formas de terminales REST torpes. GraphQL le ofrece un único punto final que escucha todos sus problemas ... me refiero a consultas. Es un gran oyente que puedes decirle exactamente lo que quieres y te lo dará, nada menos y nada más.

¿Te sientes emocionado por esta experiencia terapéutica? ¡Vamos a sumergirnos en el tutorial para que puedas probarlo lo antes posible!

?? Aquí está el repositorio si desea codificar.

P art 1: BÚSQUEDA

S tep 1: D eploy a Heroku

El primer paso de todo buen viaje es sentarse con un té caliente y beberlo con calma. Una vez que lo hayamos hecho, podemos implementarlo en Heroku desde el sitio web de Hasura. Esto nos preparará con todo lo que necesitamos: una base de datos de Postgres, nuestro motor Hasura GraphQL y algunos bocadillos para el viaje.

libros-negros.png

Paso 2: crea una tabla de planetas

Nuestros usuarios quieren revisar planetas. Entonces creamos una tabla de Postgres a través de la consola de Hasura para almacenar los datos de nuestro planeta. Es de destacar el planeta malvado, Giedi Prime, que ha llamado la atención con su cocina poco convencional.

Mesa de planetas

Mientras tanto, en la pestaña GraphiQL: ¡Hasura ha generado automáticamente nuestro esquema GraphQL! ¿Jugar con el Explorer aquí?

Explorador GraphiQL

S tep 3: C reate Reaccionar aplicación

Necesitaremos una interfaz de usuario para nuestra aplicación, por lo que creamos una aplicación React e instalamos algunas bibliotecas para solicitudes, enrutamiento y estilos de GraphQL. (Asegúrese de tener instalado Node primero).

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

Apollo Client nos ayudará con nuestras solicitudes de red GraphQL y el almacenamiento en caché, para que podamos evitar todo ese trabajo pesado. ¡También hacemos nuestra primera consulta y enumeramos nuestros planetas! Nuestra aplicación está empezando a mejorar.

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

Probamos nuestra consulta GraphQL en la consola de Hasura antes de copiarla y pegarla en nuestro código.

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 lista de Tyle

Nuestra lista de planetas es buena y todo, pero necesita un pequeño cambio de imagen con Emotion (vea el repositorio para estilos completos).

Lista de planetas con estilo

S tep 6: S forma earch y Estado

Nuestros usuarios quieren buscar planetas y ordenarlos por nombre. Entonces agregamos un formulario de búsqueda que consulta nuestro punto final con una cadena de búsqueda y pasamos los resultados a Planetspara actualizar nuestra lista de planetas. También usamos React Hooks para administrar el estado de nuestra aplicación.

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 orgullosos

¡Ya hemos implementado nuestra lista de planetas y funciones de búsqueda! Contemplamos con amor nuestro trabajo, nos tomamos algunas selfies juntos y pasamos a las reseñas.

Lista de planetas con búsqueda

P art 2: Revisiones en vivo

S tep 1: C mesa de comentarios rear

Nuestros usuarios visitarán estos planetas y escribirán reseñas sobre su experiencia. Creamos una tabla a través de la consola Hasura para nuestros datos de revisión.

Tabla de reseñas

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.

Llaves extranjeras

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.

Seguimiento de relaciones

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

Consultando reseñas de planetas

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;
Página del planeta con reseñas en vivo

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.

Danza del gusano

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.

Insertar mutación de revisión en GraphiQL

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

Insertar mutación de revisión con variables

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.

Código repetitivo para nodejs-express

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

Pegando nuestro código de controlador en 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 del controlador

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.

Probando nuestra acción en la consola

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.

Comprobación de lógica empresarial para

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

Probando nuestra lógica empresarial en la consola

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.

Probando nuestra acción a través de la interfaz de usuario

Step 7: We did it! ?

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

Cinco altos

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.

¿Qué otras características le gustaría ver en esta aplicación? Comuníquese conmigo en Twitter y haré más tutoriales. Si está inspirado para agregar funciones usted mismo, por favor comparta, me encantaría escucharlas :)