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 } = currentItem
    return { ...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 : bundles
    store.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: ItemType
    setSelected?: Dispatch<SetStateAction<SelectedFeedState>>
    selected: SelectedFeedState
    }) => {
    const isFeed = type === ItemType.FeedType
    const initialFeed: FeedState = { name: '', url: '', tags: [] }
    const initialBundle: BundleState = {
    name: '',
    description: '',
    tags: [],
    feeds: [],
    }
    const initialState: NewItemState = isFeed ? initialFeed : initialBundle
    const 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 (
    <>
    <form
    onSubmit={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 => (
    <GenerateInputField
    key={`${type}-${name}`}
    currentItem={currentItem}
    name={name}
    changeHandler={setItem}
    />
    ))}
    <div className={`py-4 ${isFeed ? null : `pt-28`}`}>
    <input
    className={`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">
    <BadgeList
    fieldName={BadgeFieldName.tags}
    action={ActionType.CREATE}
    setItem={setItem}
    item={currentItem}
    />
    </div>
    </div>
    <div className="py-2">
    <label className="block py-2">Add New Tag:</label>
    <SearchItems
    queryName={
    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">
    <BadgeList
    fieldName={BadgeFieldName.feeds}
    action={ActionType.CREATE}
    setItem={setItem}
    item={currentItem}
    />
    </div>
    </div>
    <div className="py-2">
    <label className="block py-2">Add New Feed:</label>
    <SearchItems
    queryName={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 ? (
    <NewEditItem
    type={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 ? (
    <NewEditItem
    type={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 } = 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')),
    ...data.tags.create.map(tag => ({
    __typename: isFeed ? 'FeedTag' : 'BundleTag',
    ...tag,
    })),
    ],
    ...(isFeed
    ? {}
    : {
    feeds: currentItem.feeds,
    }),
    author: me,
    }
    return {
    __typename: 'Mutation',
    [action + __typename]: {
    __typename,
    ...response,
    },
    }
    }