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.
24. Finish create item functionality
Objective: Hook up the create component to our backend with a mutation.
First, let's get a handle on what our currentItem
looks like. Add a console log in the newEditItem.tsx
file in the onSubmit
handler right after the e.preventDefault()
call. Then create a new bundle and add a name, description, and add new tags and feeds both existing and in the case of tags add a new one too. Press the submit button and look at what the console.log looks like.
We should see something like this. Here, we added one feed and two tags: the first tag was a new tag called "my new tag" while the second tag was an already existing tag called "World News".
What we need here is a function that can convert this object into something that matches what the createBundle mutation needs to create a new bundle, the new bundleTag named "my new tag", along with the connection to the feed with the name "CNN Feed".
js
{name: "New Item",description: "Description for the new item",feeds: [{__typename: "Feed",bundles: Array(2),id: "1",name: "CNN Feed",url: "http://rss.cnn.com/rss/cnn_topstories.rss",author: { } //Author information,... more feed information removed for clarity}],tags: [{ name: "my new tag" },{__typename: "BundleTag", id: "201", name: "World News"}]}
Let's do this in a new function called prepareUpdateObj
. We will eventually also use this same function for our update mutation, but for now it will just be for the create new functionality.
The main function doing all the work here is called prepareUpdateObj
. It creates two variables called tags
and feeds
and it will iterate through the current feed or bundle. While iterating it will look at each item on the list and see if an id
is set. If there is an id
we know that the item has already been created and so instead of creating a new item we only have to connect
it to the already existing one by its id
. We use the filter
function to either filter out items that don't have an id
(for the create
) or do have an id
(for the connect
).
Next, for new items we add an additional map
to specify a uuid for each of them. Once we have created the feeds and tags objects, we add them to the existing attributes for the feed/bundle as well as an id
that we create from uuid
and we are good to go.
utils/prepareUpdateObj.ts
tsx
import { v4 as uuidv4 } from 'uuid'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, ...cleanedItem } = currentItemreturn { ...cleanedItem, ...tags, ...feeds }}export const prepareNewUpdateObj = currentItem => {const currentData = genNestedItems(currentItem)return { ...currentData, id: uuidv4() }}
Next we will create a function that is in charge of taking the data that we get back from either the mutation or the optimistic update and save it to the apollo cache. We will call the writeQuery
function from the cache and write 2 new queries- a feed/bundle
query and a feeds/bundles
query. We have to write it in both places so that any of the react components which are using those two queries both know to update their data.
utils/update.ts
tsx
import {BUNDLES_QUERY,BUNDLE_QUERY,FEEDS_QUERY,FEED_QUERY,} from './api/graphql/queries'import * as _ from 'lodash'export const updateCache = (isFeed, action) => (store, { data }) => {const item = data[`${action}${isFeed ? 'Feed' : 'Bundle'}`]try {store.writeQuery({query: isFeed ? FEED_QUERY : BUNDLE_QUERY,variables: { data: { id: _.get(item, 'id') } },data: { [isFeed ? 'feed' : 'bundle']: item },})} catch (e) {}try {const { feeds, bundles } = store.readQuery({query: isFeed ? FEEDS_QUERY : BUNDLES_QUERY,})const currentItems = isFeed ? feeds : bundlesstore.writeQuery({query: isFeed ? FEEDS_QUERY : BUNDLES_QUERY,data: {[isFeed ? 'feeds' : 'bundles']: [...currentItems.filter(o => o.id !== item.id),item,],},})} catch (e) {}}
components/newEditItem.tsx
tsx
import { useMutation, useQuery } from '@apollo/client'import { Dispatch, SetStateAction, useState } from 'react'import * as _ from 'lodash'import {CREATE_BUNDLE_MUTATION,CREATE_FEED_MUTATION,} from '../utils/api/graphql/mutations'import {FIND_BUNDLE_TAGS_QUERY,FIND_FEEDS_QUERY,FIND_FEED_TAGS_QUERY,ME_QUERY,} from '../utils/api/graphql/queries'import { prepareNewUpdateObj } from '../utils/prepareUpdateObj'import {ActionType,BadgeFieldName,BundleState,FeedState,ItemType,NewItemState,SearchQueryName,SelectedFeedState,} from '../utils/types'import { BadgeList } from './badgeList'import { GenerateInputField } from './generateInputField'import { SearchItems } from './searchItems'import { ErrorSign, WaitingClock } from './svg'import { optimisticCache } from '../utils/optimisticCache'import { updateCache } from '../utils/update'export const NewEditItem = ({type,setSelected,selected,}: {type: ItemTypesetSelected?: Dispatch<SetStateAction<SelectedFeedState>>selected: SelectedFeedState}) => {const isFeed = type === ItemType.FeedTypeconst initialFeed: FeedState = { name: '', url: '', tags: [] }const initialBundle: BundleState = {name: '',description: '',tags: [],feeds: [],}const initialState: NewItemState = isFeed ? initialFeed : initialBundleconst inputFields = isFeed ? ['name', 'url'] : ['name', 'description']const [currentItem, setItem] = useState<NewItemState>(initialState)const [createItemMutation,{ loading: createLoading, error: createError },] = useMutation(isFeed ? CREATE_FEED_MUTATION : CREATE_BUNDLE_MUTATION)const { data: meData, loading: meLoading, error: meError } = useQuery(ME_QUERY)if (createLoading) {return <WaitingClock className="my-20 h-10 w-10 text-gray-500 m-auto" />}if (createError) {return <ErrorSign className="my-20 h-10 w-10 text-gray-500 m-auto" />}return (<><formonSubmit={e => {e.preventDefault()const data = prepareNewUpdateObj(currentItem)createItemMutation({variables: { data },optimisticResponse: optimisticCache(isFeed,'create',data,currentItem,meData),update: updateCache(isFeed, 'create'),})setItem(initialState)setSelected(currState => ({...currState,editMode: false,newMode: false,}))}}><div className="grid grid-cols-12 gap-4 rounded-md border-4 my-4 py-2 px-4"><h3 className="col-span-12 text-lg font-medium py-2">{isFeed ? `New Feed` : `New Bundle`}</h3><div className="col-span-6">{inputFields.map(name => (<GenerateInputFieldkey={`${type}-${name}`}currentItem={currentItem}name={name}changeHandler={setItem}/>))}<div className={`py-4 ${isFeed ? null : `pt-28`}`}><inputclassName={`py-4 ${`bg-${isFeed ? 'green' : 'purple'}-400`} hover:bg-${isFeed ? 'green' : 'purple'}-700 text-white font-bold px-12 rounded`}type="submit"/></div></div><div className="col-span-6"><div className="py-2"><label className="block py-2">Tags:</label><div className="grid grid-cols-3 gap-2"><BadgeListfieldName={BadgeFieldName.tags}action={ActionType.CREATE}setItem={setItem}item={currentItem}/></div></div><div className="py-2"><label className="block py-2">Add New Tag:</label><SearchItemsqueryName={isFeed? SearchQueryName.findFeedTags: SearchQueryName.findBundleTags}query={isFeed ? FIND_FEED_TAGS_QUERY : FIND_BUNDLE_TAGS_QUERY}setItem={setItem}currentItem={currentItem}fieldName={BadgeFieldName.tags}/></div>{isFeed ? null : (<><div className="py-2"><label className="block py-2">Feeds:</label><div className="grid grid-cols-3 gap-2"><BadgeListfieldName={BadgeFieldName.feeds}action={ActionType.CREATE}setItem={setItem}item={currentItem}/></div></div><div className="py-2"><label className="block py-2">Add New Feed:</label><SearchItemsqueryName={SearchQueryName.findFeeds}query={FIND_FEEDS_QUERY}setItem={setItem}currentItem={currentItem}fieldName={BadgeFieldName.feeds}/></div></>)}</div></div></form></>)}
You'll notice is that we are using the function setItem
which means that we'll need to add them as props to <NewEditItem>
and then pass them in on the bundles and feeds pages.
pages/bundles.tsx
tsx
// replace this line:return (// more code above{(selected.editMode || selected.newMode) && user ? (<NewEditItem type={ItemType.BundleType} />) : null})
tsx
// with these lines:return (// more code above{(selected.editMode || selected.newMode) && user ? (<NewEditItemtype={ItemType.BundleType}selected={selected}setSelected={setSelected}/>) : null})
pages/feeds.tsx
tsx
// replace this line:return (// more code above{(selected.editMode || selected.newMode) && user ? (<NewEditItem type={ItemType.FeedType} />) : null})
tsx
// with these lines:return (// more code above{(selected.editMode || selected.newMode) && user ? (<NewEditItemtype={ItemType.FeedType}selected={selected}setSelected={setSelected}/>) : null})
We also need to write the optimisticCache function for our mutation. The goal here is to write a response that matches what we get back from the server. We can console.log the response in the update function to see that what we get back is an object of the newly created item that is populated with the author, tags, and feeds if applicable. We can optimistically create this by making a function called optimisticCache
which takes the data
object that we are sending to the server which has all of the uuids in it and meld it with the current item which has all of the nested tags and feed information before we create our data
object. We have to also assign the author information which we get by making a me
query.
This is a big of leg-work, but the result is that when someone goes to create their item, we will instantly see it in the list of items and then it will update a split second later with the actual data that gets returned from the server. Awesome!
utils/optimisticCache.tsx
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')),...data.tags.create.map(tag => ({__typename: isFeed ? 'FeedTag' : 'BundleTag',...tag,})),],...(isFeed? {}: {feeds: currentItem.feeds,}),author: me,}return {__typename: 'Mutation',[action + __typename]: {__typename,...response,},}}