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 | BundleObjecttype: ItemTypeselected?: SelectedFeedStatesetSelected?: Dispatch<SetStateAction<SelectedFeedState>>}) => {const isFeed = type === ItemType.FeedTypereturn (<divonClick={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"><EditPencilclassName={`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 aboveexport const EditPencil = ({ 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="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 importsimport { 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 importsimport { optimisticCache } from '../utils/optimisticCache'import { updateCache } from '../utils/update'// createItemMutation loading hook aboveconst [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 : bundleuseEffect(() => {;(async () => {if (item && selected.editMode) {const { __typename, likes, author, ...cleanedItem } = itemsetItem({ ...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 } = currentItemreturn { ...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'] = connectedTagscurrentData.tags['disconnect'] = disconnectedTagsif (!isFeed) {const disconnectedFeeds = _.differenceWith(queriedData.feeds.connect,currentData.feeds.connect,_.isEqual)const connectedFeeds = _.differenceWith(currentData.feeds.connect,queriedData.feeds.connect,_.isEqual)currentData.feeds['connect'] = connectedFeedscurrentData.feeds['disconnect'] = disconnectedFeedsreturn 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 } = dataconst __typename = isFeed ? 'Feed' : 'Bundle'const { me } = meDataconst 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 aboveconst 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.