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.


    23. Add SearchItems component

    Objective: Create a component for searching for new tags and feeds to add to an item.

    In this component we will create a component that has a search input field that as we type stuff into it, it will send queries to the backend to find tags or feeds that match the search string based on their names. For hits that they find, it will create a list that will allow people to add them to the list of items for this new feed or bundle that we are creating.

    We store the search string in a state variable called search and we set it using the setSearch function. We've used queries before, but in this component we will use a different form of it called a lazy query. Lazy queries only get called manually- in a similar way to how the mutation works. This is perfect because we will call the query with the current state in search every time that the onChange handler gets executed by the input field.

    In the event that there is an error or it is loading, we will display ther loading/error icons and this particular loading icon is a little special because we'll actually animate it with a spin. Tailwind makes this really easy just by adding the animate-spin class.

    components/searchItems.tsx

    tsx

    import { Dispatch, SetStateAction, useState } from 'react'
    import { DocumentNode, useLazyQuery } from '@apollo/client'
    import * as _ from 'lodash'
    import {
    ActionType,
    BadgeFieldName,
    BundleState,
    FeedState,
    SearchQueryName,
    } from '../utils/types'
    import { BadgeList } from './badgeList'
    import { Search, Spin } from './svg'
    export const SearchItems = ({
    currentItem,
    setItem,
    queryName,
    query,
    fieldName,
    }: {
    currentItem: FeedState | BundleState
    setItem: Dispatch<SetStateAction<FeedState | BundleState>>
    queryName: SearchQueryName
    query: DocumentNode
    fieldName: BadgeFieldName
    }) => {
    const [search, setSearch] = useState('')
    const [findItemsQuery, { loading, data, called }] = useLazyQuery(query, {
    fetchPolicy: 'network-only',
    })
    const fetchedItems = _.get(data, queryName)
    const filtFindItems = fetchedItems
    ? fetchedItems.filter(
    oneItem =>
    !currentItem[fieldName].map(o => o.name).includes(oneItem.name)
    )
    : []
    const matchCurrent = filtFindItems.filter(o => o.name === search)
    const matchList = currentItem[fieldName].filter(o => o.name === search)
    const filtFindItemsWithAdd =
    matchCurrent.length === 0 &&
    matchList.length === 0 &&
    queryName !== 'findFeeds'
    ? [...filtFindItems, { name: search }]
    : filtFindItems
    const dummyNewItem = { ...currentItem, [fieldName]: filtFindItemsWithAdd }
    return (
    <div className="">
    <div className="flex">
    {loading ? (
    <Spin className="h-6 w-6 text-gray-500 animate-spin" />
    ) : (
    <Search className="mt-3 mr-2 w-6 h-6 text-gray-500" />
    )}
    <input
    className="border-4 rounded w-full py-2 px-3"
    value={search}
    onChange={e => {
    e.persist()
    if (e.target.value !== search) {
    setSearch(() => e.target.value)
    findItemsQuery({
    variables: { data: { search: e.target.value } },
    })
    }
    }}
    />
    </div>
    <div className="grid grid-cols-3 gap-2 flex m-2">
    {search !== '' ? (
    <BadgeList
    fieldName={fieldName}
    action={ActionType.ADD}
    setItem={setItem}
    item={dummyNewItem}
    />
    ) : called ? (
    <p className="text-gray-400">No matches</p>
    ) : null}
    </div>
    </div>
    )
    }

    When the query gets executed we get the data back as a data parameter and we have to then selectively pull the bundleTag, feedTag, or feed array and build a list of the items called filtFindItems. Critically, we omit any matches that we already have added to our list. This will ensure that, for example if someone added the feedTag news to the list within currentItem, that if they search for n in the future we will omit news from the list of matches so they won't add the same tag twice. Finally to that filtered list, we will add a new item on the end that will allow people to add a tag that matches the current search string so that people can create new tags and they aren't limited to the ones that already exist.

    Once the list has been created, we call the <BadgeList> component and this time the action that we pass in is ADD. Since <BadgeList> requires an item, we will create a dummy one which matches the current item's properties but the array of either tags or feeds matches the current list that we just fetched.

    components/svg.tsx

    tsx

    // more svgs above
    export const Spin = ({ className }) => (
    <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 20 20"
    fill="currentColor"
    className={className}
    >
    <path
    fillRule="evenodd"
    d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
    clipRule="evenodd"
    />
    </svg>
    )
    export const Search = ({ 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="M8 16l2.879-2.879m0 0a3 3 0 104.243-4.242 3 3 0 00-4.243 4.242zM21 12a9 9 0 11-18 0 9 9 0 0118 0z"
    />
    </svg>
    )

    utils/types.ts

    tsx

    // more types above
    export enum SearchQueryName {
    findFeedTags = 'findFeedTags',
    findBundleTags = 'findBundleTags',
    findFeeds = 'findFeeds',
    }

    components/newEditItem.tsx

    tsx

    import { useMutation } from '@apollo/client'
    import { useState } from 'react'
    import {
    CREATE_BUNDLE_MUTATION,
    CREATE_FEED_MUTATION,
    } from '../utils/api/graphql/mutations'
    import {
    FIND_BUNDLE_TAGS_QUERY,
    FIND_FEEDS_QUERY,
    FIND_FEED_TAGS_QUERY,
    } from '../utils/api/graphql/queries'
    import {
    ActionType,
    BadgeFieldName,
    BundleState,
    FeedState,
    ItemType,
    NewItemState,
    SearchQueryName,
    } from '../utils/types'
    import { BadgeList } from './badgeList'
    import { GenerateInputField } from './generateInputField'
    import { SearchItems } from './searchItems'
    import { ErrorSign, WaitingClock } from './svg'
    export const NewEditItem = ({ type }: { type: ItemType }) => {
    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)
    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()
    }}
    >
    <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>
    </>
    )
    }

    Now let's see how well our <SearchItems> component is working! We'll see that there is a search feed and when we type in it, we should see the loading logo right after we type and it should switch to the magnifying glass after it returns the result. If we have some feed or bundle tags in our database from the previous create queries we ran, we should see that if we type in characters that would result in a match we will see the match as well as a tag that matches the current search term.

    This basically looks like it is working but there is one missing piece. This is adding functionality to the oneBadge component so that we can either add or remove a badge from the list of items attached to that feed/bundle that we are creating.

    In the <OneBadge> component add the following checks for an ActionType of ADD or CREATE. We use the ADD on the list of items that we can add to the add/update list and we use the CREATE functionality on the list of items that are already added on the list that are associated with the feed or bundle.

    components/oneBadge.tsx

    tsx

    import { BundleTag, FeedTag } from '@prisma/client'
    import { Dispatch, SetStateAction } from 'react'
    import {
    ActionType,
    BadgeFieldName,
    BundleState,
    FeedObject,
    FeedState,
    } from '../utils/types'
    import { Minus, Plus } from './svg'
    export const OneBadge = ({
    item,
    action,
    currentItem,
    fieldName,
    setItem,
    }: {
    item: FeedTag | BundleTag | FeedObject
    action: ActionType
    currentItem?: FeedState | BundleState
    fieldName: BadgeFieldName
    setItem?: Dispatch<SetStateAction<FeedState | BundleState>>
    }) => {
    const color =
    fieldName === BadgeFieldName.tags
    ? `blue`
    : fieldName === BadgeFieldName.feeds
    ? `green`
    : `purple`
    return (
    <div className="inline-block align-middle">
    <span
    className={`flex justify-center text-sm py-2 px-2 rounded-lg bg-${color}-200`}
    >
    {action === ActionType.ADD ? (
    <div
    onClick={() => {
    setItem(feed => ({
    ...feed,
    [fieldName]: [...feed[fieldName], { ...item }],
    }))
    }}
    >
    <Plus className="h-4 w-4 text-gray-500" />
    </div>
    ) : null}
    {action === ActionType.CREATE ? (
    <div
    onClick={() => {
    setItem(feed => ({
    ...feed,
    [fieldName]: feed[fieldName].filter(o => item.name !== o.name),
    }))
    }}
    >
    <Minus className="h-4 w-4 text-gray-500" />
    </div>
    ) : null}
    <p className={`text-xs text-${color}-600 text-center`}>{item.name}</p>
    </span>
    </div>
    )
    }

    Now, when we play around with the search field we will see badges with the ability to either add or remove items from our list. One distinction you should see is that although tags allow you to create new tags on the fly, for feeds you can only add existing feeds to the list. We are making this distinctiong to force people to make feeds that they want ahead of time in order to add them to the bundle. This is because although tags only have one property- their name, feeds have many more fields which would make it more complicated to add everything in this compact ui.

    This is a good place to stop this step- we have now the ability to add text fields, feeds, and tags to our feed or bundle item but everything is being stored in state. Next, we will build a mutation so that we can submit all this data to our backend and actually add it to the database.