DEV Community

Cover image for Learning GraphQL and React: Using custom queries and remote types
Abu Sakib
Abu Sakib

Posted on

Learning GraphQL and React: Using custom queries and remote types

In my previous article I tried to create an application that interacts with an existing GraphQL API to fetch some film data based on user query.

Let's see if I can extend the app's functionality a little so that it does some more stuff for us, by being more than just a bare bones "search and see" tool.

What I'd like to add is a system that would enable users to write reviews for movies and rate them. That means I'd need a backend to save those user data. That can easily be done with Slash GraphQL which gives us a backend with a /graphql endpoint. I'll show how one can be deployed shortly. There's a free tier available so you can just go here, sign up, upload a schema that accurately represents your API and you'd have backend ready to be used.

So here's how the app should behave after I'm done with the new implementations:

  • Just like before, we can search by a word or term for a movie, the results would appear in a table with all the films and their directors
  • Clicking on a movie that we'd like write a review about would take us to someplace where we can type in a username, give that movie a rating, write our review and hit that satisfying submit button...
  • Our submission details would get added to the database. There would be a separate page/route where we can see all the review details.

Alright then, let's start writing some code!

The Schema

It's obvious that I need to add (and store) some information to a database considering the second point above. In GraphQL terms, that's called mutation. A mutation would be run each time a user hits that submit button and the details would get written to our database.

Secondly, since I'm hoping that the app would nicely show all the information that are currently in the database, I need to "fetch" (no not the fetch API!) them. You can smell it right? Yeah, I'm talking about sending "queries" to our database...

So I need a schema to define exactly what "types" of information would constitute my backend. One of the coolest things about Slash GraphQL is that all I need to do in order to have a working API, is to do just that: creating a schema. The rest is taken care of automatically; I'd have a fully-working GraphQL service that can accept queries, mutations and all that stuff.

Here's the schema:

type User {
    username: String! @id
    posted_reviews: [Review] @hasInverse(field: posted_by)
}

type Review {
    id: ID!
    text: String!
    rating: Int!
    posted_by: User!
    reviewed_film: FilmData @hasInverse(field: reviews)
}

type Film @remote {
    id: ID!
    name: String!
    directed_by: [Director!]!
}

type FilmData {
    id: String! @id
    reviews: [Review]
    data: Film @custom(
        http: {
            url: "https://play.dgraph.io/graphql"
            method: "POST"
            forwardHeaders: ["Content-Type"]
            graphql: "query($id: ID!) { getFilm(id: $id) }"
            skipIntrospection: true
        }
    )
}

type Director @remote {
    name: String!
    id: ID!
}

type Query {
    getMovieNames(name: String): [Film] @custom(
        http: {
            url: "https://play.dgraph.io/graphql"
            method: "POST"
            forwardHeaders: ["Content-Type"]
            graphql: "query($name: String!) { queryFilm(filter: {name: {alloftext: $name}}) }"
            skipIntrospection: true
        }
    )
}   
Enter fullscreen mode Exit fullscreen mode

Let's break it down by each type:

User type

The User type is for us users. The fields inside the user type (or object) defines the properties/attributes of that object. In this case, each user would have a username and some reviews that he/she's written about films.

The username is a String type which is a built-in scalar type of the GraphQL query language; beside String you have Int for integers, float for floating-point values and so on. It's obvious that they're pretty much the same thing as the primitive data types various programming language offer. Each type ultimately represents actual valid data so that makes sense.

The exclamation mark indicates that the field is non-nullable, which means that the API would always give a value when I query for a user's username.

@id is called a directive that says that each username is going to be unique and hence will be used as an ID of that user.

