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.


    25. Add update existing item functionality

    Objective: Add update functionality to the create component.

    Now that we have the create functionality working, let's turn to the update functionality of the <NewEditItem> component. Our strategy will be to have an edit button on each bundle or feed that someone owns and when it is clicked, we will populate the information into the <NewEditItem> with that information.

    This button that we will click is called <ItemEdit>, so let's make that first. It is a button that when clicked will set the current selected item to the item that is clicked and we will turn on the edit mode.

    components/itemEdit.tsx

    tsx

    import { Dispatch, SetStateAction } from 'react'
    import {
    BundleObject,
    FeedObject,
    ItemType,
    SelectedFeedState,
    } from '../utils/types'
    import { EditPencil } from './svg'
    export const ItemEdit = ({
    item,
    type,
    selected,
    setSelected,
    }: {
    item: FeedObject | BundleObject
    type: ItemType
    selected?: SelectedFeedState
    setSelected?: Dispatch<SetStateAction<SelectedFeedState>>
    }) => {
    const isFeed = type === ItemType.FeedType
    return (
    <div
    onClick={e => {
    e.stopPropagation()
    setSelected(currState => ({
    id: item.id,
    feeds: isFeed ? [item] : item['feeds'],
    editMode:
    !selected.editMode || currState.id !== item.id ? true : false,
    newMode: false,
    }))
    }}
    className="flex py-2 mx-1 z-10"
    >
    <EditPencil
    className={`h-6 w-6 ${
    item.id === selected.id && selected.editMode
    ? `text-${isFeed ? 'green' : 'purple'}-400`
    : `text-gray-500`
    } inline-block align-middle`}
    />
    </div>
    )
    }

    components/svg.ts

    tsx

    // other svgs above
    export const EditPencil = ({ 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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
    />
    </svg>
    )

    Now we need to load the <ItemEdit> into the <OneListItem> component. We define a boolean called canManipulate that will tell us whether the <ItemEdit> component should be present. Everything should not be loading and the current user should match the owner.

    components/oneListItem.tsx

    tsx

    // other imports
    import { ItemEdit } from './itemEdit';
    const canManipulate = !loading && user && item.author.auth0 === user.sub && useSelected && allowEdits;
    return (
    // more of the component above
    <div className="col-span-2 flex justify-end">
    {canManipulate ? <ItemEdit item={item} type={type} selected={selected} setSelected={setSelected} /> : null}
    </div>

    Now we need to update the <NewEditItem> component so that we can load the selected bundle or feed and insert that data into the fields. We will use the updateItemMutation handler which is loaded either with the update feed or update bundle mutation.

    We will load that information that it finds into the fields by using the useEffect hook. How it works is any time itemQueryData changes, it will call an async function that will set the updated fields with the data inside item.

    components/newEditItem.tsx

    tsx

    import { Dispatch, SetStateAction, useEffect, useState } from 'react'
    import * as _ from 'lodash'
    import {
    CREATE_BUNDLE_MUTATION,
    CREATE_FEED_MUTATION,
    UPDATE_BUNDLE_MUTATION,
    UPDATE_FEED_MUTATION,
    } from '../utils/api/graphql/mutations'
    import {
    BUNDLE_QUERY,
    FEED_QUERY,
    FIND_BUNDLE_TAGS_QUERY,
    FIND_FEEDS_QUERY,
    FIND_FEED_TAGS_QUERY,
    ME_QUERY,
    } from '../utils/api/graphql/queries'
    // other imports
    import { optimisticCache } from '../utils/optimisticCache'
    import { updateCache } from '../utils/update'
    // createItemMutation loading hook above
    const [
    updateItemMutation,
    { loading: updateLoading, error: updateError },
    ] = useMutation(isFeed ? UPDATE_FEED_MUTATION : UPDATE_BUNDLE_MUTATION)
    const variables = { data: { id: selected.id ? selected.id : '' } }
    const {
    loading: itemQueryLoading,
    error: itemQueryError,
    data: itemQueryData,
    } = useQuery(isFeed ? FEED_QUERY : BUNDLE_QUERY, {
    variables,
    })
    const { bundle, feed } = itemQueryData || {}
    const item = isFeed ? feed : bundle
    useEffect(() => {
    ;(async () => {
    if (item && selected.editMode) {
    const { __typename, likes, author, ...cleanedItem } = item
    setItem({ ...cleanedItem })
    } else {
    setItem(initialState)
    }
    })()
    }, [itemQueryData])
    if (createLoading || updateLoading || itemQueryLoading) {
    return <WaitingClock className="my-20 h-10 w-10 text-gray-500 m-auto" />
    }
    if (createError || updateError || itemQueryError) {
    return <ErrorSign className="my-20 h-10 w-10 text-gray-500 m-auto" />
    }
    //main return of component below

    Try clicking from feed or bundle to bundle by th edit button and we'll see that the fields all get populated with the actual data from the selected item.

    Now we we need to onSubmit function to handle either calling the createItemMutation function or the updateItemMutation function depending on which action is being done. Before we can call the mutation, we need to update the prepareNewUpdateObj function to add update functionality to it.

    First, we need to pass isEditing into the function, which will tell us if we are editing the field or creating a new one, and in the new case we will return the same object from the function as we did previously. In the event that we are editing the function, we need to do some more work before we will have the update object prepared.

    For editing a feed or bundle, in the event that we are creating new tags, that process is the same- we just load them as an array into the create object and it is no problem. For tags that already exist we have to do something a little different. Tags that need to be added use the connect object while tags that are being removed need to be under the disconnect object. We utilize lodash's differenceWith function to look at the difference between the update object (currentData) and the current state (queriedData). differenceWith takes 2 parameters and depending on the order you get different results because you can think of this function as kind of taking a subtraction between these two sets of tags. Putting the current data first, followed by the new data will tell us which tags need to be disconnected while putting the new data second will tell us which tags need to be connected. We prepare those 2 arrays of tags and put them into the connect and disconnect arrays for the update object.

    The next thing we need to do is check for whether this object is a bundle, which we check with !isFeed. In that case we need to perform the same business that we did with tags for the connected feeds. Due to the structure of this app, we only allow people to add or remove feeds from bundles not the other way around. This is why we don't need a similar block of code for managing bundle connections- that behavior is not allowed via editing a feed.

    Here is what the final prepareNewUpdateObj file looks like:

    utils/prepareNewUpdateObj.ts

    tsx

    import { v4 as uuidv4 } from 'uuid'
    import * as _ from 'lodash'
    const cleanOps = (currentData, items) => {
    items.map(oneItem => {
    ;['connect', 'disconnect', 'create'].map(op => {
    if (op in currentData[oneItem]) {
    currentData[oneItem][op].length === 0
    ? delete currentData[oneItem][op]
    : null
    }
    })
    if (_.isEmpty(currentData[oneItem])) {
    delete currentData[oneItem]
    }
    })
    return currentData
    }
    const genNestedItems = currentItem => {
    const tags =
    'tags' in currentItem
    ? {
    tags: {
    connect: currentItem.tags
    .map(({ id }) => ({ id }))
    .filter(({ id }) => id !== undefined),
    create: currentItem.tags
    .filter(({ id }) => id === undefined)
    .map(o => ({ ...o, id: uuidv4() })),
    },
    }
    : {}
    const feeds =
    'feeds' in currentItem
    ? {
    feeds: {
    connect: currentItem.feeds
    .map(({ id }) => ({ id }))
    .filter(({ id }) => id !== undefined),
    },
    }
    : {}
    const { __typename, likes, author, bundles, ...cleanedItem } = currentItem
    return { ...cleanedItem, ...tags, ...feeds }
    }
    export const prepareNewUpdateObj = (
    queriedItem,
    currentItem,
    isFeed,
    isEditing
    ) => {
    const currentData = genNestedItems(currentItem)
    if (!isEditing) {
    return { ...currentData, id: uuidv4() }
    }
    const queriedData = genNestedItems(queriedItem)
    const disconnectedTags = _.differenceWith(
    queriedData.tags.connect,
    currentData.tags.connect,
    _.isEqual
    )
    const connectedTags = _.differenceWith(
    currentData.tags.connect,
    queriedData.tags.connect,
    _.isEqual
    )
    currentData.tags['connect'] = connectedTags
    currentData.tags['disconnect'] = disconnectedTags
    if (!isFeed) {
    const disconnectedFeeds = _.differenceWith(
    queriedData.feeds.connect,
    currentData.feeds.connect,
    _.isEqual
    )
    const connectedFeeds = _.differenceWith(
    currentData.feeds.connect,
    queriedData.feeds.connect,
    _.isEqual
    )
    currentData.feeds['connect'] = connectedFeeds
    currentData.feeds['disconnect'] = disconnectedFeeds
    return cleanOps(currentData, ['feeds', 'tags'])
    } else {
    return cleanOps(currentData, ['tags'])
    }
    }

    Now that our prepareNewUpdateObj has been updated, lets update the optimisticCache function. We need to use the _.get function so that we can gracefully handle situations where tags.create does not exist- this would be when we make an edit that doesn't add new tags.

    utils/optimisticCache.ts

    tsx

    import * as _ from 'lodash'
    export const optimisticCache = (isFeed, action, data, currentItem, meData) => {
    const { id } = data
    const __typename = isFeed ? 'Feed' : 'Bundle'
    const { me } = meData
    const response = {
    id,
    ...currentItem,
    likes: [],
    [isFeed ? 'bundles' : 'feeds']: [],
    tags: [
    ...currentItem.tags.filter(tag => _.has(tag, 'id')),
    ..._.get(data, 'tags.create', []).map(tag => ({
    __typename: isFeed ? 'FeedTag' : 'BundleTag',
    ...tag,
    })),
    ],
    ...(isFeed
    ? {}
    : {
    feeds: currentItem.feeds,
    }),
    author: me,
    }
    return {
    __typename: 'Mutation',
    [action + __typename]: {
    __typename,
    ...response,
    },
    }
    }

    Finally, we are ready to update the onSubmit handler:

    components/newEditItem.tsx

    tsx

    // code is above
    const data = prepareNewUpdateObj(item, currentItem, isFeed, selected.editMode)
    selected.editMode
    ? updateItemMutation({
    variables: { data },
    optimisticResponse: optimisticCache(
    isFeed,
    'update',
    data,
    currentItem,
    meData
    ),
    update: updateCache(isFeed, 'update'),
    })
    : createItemMutation({
    variables: { data },
    optimisticResponse: optimisticCache(
    isFeed,
    'create',
    data,
    currentItem,
    meData
    ),
    update: updateCache(isFeed, 'create'),
    })

    We now will just select the mutation based on whether we are editing or not.

    With that, we are in really good shape. We have the ability to create or update feeds/bundles and we can add or remove tags from existing items. That's quite a bit of functionality that we've crammed into a single NewEditItem component.