Quick prototyping with GRAND stack: part 3
- Part 1 – Product introduction, tech spec and graph model.
- Part 2 - Apollo Graphql server and neo4j-graphql-js
- Part 3 - Apollo client and React
In the previous part we set up our graphql server with neo4j-graphql-js. Here we will consider how apollo-client can add a productivity boost by simplifying data management in your React applications.
Data management made simple
Apollo client is a great data management solution as it covers all the possible data requirements of a modern spa. Let's consider how helplful it is for our product. Here is the react component for managing the home screen of our application – potential collaboration candidates.
import React from "react";
import { useQuery, gql } from "@apollo/client";
import { Viewer } from "../generated/Viewer";
import FireCard from "./FireCard";
import { useHistory } from "react-router-dom";
import NoResults from "./NoResults";
import Loading from "./Loading";
export const GET_USER = gql`
query Viewer($id: ID!) {
viewer(userId: $id) {
userId
matchCandidates(first: 10) {
score
user {
name
bio
imageUrl
userId
skills {
name
}
}
}
}
}
`;
const Fire: React.FC<{ id: string }> = (props) => {
const { loading, data, error, refetch, client } = useQuery<Viewer>(GET_USER, {
variables: { id: props.id },
notifyOnNetworkStatusChange: true,
});
const history = useHistory();
// once viewer made a decision about (dis)liking, update the cache by removing cards viewer dis(liked)
// pass function to FireCard component, which runs (dis)like mutation
const update = (id: string) => {
client.writeQuery({
query: GET_USER,
variables: { id: props.id },
data: {
viewer: {
...data?.viewer,
matchCandidates: data?.viewer.matchCandidates.filter(
(match) => match.user.userId !== id
),
},
},
});
};
// refetch when swiped on all suggested candidates
React.useEffect(() => {
if (data && data.viewer.matches.length < 1) {
refetch();
}
}, [data, refetch]);
if (loading) {
return <Loading>Loading potential candidates...</Loading>;
}
if (error || !data) {
return (
<h1 style={{ textAlign: "center", height: "100vh" }}>
Try reloading the page...
</h1>
);
}
const { viewer } = data;
if (viewer.matches.length < 1) {
return (
<NoResults
buttonText={"Update preferences"}
description="We don't have any candidates for you now. Try updating your preferences."
action={() => history.push("/profile")}
/>
);
}
return (
<section className="f-col-center">
<h1>Best candidates for {viewer.name}</h1>
{viewer.matchCandidates.map((item) => (
<FireCard
key={item.user.userId}
update={update}
viewerId={props.id}
score={item.score}
{...item.user}
/>
))}
</section>
);
};
export default Fire;
A lot is going on there. But let's start from the very beginning. At first, we define our graphql query GET_USER to specify our data requirements for the component. In part two, we had the matchCandidates field on type User, here we are requesting that data so our client can show potential match candidates. Apollo-client ships with a bunch of helpful react hooks to take advantage of the new react hooks functionality. First line of our function component calls useQuery hook and gets back convenient properties to manage the state of the query. Next, we have an update function to update our cache after the like or dislike has been made. Apollo-client has a nice cache.modify api which can be specified in the update argument of the mutation. Here is the extract from their docs:
const [addComment] = useMutation(ADD_COMMENT, {
update(cache, { data: { addComment } }) {
cache.modify({
fields: {
comments(existingCommentRefs = [], { readField }) {
const newCommentRef = cache.writeFragment({
data: addComment,
fragment: gql`
fragment NewComment on Comment {
id
text
}
`,
});
return [...existingCommentRefs, newCommentRef];
},
},
});
},
});
The reason I am specifying an update function in the parent component is that I have 2 mutations, like and dislike, so it is less cumbersome in the FireCard component:
const ADD_LIKE = gql`
mutation AddLike($from: ID!, $to: ID!) {
like(from: $from, to: $to) {
matched
matchId
email
}
}
`;
const DISLIKE = gql`
mutation AddDislike($from: ID!, $to: ID!) {
dislike(from: $from, to: $to)
}
`;
const FireCard: React.FC<Props> = ({
imageUrl,
bio,
name,
skills,
userId,
score,
viewerId,
update,
}) => {
const history = useHistory();
const variables = { from: viewerId, to: userId };
const [addLike, { loading }] = useMutation<AddLike>(ADD_LIKE, {
notifyOnNetworkStatusChange: true,
});
const [addDislike, { loading: disLoading }] = useMutation<AddDislike>(
DISLIKE,
{
notifyOnNetworkStatusChange: true,
}
);
const dislike = async () => {
await addDislike({ variables });
update(userId);
};
const like = async () => {
const result = await addLike({ variables });
const matchId = result.data?.like?.matchId;
if (matchId) {
// go to match
message.success(
`Great! You matched with ${name}! Say hi, by adding your first track.`
);
history.push(`/matches/${matchId}`);
}
update(userId);
};
return (
<Card
hoverable
className={"card"}
style={{
cursor: "auto",
marginTop: 20,
}}
actions={[
disLoading ? (
<Spin indicator={antIcon} />
) : (
<DislikeFilled
onClick={dislike}
style={{ fontSize: 22 }}
key="dislike"
/>
),
loading ? (
<Spin indicator={antIcon} />
) : (
<LikeFilled style={{ fontSize: 22 }} onClick={like} key="like" />
),
]}
>
<Meta
avatar={<Avatar size={50} src={imageUrl || getRandomImage(name)} />}
title={name}
description={bio}
/>
<p style={{ marginTop: 20, color: "rgb(150,150,150)" }}>
<span>{score} overlapping</span>
</p>
<div
style={{
borderTop: "1px solid rgb(200,200,200)",
}}
>
<h4 style={{ marginTop: 20 }}>I am skilled at</h4>
</div>
<Tags items={skills} />
</Card>
);
};
export default FireCard;
This is really what is great about apollo – it takes care of your data fetching and management needs in an intuitive, easy to grasp way. No more of your cache management redux code or fetching sagas' testing. Apollo just works and takes a burden of maintaining and testing remote-data synchronization logic away from you. Just concentrate on your application requirements and not on the common data fetching and management setup!
To further illustrate this, one of the use cases of the application is to be able to specify preferences to be matched on. Once you choose your preferences, the application should show you a new candidates list in our home screen.
Originally, I had a useEffect hook set up in the Fire component and had some logic for refetching data on the preference update. But then I thought, this use case is so common, what does apollo have for that? And as expected, they have a convenient refetchQueries api which amounted to adding our GET_USER query to the refetch list, once the viewer updated their preferences:
const [batchPrefer, { loading: mutationLoading }] = useMutation<Batch>(
BATCH_PREFER,
{
notifyOnNetworkStatusChange: true,
refetchQueries: [{ query: GET_USER, variables: { id: props.id } }],
awaitRefetchQueries: true,
}
);
This is the running theme of GRAND stack, let great technologies abstract away common tasks, so you can concentrate on your business requirements. Don't spend your precious brain cycles on boilerplate, delegate this to talented engineering teams at neo4j and apollo and ship your ideas faster.
Building the product
In these series I went through building a GRAND stack application which is now live. Originally, after specifying the tech spec, I timeboxed it to within a weekend after having my graphql server powered by neo4j graph working in 2 hours. But then, spend the week drawing components using tailwindcss, after finally giving up and switching to ant design, which is a great solution for quick prototyping. Al in all, I went 2 weeks over my original expectation, mainly due to ui concerns. The motivation for this product and building in public came from https://www.indiehackers.com/ and me wanting to learn a graph technology. Now I am hoping to find co-founders for this product through its users.
Top comments (0)