The posted_reivews field is an array of Review types (which I'll discuss next): this field signifies the fact that a user has written some reviews that is accessible by querying for this field.

@hasInverse is another directive establishes connection between a review and the posted_by field of the Review type, in both directions. What this means is that I'm associating a review with the the user who wrote it. Since it establishes a bi-directional edge between two nodes, I can also get from a review to the person who wrote it. This is neat; remember that a GraphQL API can give you quite the flexibility on how you set up your data and able to interact with them. This directive is a neat proof of that.

It isn't a native GraphQL thing though, but rather provided by Dgraph. You can look at the other directives that Dgraph supports here.

Review type

This type represents a user's reviews. So what fields does it contain?

  • The id field that just attaches a unique identifier (the ID is another default scalar type of GraphQL) to each review
  • The text field is the textual content of the review, which is of course a String
  • Rating represents the rating given to a film by a user (my app would employ a 5-star rating system), which would be an integer
  • posted_by field, as I told before, is for associating a review with a user. We're representing users under the User type right? So that's the value of this field
  • Lastly, reviewed_film represents which film the review is about. I'm associating it with the reviews field of the FilmData type. This would become clearer when I talk about that field, but basically doing this would enable me to get info about the reviewed film, like its name and director.

Now the the juicy stuff begins. Notice that I need to work with two kinds of dataset here corresponding to two GraphQL APIs: one that is "remote", i.e. the information that I'd get from the remote server (https://play.dgraph.io/graphql), and the other that's going to reside in the app's own database. My app is using remote data for processing. We need to establish connection between that and what the users would supply (the usernames, ratings and reviews), since after processing I'm storing the final result in our backend by running mutations; I'd also need the ability to run useful queries. So I'm talking about a kind of "combination" of data, part of which comes from "outside" the app, part of which is the result of user interaction with that outside data.

Let's discuss about the next types and discuss how they're going to play the key role in this scenario

Film type

This is a remote type, indicated by the @remote directive, meaning that this field represents data that comes from somewhere else, not the native API this schema is belongs to. You guessed it right, this type is for holding the data fetched from the remote Dgraph server. We have to write our own resolver for this type, since it's a remote one.

The fields are pretty obvious; name is for the film name, and id is an associated unique ID. Notice the field directed_by has the value [Director!]!. The outer exclamation mark means the same thing: the field is non-nullable, i.e. I can always expect an array of Director objects, with zero or more items. The Director! being also non-nullable, ensures that each item of this array is going to be a Director object. It being a remote type, Director is also going to be the of the same type.

FilmData type

This is the type inside whicn I'm going to be establishing a connection between our local data and the remote one. Notice that this doesn't have any @remote attached, so this would get stored in our Dgraph backend.

First I have the id field which is a String and also works as a unique identifier.

Then there's the reviews field that we saw in the previously discussed Review type where I established a two-way edge between this and the reviewed_film node. This would enable me to do a query like the following:

queryReview {
    reviewed_film {
      id
      data {
        name
      }
      reviews {
        posted_by {
          username
        }
        id
        rating
        text
      }
    }
}
Enter fullscreen mode Exit fullscreen mode

So I'd be able to get all reviews of each film in our database.
In fact, this would be the exact query that I use later to implement a route where the app would show all the reviews arranged by films.

Since a film might have multiple reviews by multiple users, here I've defined an array of Review objects as the value.

The data field is the "custom" field, where we write our resolver for the remote Film type, making a connection between the remote data and local. The syntax is quite understandable; an http POST request would send a graphql call to the the remote https://play.dgraph.io/graphql by id (which I'm going to supply from within the app based on what film the user selected, as we'll see soon). The result would be a JSON response object with data matching the fields of the Film type. As you can see from the above query structure, I can access that through this custom data field. Hence I've effectively established my desired connection; basically I now have a node that holds a copy of my remote data so I can traverse it for meaningful queries.

Director type

This, as I mentioned, is also a remote type and part of Film that represents the director's name and ID.

Query type

This is the type responsible for managing the search functionality of the app. Let's go over that again a bit more:

  • We would type in a word or term, which is just a String, and a query should be fired towards the remote server, fetching all the film's whose names contain our search term.
  • The response would consist of the film names and their directors' names. I also need to get the IDs of those films since I need that for the custom data field of FilmData.

I give the query a name, getMovieNames (this is the name I'd use inside our app to fire the query, with variables that would hold the user's search term, just like we saw in the first version of the app), which has an argument called name, which is a String, corresponding to the search term . We've already seen the remote Film type that contains fields that would suit our needs for the response we're hoping to get. So that's what I use here; we might get multiple results, which means I have to use an array of Film objects, and hence I use [Film]. In the graphql field of the HTTP request object, I pass in the search term using the variable name and define the custom query.

Deploying a backend

With the schema ready, it just needs to be uploaded to Slash GraphQL to get a production-ready service up and running.

First we need to head over to https://slash.dgraph.io. There'll be a a log in/sign up page.

Slash GraphQL signup/signin page

After registering, we're presented with the following:

Slash GraphQL after signup

Just click on the Launch a New Backend button.

Create a new Slash GraphQL backend

As you can see there's a free tier available. Just give your backend a name and click on Launch.

Slash GraphQL backend deployed

Soon you'll have a live backend ready to be used. Note down your endpoint (which as you can see is given a randomly unique name; I'm particularly feeling good about this one...) since that's where the app would be making all the requests.

