End to End React with Prisma 2
Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.
27. Add like button
Objective: Add a like button to heart your favorite feeds and bundles.
In this step, we are going to create the like button. Luckily a lot of the hard work has been done for us on the backend with the like
mutation so now we just need to use it on the frontend.
components/itemLike.tsx
tsx
import { useMutation, useQuery } from '@apollo/client'import { useFetchUser } from '../utils/user'import * as React from 'react'import { BundleObject, FeedObject, ItemType } from '../utils/types'import {LIKE_BUNDLE_MUTATION,LIKE_FEED_MUTATION,} from '../utils/api/graphql/mutations'import { ME_QUERY } from '../utils/api/graphql/queries'import * as _ from 'lodash'import { HeartOutline } from './svg'export const ItemLike = ({item,type,}: {item: FeedObject | BundleObjecttype: ItemType}) => {const isFeed = type === ItemType.FeedTypeconst [likeItemMutation, { loading: likeItemLoading }] = useMutation(isFeed ? LIKE_FEED_MUTATION : LIKE_BUNDLE_MUTATION)const { user, loading } = useFetchUser()const likeMatches = item.likes.filter(oneLike => oneLike.auth0 === (user ? user.sub : ''))const hasMatch = likeMatches.length > 0 ? true : falsereturn (<divonClick={e => {e.stopPropagation()if (user) {const idObj = isFeed ? { feedId: item.id } : { bundleId: item.id }likeItemMutation({variables: {data: {...idObj,likeState: hasMatch ? false : true,},},})}}}className="flex py-2 mx-1 z-10"><p>{item.likes.length} </p><HeartOutlineclassName={`h-6 w-6 ${hasMatch ? `text-red-500` : `text-gray-500`} inline-block align-middle`}/></div>)}
The main visual elements of this component are a <p>
that shows the number of likes which we get from the length of the likes array and the HeartOutline
component which will be colored or not based on whether someone has liked it. What's nice about Tailwind is that we can switch between gray
and red
just by changing the classes.
We need to finally add the HeartOutline
svg to the long list of other svgs that we've already made.
components/svg.tsx
tsx
// many more svgsexport const HeartOutline = ({ className }) => (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"stroke="currentColor"className={className}><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>)
Let's add this to the OneListItem
component.
components/oneListItem.tsx
tsx
// other importsimport { ItemLike } from './itemLike'//more of the react component;<div className="col-span-2 flex justify-end"><ItemLike item={item} type={type} />{canManipulate ? (<ItemEdititem={item}type={type}selected={selected}setSelected={setSelected}/>) : null}{canManipulate ? <ItemDelete item={item} type={type} /> : null}</div>
Try it out and verify that when you click the heart it loads and then shows a red heart with a 1 next to the item. Then if you click on it again, it should turn back to gray and the count should decrease by 1.
We can see that there is a little bit of a delay and that's because we haven't added an optimistic response yet- let's start by adding an update function.
components/itemLike.tsx
tsx
// more imports aboveimport { updateCache } from '../utils/update'// more code abovelikeItemMutation({variables: {data: {...idObj,likeState: hasMatch ? false : true,},},update: updateCache(isFeed, 'like'),})
We notice that the behavior changes the behavior slightly- now why is that? If we open the updateCache
function we can see that the feeds/bundles
query works by removing the item and adding the updated one on the end. Now it becomes clear why we see that item go to the end when we modify it. What if we just disabled the updateCache
function alltogether for the likes
component? We already know that it works- apollo is smart enough to know how to update the cache for update functionality, it is just the new item that it has trouble with. Let's try disabling the updateCache
from the update functionality in the NewEditItem
component:
components/newEditItem.tsx
tsx
// remove this line:import { updateCache } from '../utils/update'// some more code// modify this:selected.editMode? updateItemMutation({variables: { data },optimisticResponse: optimisticCache(isFeed,'update',data,currentItem,meData),}): createItemMutation({variables: { data },optimisticResponse: optimisticCache(isFeed,'create',data,currentItem,meData),update: updateCache(isFeed, 'create'),})
We can see that the optimisticResponse is still there but the update is missing just for the updateItemMutation
. Now when we try editing an existing feed or bundle it still works just as before. The best code is no code, right? Let's leave that out so the only place we are using the updateCache
is for the createItemMutation
.
Let's add the optimisticResponse
to the ItemLike
component.
components/itemLike.tsx
tsx
import { useMutation, useQuery } from '@apollo/client';// more importsimport { ME_QUERY } from '../utils/api/graphql/queries';export const ItemLike = ({ item, type }: { item: FeedObject | BundleObject; type: ItemType }) => {const isFeed = type === ItemType.FeedType;const [likeItemMutation, { loading: likeItemLoading }] = useMutation(isFeed ? LIKE_FEED_MUTATION : LIKE_BUNDLE_MUTATION);const { data: meData, loading: userLoadingQuery } = useQuery(ME_QUERY);const { user, loading } = useFetchUser();const loading = likeItemLoading || userLoadingQuery || fetchUserLoading;const likeMatches = item.likes.filter(oneLike => oneLike.auth0 === (user ? user.sub : ''));const hasMatch = likeMatches.length > 0 ? true : false;const me = _.get(meData, 'me');//more of the componentonClick={e => {e.stopPropagation();if (user && !loading) {const idObj = isFeed ? { feedId: item.id } : { bundleId: item.id };likeItemMutation({variables: {data: {...idObj,likeState: hasMatch ? false : true,},},optimisticResponse: () => {const likes = item.likes.filter(item => (item.id === me ? me.id : ''));return {__typename: 'Mutation',[`like${isFeed ? 'Feed' : 'Bundle'}`]: {...item,...(hasMatch? {likes,}: {likes: [...likes, me],}),},};},});}}}
How the optimisticResponse
works is that we create a likes array where we filter out the current user's like. We use this as the new likes array if the user unlikes an item. Conversely, if the user is liking the item, then we add them to the end. Note that since we can either like or unlike, those actions are either adding or removing the user from the likes array. There is no operation where we'd be finding a user and updating their information, so that's why we can simply filter the item out and then add it on the end- we'd only be doing that in the event that a user is liking an item. This is why we won't have to worry about the bad array re-ordering behavior that we saw when we were updating the items array.