Using the Rick and Morty API with GraphQL and React JS.

Using the Rick and Morty API with GraphQL and React JS.

ยท

14 min read

We have previously used the Rick and Morty API with React but this time we will use it with the help of GraphQL and Apollo Client.

ย 

๐Ÿ”บ Technologies to be used.

  • React JS
  • Apollo Client
  • TypeScript
  • Vite JS
  • Tailwind CSS (the installation and configuration are not shown in this article, check the repository better.)

๐Ÿ”บ Creating the project.

We will name the project: rick-morty-graphql-react (optional, you can name it whatever you like).

npm create vite@latest

We create the project with Vite JS and select React with TypeScript.
Then we run the following command to navigate to the directory just created.

cd rick-morty-graphql-react

Then we install the dependencies.

npm install

Then we open the project in a code editor (in my case VS code).

code .

๐Ÿ”บ What is GraphQl?.

GraphQL is an API definition and query language and an alternative to REST. It makes it easy for UI components to get data declaratively without having to worry about backend implementation details. As such a powerful abstraction, GraphQL can speed up application development and ease code maintenance.

๐Ÿ”บ First steps.

Inside the src/App.tsx file we delete everything and create a component that displays a hello world.

const App = () => {
  return <div>Hello world</div>;
};
export default App;

๐Ÿšจ Note: Every time we create a new folder, we will also create an index.ts file to group and export all the functions and components of other files that are inside the same folder, so that those functions can be imported through a single reference, this is known as barrel file.

First we are going to create a small layout for our app. We create the src/components folder and inside a Layout.tsx file, which will contain the following.

export const Layout = ({ children }: { children: React.ReactNode }) => {
  return (
    <main className='container mx-auto my-5 text-center'>
      <h1 className='text-5xl font-bold'>
        Rick & Morty -{' '}
        <span className='font-black bg-clip-text text-transparent bg-gradient-to-r from-pink-500 to-violet-500'>
          GraphQl
        </span>
      </h1>

      {children}
    </main>
  );
};

Now we use the layout in the file src/App.tsx.

import { Layout } from './components';

const App = () => {
  return (
    <Layout>
      <div>Hello world</div>
    </Layout>
  );
};
export default App;

After that, let's install several packages.

  • apollo/client: apollo client allows you to easily create UI components that fetch data through GraphQL.
  • graphql: dependency needed by @apollo/client.
  • framer-motion: animation library for react.
npm install @apollo/client graphql framer-motion

๐Ÿ”บ Configuring Apollo client.

The configuration in this case is very simple, since the Rick & Morty API only allows us to read data.

First we are going to create a new Apollo client. For this we import the ApolloClient class, and in the src/App.tsx file we are going to create the client (although you can create it in a separate file if you want).

import { ApolloClient } from '@apollo/client';

We create a new constant that will be our instance of ApolloClient, which receives the following mandatory configuration.

To create the client you need:

  • uri: the link to the GraphQL server where we will make the queries.
  • cache: the cache that Apollo client will use to store locally the results of the requests. In the documentation of Apollo Client it mentions the cache they recommend and which Apollo client provides.
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://rickandmortyapi.com/graphql',
  cache: new InMemoryCache(),
});

Finally, we need to import the apollo provider and place it at the top of the application, so that it wraps the entire app. To the ApolloProvider we pass the client we just created and ready, this would be the basic configuration to use Apollo client.

import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { Grid, Layout } from './components';

const client = new ApolloClient({
  uri: 'https://rickandmortyapi.com/graphql',
  cache: new InMemoryCache(),
});

const App = () => {
  return (
    <ApolloProvider client={client}>
      <Layout>
        <div>Hello world</div>
      </Layout>
    </ApolloProvider>
  );
};
export default App;

๐Ÿ”บ Creating the first query.

Now we are going to create the query that we will use later. In the queries we basically define what data the server has to send back to us.

Unlike a REST API where it will always send you all the data, whether you need it or not. But with GraphQl you can tell the server what data you really need and thus improve the loading speed of the request.

First we create the folder src/graphql/query and inside the file characters.graphql.ts.

Inside the file, we define a constant and we will use the gql function. And as we are making a request of type read, we will say that we want to make a query (because we only want to read the data) and as if it were an object, we begin to place the properties that we want to obtain from the request.

import { gql } from '@apollo/client';

export const GET_CHARACTERS = gql`
  query {
    characters {
      results {
        name
        id
        image
      }
    }
  }
`;

And to know what data the server response handles, almost always the servers that use GraphQl come with a documentation, in a playground. For example: this is the one for https://rickandmortyapi.com/graphql. (Usually by accessing the /graphql path of almost any backend, you can access the playground to see the API documentation and its data types, etc.)