You can later access it though from the Overview section of your sidebar on the top-left, along with some other statistics about your service.

Now to upload the schema, click on Create your Schema.

Upload schema

Paste it inside the area and hit Deploy. That's it, you're done setting up our backend. You can now calmly just focus on building your application.

In case you want to feast your eyes on all the goodies Slash auto-generated from the schema to serve all your needs, you can download the generated schema, by clicking on the Schema section of the sidebar, as shown below:

Download generated schema from Slash

The UI

The UI needs to be customized to account for the new functionalities. There are going to be two new components:

  • AddReviews
  • ShowReviews

The first one is where we can submit our review details and the second one is where the app will show all of the reviews. These are going to be implemented by two routes using React Router.

So let's install it:

npm install --save react-router-dom
Enter fullscreen mode Exit fullscreen mode

I'm going to set up the routes in the App.js file so let's import the necessary modules for that:

import { 
  BrowserRouter as Router, 
  useHistory, 
  Route } from "react-router-dom";
Enter fullscreen mode Exit fullscreen mode

And the new components too:

import AddReviews from "./Components/Pages/AddReviews";
import ShowReviews from "./Components/Pages/ShowReviews";
Enter fullscreen mode Exit fullscreen mode

Now let's set up those two routes:

<Route path="/add-reviews/:movieid/:moviename">
  <AddReviews />
</Route>
<Route path="/reviews">
  <ShowReviews />
</Route>
Enter fullscreen mode Exit fullscreen mode

The add-reviews route would serve the AddReviews component and reviews would serve ShowReviews. Now when using React router in a React app, the return body of App.js needs to be wrapped in Router, which I imported earlier. Also, I'm going to designate / to indicate my app's home page. Notice that the home page, i.e. the App component itself renders multiple components: Container, UserInput and MaterialTable. These can be conceived as children of the parent component App. In this scenario, it makes sense to use something called React.Fragment to wrap all of them. What this basically does is that no extra nodes aren't created in the DOM; it's just one component App. You can find out more about fragments here.

So the return body looks like this:

