Previously I wrote about how to use Angular with Apollo Graph QL here. I was watching a GraphQL video on egghead.io from Eve Porcello and I thought it would be fun to mimic the actions she performed on a GraphQL Playground (https://pet-library.moonhighway.com) using Angular and Apollo GraphQL Client.
Since I have been wanting to try out React for a while, I thought of rewriting the angular app in React using hooks and functional components and would like to share my learning experience with you.
Project
Our project will be a simple clone of pet library playground from scratch using React and Apollo Client,with features including authentication, returning a list of all pets and check-in/check-out pets. We will not use class-based components in this project since react hooks let us manage local component state and component life cycle.
You can view the complete source code here.
Folder Structure
Components folder will include the following components:
List.js : A simple presentational component that will render the list of pets and will take pets as props.
CheckIn.js: component to check in a pet and also to refresh the list.
CheckOut.js: component to check out a pet, both CheckIn and CheckOut component takes a pet Id as prop.
SelectStatus.js: component to filter out pets based on status.
Pages folder will include the following components:
ListContainer.js: A container component that will contain both the filter drop down and List component.
Login.js: component that deals with authentication.
Let’s use create-react-app to create our project.
create-react-app react-apollo-graphql-hooks
We will also be using react bootstrap in our project, so let’s add it to our project by typing:
npm install react-bootstrap bootstrap
Then we will create SelectStatus.js in components folder and add the following code.
export const SelectStatus = ({ petStatus, defaultValue, onSelect }) => {
const setSelect = (e) => {
e.preventDefault();
let index = e.target.options.selectedIndex;
let status = petStatus[index];
if (onSelect) {
onSelect(status);
}
};
return (
<>
<Form.Group controlId="status">
<Col>
<Form.Label>Pet Status:</Form.Label>
</Col>
<Col>
<Form.Control
as="select"
defaultValue={defaultValue?.name}
onChange={(e) => setSelect(e)}
>
{petStatus.map((item) => {
return <option key={item.key}>{item.name}</option>;
})}
</Form.Control>
</Col>
</Form.Group>
</>
);
};
Ignore the props { petStatus, defaultValue, onSelect } for now, we will get back to those later. As you can see this component is just a presentational component that doesn’t hold any internal state and just renders the bootstrap “select” component, by looping through the pet status list.
Let’s move on to the List Component.
export const List = ({ pets }) => {
return (
<>
<div className="row mt-4">
<div className="col-sm-8">
<table className="table table-striped">
<thead>
<tr>
<td className="w-25">
<p> Pet </p>
</td>
<td className="w-30">
<p> Category</p>
</td>
<td className="w-50">
<p> Customer</p>
</td>
<td className="w-50">
<p> Action</p>
</td>
</tr>
</thead>
<tbody>
{pets.map((item) => {
return (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.category}</td>
<td>{item.inCareOf?.name}</td>
<td>
{item.status === "AVAILABLE" ? (
<CheckOut petId={item.id} />
) : (
<CheckIn petId={item.id} />
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
;
</>
);
};
It’s also just a presentational component that will render a list of pets. It also has a CheckIn/CheckOut component that takes in a pet id as a prop. Let’s get back to CheckIn/CheckOut components later.
Before creating the container components, let’s write our first Apollo Client hook. Create usePetsHooks.js in hooks folder with the following code.
import { useQuery } from "@apollo/client";
import gql from "graphql-tag";
const petFieldsQuery = gql`
fragment petFields on Pet {
id
name
category
status
inCareOf {
name
}
}
`;
export const filterPetsQuery = gql`
query petsQuery($status: PetStatus) {
allPets(status: $status) {
...petFields
}
}
${petFieldsQuery}
`;
export default (status) => {
return useQuery(filterPetsQuery, {
fetchPolicy: "network-only",
variables: {
status: status,
},
});
};
We are using Apollo Client’s useQuery hook to fetch GraphQL data. A call to useQuery returns an object with properties including loading, error, data and a refetch function. We will look into how to use the refetch function when we get to CheckIn/CheckOut functionality. I am also keeping fetchPolicy as “network-only”, since we are not interested in caching query results in this project.
We also need to make sure that the List refreshes, when we do a CheckIn/CheckOut, so the current pet disappears from the list. Remember useQuery returns a refetch function ? And we want to call this refetch function from the CheckIn component, when we checks in a pet. How do we do that without making the List component a pass through component for refetch ?
One way is to use Context API, so we don’t have to manually pass props through the List component to CheckIn/CheckOut components. So using the API we can create a new context.
Create a file, refetchProvider.js with the following code.
import React from "react";
export const PetsContext = React.createContext({});
export const PetsProvider = PetsContext.Provider;
Provider can contain any values, and it can be a function (action) too. We will set the refetch function as a provider value in the next section.
Next lets’ create the container component ListContainer.js.
export const ListContainer = () => {
const petStatus = [
{ key: 1, id: null, name: "All" },
{ key: 2, id: "AVAILABLE", name: "Available" },
{ key: 3, id: "CHECKEDOUT", name: "Checked Out" },
];
const [selectedStatus, setSelectedStatus] = useState(() => null);
const { loading, error, data, refetch } = usePetsQuery(
selectedStatus ? selectedStatus.id : null
);
const onSelectStatus = (status) => {
setSelectedStatus(status);
};
const onRefetch = () => {
refetch();
};
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<>
<Container className="mt-4">
<Form>
<Form.Row>
<SelectStatus
petStatus={petStatus}
onSelect={onSelectStatus}
defaultValue={selectedStatus}
/>
<div className="ml-auto">
<Logout />
</div>
</Form.Row>
</Form>
<PetsProvider value={() => onRefetch()}>
<List pets={data.allPets} />
</PetsProvider>
</Container>
</>
);
};
We are using the Container pattern, so that we can separate the state and events from the presentational components.
const [selectedStatus, setSelectedStatus] = useState(() => null);
Here we are using React.useState to maintain the state of select drop down. useState returns an array and we can use ES6 destructuring syntax to access the values. When we change the select filter, we need to re render the entire list component and the updater function (setSelectedStatus) will take care of that.
Also notice how we have wrapped the List component into a PetsProvider. This helps us to use the context in each component. We will see that shortly in CheckIn component.
For check in functionality, lets’ create a CheckIn component.
export const CheckIn = ({ petId }) => {
const refetch = useContext(PetsContext);
const doCheckIn = useCheckInMutation();
const checkIn = () => {
doCheckIn(
{
variables: { petId: petId },
},
{ refetchQueries: [`petsQuery`] }
)
.then((_) => {
refetch();
})
.catch((e) => console.log(e));
};
if (!isLoggedIn()) {
return null;
}
return (
<>
<button onClick={() => checkIn()} className="btn btn-link">
Check In
</button>
</>
);
};
We get a reference to the refetch handler from the useContext API. After the check in mutation happens, we will call the refetch() function, which will in turn invoke the onRefetch handler in ListContainer.js.
Conclusion
This was my attempt to share what I learned using React hooks and Context API. This sample project shows how to maintain local state using useState and how to pass the context to inner components, as long as they are somewhere in the same component tree. You can find more information on hooks here.
You can view the complete source code here.
My original article is here.
Top comments (0)