๐Ÿ”บ Making the first request.

Now we are going to create a new component where we are going to make our first request. We create a Grid.tsx file in src/components.

export const Grid = () => {
  return (
    <section className='grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-4 my-10 lg:gap-10 gap-5  px-5 sm:px-8 lg:px-12'></section>
  );
};

To make read-only requests we will use the hook provided by apollo client, useQuery. We will use it in the following way:

We pass the hook the query we created earlier as the first argument: GET_CHARACTERS. The hook useQuery returns several properties, of which we are going to use 3 in this occasion.

  • loading: Indicates the status of the request and is a boolean value.
  • error: Indicates if there was an error with the request and tells us what it is, otherwise it is undefined.
  • data: It is the information we receive from the backend, which by default is undefined.
const { loading, error, data } = useQuery(GET_CHARACTERS);

And this is how the component would look like using the hook. Note that we make conditions in case the loading is set to true or if there is an error. The useQuery hook has also been typed to be of type Characters (which I'm not going to show here, but you can see the type in the repository). Once we have passed the conditions we then traverse the array of characters we get from the request.

import { useQuery } from '@apollo/client';
import {
  CardCharacter,
  CharacterSelected,
  ErrorMessage,
  Loading,
} from '../components';
import { GET_CHARACTERS } from '../graphql/query';
import { Characters } from '../types';

export const Grid = () => {
  const { loading, error, data } = useQuery<Characters>(GET_CHARACTERS);

  if (loading) return <Loading />;

  if (error) return <ErrorMessage error={error.message} />;

  return (
    <section className='grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-4 my-10 lg:gap-10 gap-5  px-5 sm:px-8 lg:px-12'>
      {data?.characters.results.map((character, index) => (
        <div key={character.id}>character</div>
      ))}
    </section>
  );
};

๐Ÿ”บ Creating the cards for the characters.

Now it's time to use framer-motion to give an animation to each card so that they appear with an animation from bottom to top and that each card has its own time delay.

First we create the new file in src/components and name it CardCharacter.tsx.

The way to use framer motion is to simply add the word motion. before the name of each tag.

<motion.div> Animation </motion.div>
<motion.img/>

Now, this component is going to receive, apart from the request data, also:

  • onSelected: function that will be used to know which card has been selected and obtain all its data.
  • index: the index of each card to calculate the delay and make the animation.

Also note that we are passing 3 props, apart from the className, to the first motion.div:

  • initial: what the element should look like at startup.
  • animate: how the element should look at the end, what properties will be animated.
  • transition: in this case we only modify the delay so that the animation is executed later.
import { motion } from 'framer-motion';
import { ResultCharacters } from '../types';

interface Props extends ResultCharacters {
  onSelected: (id: string) => void;
  index: number;
}

export const CardCharacter = (props: Props) => {
  const { id, image, name, onSelected, index } = props;

  return (
    <motion.div
      initial={{ opacity: 0, y: -100 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ delay: 0.1 * index }}
      className='bg-black rounded-md flex flex-col sm:w-auto mx-auto overflow-hidden'
    >
      {/* content... */}
    </motion.div>
  );
};

This is how our component would look like in the end. Note that we place the onSelected in the second motion.div and we send the id that we received by props.

import { motion } from 'framer-motion';
import { ResultCharacters } from '../types';

interface Props extends ResultCharacters {
  onSelected: (id: string) => void;
  index: number;
}

export const CardCharacter = (props: Props) => {
  const { id, image, name, onSelected, index } = props;

  return (
    <motion.div
      initial={{ opacity: 0, y: -100 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ delay: 0.1 * index }}
      className='bg-black rounded-md flex flex-col sm:w-auto mx-auto overflow-hidden'
    >
      <motion.div
        className='overflow-hidden cursor-pointer'
        onClick={() => onSelected(id)}
      >
        <motion.img
          src={image}
          alt={name}
          className='align-top hover:scale-105 transition-all hover:grayscale-0 grayscale'
        />
      </motion.div>

      <motion.p className='break-words p-2 font-semibold flex-1 grid place-items-center'>
        {name}
      </motion.p>
    </motion.div>
  );
};

And now we use this CardCharacter component in src/components/Grid.tsx.

Note that we create a state to store the id of the selected character.

import { useQuery } from '@apollo/client';
import { useState } from 'react';
import {
  CardCharacter,
  CharacterSelected,
  ErrorMessage,
  Loading,
} from '../components';
import { GET_CHARACTERS } from '../graphql/query';
import { Characters } from '../types';

export const Grid = () => {
  const [selected, setSelected] = useState<string | null>(null);

  const { loading, error, data } = useQuery<Characters>(GET_CHARACTERS);

  if (loading) return <Loading />;

  if (error) return <ErrorMessage error={error.message} />;

  return (
    <section className='grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-4 my-10 lg:gap-10 gap-5  px-5 sm:px-8 lg:px-12'>
      {data?.characters.results.map((character, index) => (
        <CardCharacter
          key={character.id}
          {...character}
          index={index}
          onSelected={(characterId: string) => setSelected(characterId)}
        />
      ))}
    </section>
  );
};

๐Ÿ”บ Getting character by ID.

First we are going to start by creating some components to make the modal that will open when we have selected a character.

  1. src/components/Modal.tsx

This component receives 3 props:

  • onClearSelected: function to hide the modal and remove the selected character.
  • children**: the elements that will be shown inside the modal.
  • isOpen**: boolean value to know if the modal is open or not.

We use the isOpen props to define the variants to create the animation of the modal when it opens or closes. We will create variants for the first div, since that will be the background with blur style, and we will also create another variant for the content inside the modal.

import { motion, Variants } from 'framer-motion';

interface Props {
  onClearSelected: () => void;
  children: JSX.Element | JSX.Element[];
  isOpen: boolean;
}

const variants: Variants = {
  opened: {
    width: '100vw',
    height: '100vh',

    borderRadius: '5px',
    transition: { duration: 0.5 },
  },
  closed: {
    width: '0vw',
    height: '0vh',
    borderRadius: '20px',
    transition: { duration: 0.5 },
  },
};

const variantsContent: Variants = {
  opened: {
    opacity: 1,
    y: 0,
    transition: { delay: 0.6, duration: 0.3 },
  },
  closed: {
    opacity: 0,
    display: 'none',
    y: 200,
    transition: { delay: 0 },
  },
};

export const ModalCharacter = ({
  children,
  isOpen,
  onClearSelected,
}: Props) => {
  return (
    <motion.div
      className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-black/50 backdrop-blur-lg text-white select-none'
      variants={variants}
      animate={isOpen ? 'opened' : 'closed'}
    >
      <motion.div
        variants={variantsContent}
        animate={isOpen ? 'opened' : 'closed'}
      >
        {children}
      </motion.div>
      {isOpen && (
        <button
          onClick={onClearSelected}
          className='bg-red-500 text-white rounded-md fixed sm:top-8 top-2 sm:right-10 right-4 py-1 px-2 font-semibold'
        >
          close
        </button>
      )}
    </motion.div>
  );
};
  1. src/components/CharacterSelected.tsx

  2. characterId: the id of the selected character.

  3. onClearSelected: function to hide the modal and remove the selected character.

Here we use the modal component and inside the ModalCharacter we will evaluate if the characterId exists then we show the content of the modal.

Note that we are using the CharacterInfo component, but we have not created it yet, but at the end it will be in scr/components/CharacterInfo.tsx and will receive as props only the characterId.

import { CharacterInfo } from './CharacterInfo';
import { ModalCharacter } from './Modal';

interface Props {
  characterId: string | null;
  onClearSelected: () => void;
}

export const CharacterSelected = ({ characterId, onClearSelected }: Props) => {
  return (
    <ModalCharacter
      isOpen={!!characterId}
      onClearSelected={onClearSelected}
    >
      <>{characterId && <CharacterInfo characterId={characterId} />}</>
    </ModalCharacter>
  );
};

Before creating the component where the character info will be, let's create the graphql query. We go back to the file src/graphql/query/character.graphql.ts, and add the following query.

First we will create the constant for query

export const GET_CHARACTER_BY_ID = gql`
  query {
  }
`;

And as if it were a function, we are going to open some parenthesis after the query to make reference that we are going to receive a parameter.

We use the character "$" to set the name that our variable will receive, which in this case we will name it $id. Following the name of the variable, we must set the type of data that is the variable. In this case it is of type ID since this way the API represents it, and you can review the documentation. Finally we add the character "!", which means that this variable $id is mandatory.

export const GET_CHARACTER_BY_ID = gql`
  query ($id: ID!) {

  }
`;

Well, so far we have only established that this query receives an ID, but what do we want to do with this ID?

Well, we want to filter a character by that ID. For it, and following the documentation of the API, we access to character and as if it was a function, we access to the property id, because we want to filter by id, so we send our variable $id that we received from the query and then we already access to the properties that we want to obtain.

export const GET_CHARACTER_BY_ID = gql`
  query ($id: ID!) {
    character(id: $id) {
      id
      name
      status
      species
      gender
      image
      created
    }
  }
`;

Now we create this file if you do not have it yet, src/components/CharacterInfo.tsx and this component receives the characterId.

export const CharacterInfo = ({ characterId }: { characterId: string }) => {};

And it will be practically the same as how we did the data retrieval using useQuery a few moments ago. The only difference is that in the useQuery we are going to pass a variable.

The second parameter of useQuery is a configuration object, from which we access the variables property, and we send an object with all the variables that this query needs, which in this case is only one.

Note that the key of the object is called id, which refers to the name of the variable $id, basically we must put the same name of the variable that we declared when creating the query, only without the symbol of "$".

The value of the variable id will be the characterId that we receive in the props of the component.

const { data, loading, error } = useQuery<CharacterByID>(
  GET_CHARACTER_BY_ID,

  { variables: { id: characterId } }
);

Finally our component would look like this.

import { useQuery } from '@apollo/client';
import { GET_CHARACTER_BY_ID } from '../graphql/query';
import { CharacterByID } from '../types';
import { ErrorMessage } from './ErrorMessage';
import { Loading } from './Loading';

export const CharacterInfo = ({ characterId }: { characterId: string }) => {
  const { data, loading, error } = useQuery<CharacterByID>(
    GET_CHARACTER_BY_ID,
    { variables: { id: characterId } }
  );

  if (loading) return <Loading />;

  if (error || !data) return <ErrorMessage error={error!.message} />;

  const { created, gender, image, name, species, status } = data.character;

  return (
    <div className='max-w-4xl mx-auto mt-16 flex flex-col justify-center items-center sm:gap-8 gap-3 sm:px-0 px-8'>
      <span className='text-3xl lg:text-4xl font-semibold'>{name}</span>

      <img
        src={image}
        className='sm:w-52 sm:h-52 w-32 h-32 rounded-md shadow-2xl shadow-black'
      />

      <div className='w-full sm:w-fit flex flex-col gap-2'>
        <p className='rounded-lg bg-gray-800 p-4 text-start sm:text-xl text-lg relative'>
          Status: {status}
        </p>
        <p className='rounded-lg bg-gray-800 p-4 text-start sm:text-xl text-lg relative'>
          Specie: {species}
        </p>
        <p className='rounded-lg bg-gray-800 p-4 text-start sm:text-xl text-lg relative'>
          Gender: {gender}
        </p>

        <p className='mt-2 text-lg'>
          Created at: <span>{created}</span>
        </p>
      </div>
    </div>
  );
};

Note: make sure that in the component src/components/CharacterSelected.tsx you have imported the component we just created and send it the characterId.

Now we go to the src/components/Grid.tsx component. We add the component CharacterSelected and send it its respective props.

import { useQuery } from '@apollo/client';
import { useState } from 'react';
import {
  CardCharacter,
  CharacterSelected,
  ErrorMessage,
  Loading,
} from '../components';
import { GET_CHARACTERS } from '../graphql/query';
import { Characters } from '../types';

export const Grid = () => {
  const [selected, setSelected] = useState<string | null>(null);

  const { loading, error, data } = useQuery<Characters>(GET_CHARACTERS);

  if (loading) return <Loading />;

  if (error) return <ErrorMessage error={error.message} />;

  return (
    <section className='grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-4 my-10 lg:gap-10 gap-5  px-5 sm:px-8 lg:px-12'>
      {data?.characters.results.map((character, index) => (
        <CardCharacter
          key={character.id}
          {...character}
          index={index}
          onSelected={(characterId: string) => setSelected(characterId)}
        />
      ))}

      <CharacterSelected
        onClearSelected={() => setSelected(null)}
        characterId={selected}
      />
    </section>
  );
};

Note: You may notice that even if the status of the selected character changes, the request to search for all characters will not be triggered again, since the data is kept in cache by Apollo Client.

Now, let's use our application!

๐Ÿ”บ Conclusion.

No doubt GraphQl is a very good alternative to Rest API, although as this is a very small application, you may not notice the difference in performance when making API calls, but as your app grows, it will be a very good option to opt for GraphQl. ๐Ÿ˜‰

I hope you liked this post and I also hope I helped you understand how to make basic requests using GraphQl and Apollo client. ๐Ÿ™Œ

If you know any other different or better way to perform this application feel free to comment!

I invite you to review my portfolio in case you are interested in contacting me for a project! โžก๏ธ Franklin Martinez Lucas

Don't forget to follow me also on... โžก๏ธ Twitter: @Frankomtz361 โžก๏ธ GitHub: Franklin361

๐Ÿ”บ Demo.

https://rick-morty-graphql-react.netlify.app/

๐Ÿ”บ Source code.

https://github.com/Franklin361/rick-morty-grapqhl-react

ย