return (
    <Router>
      <div>
        <Header />
        <Route
          exact
          path="/"
          render={() => (
            <React.Fragment>
              <br></br>
              <Container maxWidth="xs" style={getContainerStyle}>
                <Typography
                  variant="h5"
                  style={{ marginTop: 50, marginBottom: 50 }}
                >
                  Enter a film name or phrase:
                </Typography>

                <UserInput
                  handleInputChange={handleInputChange}
                  handleSubmit={handleSubmit}
                />
              </Container>
              <MaterialTable
                title=""
                columns={[
                  {
                    title: "Name",
                    field: "name",
                    headerStyle: {
                      backgroundColor: "#A5B2FC",
                    },
                  },
                  {
                    title: "Director",
                    field: "director",
                    headerStyle: {
                      backgroundColor: "#A5B2FC",
                    },
                  },
                ]}
                // TODO: should add a progress bar or skeleton
                data={dataForRender}
                options={{
                  search: true,
                  actionsColumnIndex: -1,
                  headerStyle: {
                    backgroundColor: "#A5B2FC",
                  },
                }}
                actions={[
                  {
                    icon: () => <BorderColorIcon />,
                    tooltip: "Write a review",
                    // just using the window object to take to that route
                    // with the movie ID and name passed for running mutation
                    onClick: (event, rowData) =>
                      (window.location.pathname =
                        "/add-reviews/" +
                        rowData.id +
                        "/" +
                        rowData.name.split(" ").join("-")),
                  },
                ]}
                style={{ margin: "5rem" }}
              ></MaterialTable>
            </React.Fragment>
          )}
        ></Route>
        {/* we need some dynamic part in our URL here */}
        <Route path="/add-reviews/:movieid/:moviename">
          <AddReviews />
        </Route>
        <Route path="/reviews">
          <ShowReviews />
        </Route>
      </div>
    </Router>
  );
Enter fullscreen mode Exit fullscreen mode

You'll notice that I didn't place Header inside the fragment. That's because it's a fixed stateless component that is going to be rendered every time in all of the routes. Also, I've used Material UI's typography instead of plain HTMLh5 just as a design sugar; we could do just as well with a plain <h5>Enter a film name or phrase:</h5> like before. Typography can be imported with the following:

import Typography from "@material-ui/core/Typography";
Enter fullscreen mode Exit fullscreen mode

I'm using URL parameters (the one's starting with the colon, i.e. movieid and moviename) to make the movie ID and name available in AddReviews page. The ID is going to be necessary in mutation and the moviename is strictly for displaying a text saying what film the user is writing a review of.

Also, it'd be nice if there were navigation links in the application header so that we can go back and forth from the reviews page to our home page.

That can be done easily by tweaking our Header component a bit.

First I need to import the following:

import { Link } from "react-router-dom";
Enter fullscreen mode Exit fullscreen mode

I need two navigation links to navigate to two places: Home and Reviews corresponding to the route / and reviews. So inside the Toolbar I add the following:

<Link id="navlink" to="/">
  Home
</Link>
<Link id="navlink" to="/reviews">
  Reviews
</Link>
Enter fullscreen mode Exit fullscreen mode

Below is our tweaked return body:

return (
  <AppBar position="static">
    <Toolbar className="header-toolbar">
      <h2>Film Information</h2>
      <Link id="navlink" to="/">
        Home
      </Link>
      <Link id="navlink" to="/reviews">
        Reviews
      </Link>
    </Toolbar>
  </AppBar>
);
Enter fullscreen mode Exit fullscreen mode

A bit of CSS styling on Toolbar is involved here, in index.js:

.header-toolbar {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  /* background-color: #828fd8; */
  color: white;
}

.header-toolbar #navlink {
  margin-left: 3em;
  color: white;
  text-decoration: none;
}
Enter fullscreen mode Exit fullscreen mode

And here's the Header in all its new glories:

film-reviews header bar

Also, in index.js, I need to replace the uri field of the ApolloClient constructor object with the new backend for my app that Slash GraphQL deployed for me:

const APOLLO_CLIENT = new ApolloClient({
  uri: "https://hip-spring.us-west-2.aws.cloud.dgraph.io/graphql",
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          queryFilm: {
            merge(_ignored, incoming) {
              return incoming;
            },
          },
        },
      },
    },
  }),
});
Enter fullscreen mode Exit fullscreen mode

So, requests of every kind would now go there instead of what the app previously had, https://play.dgraph.io/graphql.

Let's go back and take a look at the return body of App.js.

