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 | BundleObject
    type: ItemType
    }) => {
    const isFeed = type === ItemType.FeedType
    const [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 : false
    return (
    <div
    onClick={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>
    <HeartOutline
    className={`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 svgs
    export const HeartOutline = ({ className }) => (
    <svg
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
    className={className}
    >
    <path
    strokeLinecap="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 imports
    import { ItemLike } from './itemLike'
    //more of the react component
    ;<div className="col-span-2 flex justify-end">
    <ItemLike item={item} type={type} />
    {canManipulate ? (
    <ItemEdit
    item={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 above
    import { updateCache } from '../utils/update'
    // more code above
    likeItemMutation({
    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 imports
    import { 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 component
    onClick={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.