We need a way so that upon clicking on a film the user is taken to the AddReviews component to write a review for that particular film. That's what I do that with the actions prop of MaterialTable:

actions={[
  {
    icon: () => <BorderColorIcon />,
    tooltip: "Write a review",
    // just using the window object to take to that route
    // with the movie ID and name passed for running mutation
    onClick: (event, rowData) => (window.location.pathname = 
      "/add-reviews/" +
      rowData.id +
      "/" +
      rowData.name.split(" ").join("-")),
  },
]}
Enter fullscreen mode Exit fullscreen mode

actions is just going to be another column in the table. Each row is basically a clickable icon, which is given through the icon property, the value of which is just a component for the icon. Upon hovering, a tooltip is going to give the user a useful prompt.

BorderColorIcon is imported like this:

import BorderColorIcon from "@material-ui/icons/BorderColor";
Enter fullscreen mode Exit fullscreen mode

I add an onClick event handler that would take us to the add-reviews route while adding the film ID corresponding to the row the user clicked on to the URL, along with the film name (the film name is just for the UI it won't play any role in the logic). So here we've basically set up a dynamic URL routing for our app! Cool isn't it?

After all this the table looks like the following after a search:

film-reviews after search

Let's look at the two components now.

AddReviews

This component is all about mutations. Basically there are going to be two mutations: one where I'd add info about the film that's getting a review written about, and the other are review details--rating and review text. Now, taking into the fact that a film already has a review by a user, that film's data is already in the database so I just need to run mutation for the review. So I set up two constants for each of the scenarios:

const ADD_REVIEW = gql`
  mutation($review: AddReviewInput!) {
    addReview(input: [$review]) {
      review {
        text
        rating
        posted_by {
          username
        }
        reviewed_film {
          id
          data {
            name
            id
          }
        }
      }
    }
  }
`;

const ADD_FILMDATA_AND_REVIEW = gql`
  mutation($filmData: [AddFilmDataInput!]!, $review: AddReviewInput!) {
    addFilmData(input: $filmData) {
      filmData {
        id
        data {
          name
          id
        }
      }
    }
    addReview(input: [$review]) {
      review {
        text
        rating
        posted_by {
          username
        }
        reviewed_film {
          id
          data {
            name
            id
          }
        }
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

ADD_REVIEW is just for adding a review, while the other is going to add film data too, in case that film doesn't already exist in the database. Notice that AddFilmDataInput and AddReviewInput are GraphQL input types automatically generated by Dgraph based on the schema, representing the local types FilmData and Review, corresponding to the variables $filmData and $review. $filmData would need to be supplied with the film ID that we pass from the home page to this component by the dynamic URL. $review, you guessed it right, would hold the review details. These are inputs for mutations represented as objects, by those two types AddFilmDataInput and AddReviewInput. Naturally one would have to write them on his/her own, but since I'm using Dgraph, I don't have to. That's another load out of my mind...

Wait... how would I know whether a film is present in my database and make the decision of running either one of those two mutations? I guess I have to check by ID by running a query. If I get a null response back, that means there are no films with that ID, i.e. I have to run ADD_FILMDATA_AND_REVIEW; otherwise, ADD_REVIEW.

Here's the query I'd need:

const CHECK_FILM_ID = gql`
  query($id: String!) {
    getFilmData(id: $id) {
      id
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

I set it up using Apollo's userQuery hook, just like the search function of App.js:

const { loading, error, data } = useQuery(CHECK_FILM_ID, {
    variables: { id: movieid },
  });
Enter fullscreen mode Exit fullscreen mode

Now I set up the states for the review details that would be submitted by the user:

const [reviewText, setReviewText] = useState("");
const [userName, setUserName] = useState("");
const [userRating, setUserRating] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Next up is getting an executable mutation using Apollo's useMutation hook, a counterpart of the useQuery hook:

const [addFilmDataAndReview] = useMutation(ADD_FILMDATA_AND_REVIEW);
const [addReview] = useMutation(ADD_REVIEW);
Enter fullscreen mode Exit fullscreen mode

I need four event handlers for keeping track of what the user enters as username, rating, review text and not to mention the submission handler...

// event handlers
const handleReviewChange = (event) => setReviewText(event.target.value);
const handleNameChange = (event) => setUserName(event.target.value);
const handleRatingChange = (event) => setUserRating(event.target.value * 1);
const handleSubmit = (event) => {
  event.preventDefault();
  // we add filmData only if that film doesn't already exist
  if (data.getFilmData === null) {
    addFilmDataAndReview({
      variables: {
        filmData: [
          {
            id: movieid,
          },
        ],
        review: {
          text: reviewText,
          rating: userRating,
          posted_by: {
            username: userName,
          },
          reviewed_film: {
            id: movieid,
          },
        },
      },
    });
  } else {
    addReview({
      variables: {
        review: {
          text: reviewText,
          rating: userRating,
          posted_by: {
            username: userName,
          },
          reviewed_film: {
            id: movieid,
          },
        },
      },
    });
  }
  // TODO: timeout could be removed
  setTimeout(() => (window.location.pathname = "/"), 1000);
};
Enter fullscreen mode Exit fullscreen mode

I check for a null response and let the app decide what mutation to run based on that.

Go back and take a look the addFilmData mutation again; the value of the variable $filmData looks like an array of AddFilmDataInput, right? So notice how I'm supplying it as a GraphQL variable here, as an array which contains the movie ID as object's key-value pair. I supply the movie ID as the value of a variable called movieid, which is none other than the dynamic part of the URL that contains it. That, and moviename, are easily accessible by using the useParams hook of React Router that extracts the URL parameters. I store that in the variable movieid. It can be imported with:

import { useParams } from "react-router-dom";
Enter fullscreen mode Exit fullscreen mode

And then I can get get the params using:

let { movieid, moviename } = useParams();
Enter fullscreen mode Exit fullscreen mode

The rest is pretty straightforward, I have all the user inputs stored in state variables so I'm using them to give the variables their necessary values.

After the mutations have been run, I redirect back to the home page, that is /. The setTimeout is just for debugging purposes in case something goes wrong and this would allow me to see the error screen before the URL changes.

Next, to set up the necessary "fields" for the user to submit his review, I import the following components from the material-ui package:

import TextField from "@material-ui/core/TextField";
import TextareaAutosize from "@material-ui/core/TextareaAutosize";
import Button from "@material-ui/core/Button";
import Radio from "@material-ui/core/Radio";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormLabel from "@material-ui/core/FormLabel";
import RadioGroup from "@material-ui/core/RadioGroup";
Enter fullscreen mode Exit fullscreen mode

The return body of AddReviews looks like the following:

return (
  <div className="container">
    <Typography variant="h4" style={getPageHeaderStyle}>
      Write your review of <em>{movieName}</em>
    </Typography>
    <Container maxWidth="xs" style={getContainerStyle}>
      <form
        className={styleClass.root}
        noValidate
        autoComplete="off"
        onSubmit={handleSubmit}
      >
        <div>
          <TextField
            label="Username"
            required
            value={userName}
            onChange={handleNameChange}
          />
          <div className="rating-input">
            <FormLabel component="legend" required>
              Rating
            </FormLabel>
            <RadioGroup
              aria-label="movie-rating"
              name="rating"
              value={userRating.toString()}
              onChange={handleRatingChange}
            >
              <FormControlLabel value="1" control={<Radio />} label="1" />
              <FormControlLabel value="2" control={<Radio />} label="2" />
              <FormControlLabel value="3" control={<Radio />} label="3" />
              <FormControlLabel value="4" control={<Radio />} label="4" />
              <FormControlLabel value="5" control={<Radio />} label="5" />
            </RadioGroup>
          </div>
          <TextareaAutosize
            id="review-textarea"
            required
            aria-label="review-text"
            rowsMin={10}
            placeholder="Review..."
            onChange={handleReviewChange}
          />
        </div>
        <div>
          <Button
            type="submit"
            variant="contained"
            color="primary"
            style={{ marginTop: 20 }}
          >
            Submit
          </Button>
        </div>
      </form>
    </Container>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

I need to make moviename displayable as a space separated string:

let movieName = moviename.split("-").join(" ");
Enter fullscreen mode Exit fullscreen mode

All this, as I said before, is just for displaying a nice header that says what film is getting reviewed.

Next is just plain HTML form, inside which I make use of the components that I imported earlier. TextField is where one types in his/her username, a bunch of radio buttons for the 5-star rating system, a re-sizable textarea for where we write our thoughts on the film, and finally the submit button. The container works just like before, placing the whole thing at the centre of the page.

So, after clicking on a film, the user gets greeted with this page:

AddReviews page of the app

ShowReviews

This component renders all the information stored in the database, arranged by films, i.e. for each film I show all the reviews submitted by various users.

Here's the query that gets the job done (it's the same as I mentioned when we discussed the schema):

const GET_REVIEWS = gql`
  query q2 {
    queryReview {
      reviewed_film {
        id
        data {
          id
          name
        }
        reviews {
          posted_by {
            username
          }
          rating
          text
        }
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

I don't need to explicitly define any state here though, because each time this page is accessed the query would automatically be run and the data we're rendering through the return body would change accordingly. So the following is pretty standard stuff:

function ShowReviews() {
  const { loading, error, data } = useQuery(GET_REVIEWS);

  if (loading) {
    return <CircularProgress />;
  } else if (error) {
    console.log(error);
    return (
      <Alert severity="error">
        <AlertTitle>Error</AlertTitle>
        Sorry, something might not be working at the moment!
      </Alert>
    );
  }

  return (
    <div className="review-content">
      <Typography id="page-title" variant="h2" align="center">
        Reviews
      </Typography>
      {/* map over to render the review details */}
      {data.queryReview.map((content) => (
        <div id="review-details">
          <Typography variant="h4" align="left">
            {content.reviewed_film.data.name}
          </Typography>
          <Divider />
          <br></br>
          {content.reviewed_film.reviews.map((reviewObj) => (
            <Typography variant="subtitle2" align="left">
              {reviewObj.posted_by.username}
              <Typography variant="subtitle1" align="left">
                Rating: {reviewObj.rating}
              </Typography>
              <Typography variant="body1" align="left">
                {reviewObj.text}
              </Typography>
              <br></br>
              <Divider light />
              <br></br>
            </Typography>
          ))}
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

I just use JavaScript's map method to iterate over the the JSON response tree and render the details.

And Divider is just a Material UI component that's nothing but HTML's <hr> tag under the hood, strictly for decorative purposes so that the "Reviews" are a bit nicely displayed.

This is how the page looks:

ShowReviews page of the app

Here's a GIF showing the flow of the app:

A GIF of film-reviews app

Conclusions

Whew! That was a lot of work wasn't it? But Dgraph took most of the pains away; I just had to focus on the data my app would be handling and how that could be represented by a GraphQL schema. "Thinking in terms of graph" is a saying that goes when building something with GraphQL. I just had to do that; when those pieces are put together and a couple of types are nicely defined in my schema, I just needed to deploy it using Slash GraphQL and I had a working API up and running that could handle my data perfectly and allow me to use it however I chose. The rest is just JavaScript and some rudimentary front-end tooling.

Another rewarding experience that can be taken from here is that this a pretty close experiment that gives a peek at a real-world application that functions by handling remote and local data. We're using utilities like that everyday, and through this small app, this has been a gentle introduction into the whole orchestration of a large-scale app.

You can check out the entire code of this project that lives on the repo here.

References

Oldest comments (0)