Refresh a component after a CRUD operation

Hello! I keep my question generic, then I’ll go into more details if needed.

I have a table with entries and some action buttons (among which there is one to delete an entry), then a button to add a new entry. Now, when I use the form to save the data, the entry gets inserted on the db side, however on the frontend I have to refresh the page to see the change. Same goes if I delete an entry. How is this handled in Redwood?

I’ve also not quite worked this out, I have something similar, although my items get created on a separate page.

I have got it working that when I update an item from the list the table of items updates, and this depends on making sure that my mutation retrieves the fields that have changed (in this case status, and endActualDateTime.

const COMPLETE_SESSION_MUTATION = gql`
  mutation CompleteHireSessionMutation(
    $id: String!
    $input: UpdateHireSessionInput!
  ) {
    completeHireSession(id: $id, input: $input) {
      id
      endActualDateTime
      status
    }
  }

In this example here, when I click the button an item to update it’s status, the status field updates automatically.

I’ve not worked out how to get it to update the table automatically when an item is deleted though, that still needs a refresh of the page (maybe I need to do something to re-fetch the whole list, or do soft deletes).

Is your form and list on the same page?

Yes, but not on the same component. I have on the page the Cell which fetches and renders the data, then below it the button to add (which only opens the modal) and the modal with the form and the component. The cell renders a component (defined) and passes down the results of the fetching query as a prop to Success (as explained in the tutorial)

It doesn’t need to be the same component, updating the cache like mentioned above should work, if you return the correct stuff.

For me Apollo Cache is Your Friend, If You Get To Know It (2023) was really helpful.

I would usually recommend just updating the cache (as that doesn’t hard reload data, so you can animate and stuff), but there is also refetchQueries which you can hand some query to and it will reload whatever data.

We have similar pages with a Redwood Cell structure that renders a table of teams and a button to add a new item. The button opens a modal which contains a Redwood form.

In the Success state of the Redwood Cell we can add refetch as an input:

import type { FindItems, FindItemsVariables } from 'types/graphql'

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

export const QUERY = gql`
  query FindItems() {
    items: items() {
      id
      ...
    }
  }
`

export const Loading = () => (
  // table skeleton loading component
)

export const Empty = () => {
  <div className="flex items-center justify-center">No items found</div>
}

export const Failure = ({ error }: CellFailureProps) => (
  <div className="rw-cell-error">{error?.message}</div>
)

export const Success = ({ item, refetch }: CellSuccessProps<FindItems, FindItemsVariables>) => {
  // table component and other stuff
}

Now you can pass the refetch to your button or modal and what we do in our form is to make it part of the useMutation:

import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'

const ADD_NEW_ITEM = gql`
  mutation AddNewItemMutation($input: AddItemInput!) {
    addItem(input: $input) {
      id
    }
  }
`

const AddNewItem = ({ ..., setIsModalOpen, refetch }: Props) => {
  const [addNewItem] = useMutation(ADD_NEW_ITEM, {
    onCompleted: () => {
      toast.success('New item was added successfully')
      refetch()
      setIsModalOpen(false)
    },
  })
}

In some other usages we get refetch from useQuery like:
const { loading, error, data, refetch } = useQuery(QUERY, { variables: { ... } }) and in some more complex patterns we wrap the refetch in a React.useRef() and pass the hook to the child component(s).

1 Like

Thank you for answering.
I did not quite understand where does the refetch() function come from

Does refetchQueries address this? Creating a Comment Form | RedwoodJS Docs

Yes, but also, sadly, no.
The issue is not related to the application not performing the refetching, but rather to the refetching causing the server to restart (sometimes it is ECONNRESET, sometimes ECONNREFUSED).

I drop the creating and deleting components and services, in case I am doing something wrong:

// DELETE SERVICE
export const deleteItem: MutationResolvers['deleteItem'] = async ({
  id,
}) => {
  const item = await db.item.findUnique({
    where: { id },
    select: { folderName: true },
  })

  if (!item) {
    throw new Error('Item not found')
  }

  const folderName = item.folderName
  const folderPath = `api/folders/${folderName}`

  logger.info(`Deleting folder ${folderName}`)

  try {
    // Delete the folder synchronously
    fs.rmdirSync(folderPath)
    logger.info(`Folder ${folderName} deleted successfully`)
  } catch (error) {
    logger.error(`Error deleting folder ${folderName}: ${error}`)
    throw error
  }

  // Remove the entry from the database
  return db.item.delete({ where: { id } })
}
//  Button performing the delete mutation
const ActionButton = ({ icon, variant, action, id }: ActionButtonProps) => {
  const [deleteItem] = useMutation(DELETE_ITEM, {
    refetchQueries: [{ query: FETCH_ITEMS }],
    onCompleted: () => {
      toast.success('Item deleted!')
    },
    onError: (error) => {
      toast.error(
        'Error trying to delete: ' +
          error.message
      )
      console.error(error)
    },
  })

  // other stuff

  const handleClick = (action: string, id: number | string) => {
    if (action === 'delete') {
      // If alert is confirmed, invoke a mutation to delete the item by id
      if (
        confirm('Are you sure you wish to delete item with id ' + id + '?')
      ) {
        deleteItem({ variables: { id: id } })
      }
    } else if (action === 'edit') {
      // Invoke a mutation to edit the item by id
    } else if (action === 'show') {
      // Invoke a query to show the images of the item by id
    }
  }

  const color = buildColor(variant)

  return (
    <>
      <Toaster />
      <button className={color} onClick={() => handleClick(action, id)}>
        <FontAwesomeIcon icon={icon} className="mr-2 ml-2 mt-2 mb-1" />
      </button>
    </>
  )
}
// Creation service
export const createItem: MutationResolvers['createItem'] = async ({
  input,
}) => {
  const itemCounter: number = await db.item.count()

  // Create the item
  const createdItem = await db.item.create({
    data: {
      ...input,
      folderName: Number.isNaN(itemCounter) ? 'R0' : `R${itemCounter}`,
    },
  })

  const folderName = createdItem.folderName

  // Create the folder using the folderName variable
  fs.mkdirSync(`api/folders/${folderName}`, { recursive: true })
  logger.info(`Created folder ${folderName}`)

  return createdItem
}
// Creation modal
const ItemModal = ({ isModalOpen, setIsModalOpen }: ItemModalProps) => {
  const formMethods = useForm({ mode: 'onBlur' })
  const [create, { loading, error }] = useMutation(CREATE_ITEM, {
    refetchQueries: [{ query: FETCH_ITEMS }],
    onCompleted: () => {
      formMethods.reset()
      closeModal()
      toast.success('Item created!')
    },
  })

  const onSubmit = (data: FormValues) => {
    create({ variables: { input: data } })
    console.log(data)
  }

  const closeModal = () => {
    setIsModalOpen(false)
    console.log(isModalOpen)
  }

  const customModalStyles = {
    content: {
      top: '50%',
      left: '50%',
      right: 'auto',
      bottom: 'auto',
      marginRight: '-50%',
      transform: 'translate(-50%, -50%)',
      backgroundColor: '#fff',
      padding: '2rem',
      borderRadius: '0.5rem',
    },
    overlay: {
      backgroundColor: 'rgba(0, 0, 0, 0.5)',
      zIndex: '50',
    },
  }
  return (
    <Modal
      isOpen={isModalOpen}
      onRequestClose={closeModal}
      style={customModalStyles}
    >
      <div className="p-6 bg-white rounded max-w-lg mx-auto">
        <Toaster />
        <h2 className="text-xl mb-4">Add item</h2>
        <Form onSubmit={onSubmit} error={error} formMethods={formMethods}>
          <FormError error={error} wrapperClassName="text-red-500 mb-4" />

          // Form to insert data

          <div className="flex justify-end mt-4">
            <Submit
              className="flex items-center justify-center bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
              disabled={loading}
            >
              <FontAwesomeIcon icon={faSave} className="mr-2" />
              Save
            </Submit>

            <button
              className="flex items-center justify-center bg-gray-300 hover:bg-gray-400 text-gray-700 font-medium py-2 px-4 rounded ml-4"
              onClick={closeModal}
            >
              <FontAwesomeIcon icon={faTimes} className="mr-2" />
              Abort
            </button>
          </div>
        </Form>
      </div>
    </Modal>
  )
}

Within the code above, when I create an item, the toaster appears, so the onCompleted() method gets called; however, that is when the server side restarts. When I try to delete the item, it frequently throws an error about the server not being up.

Are you running the dev server with yarn rw dev or alternatives?

I suppose the issue is that you are doing writes (creates/deletes) at the file level, which causes the watcher to restart the dev server to reload the change because a file in the watched directory was changed. The backend doesn’t reload instantaneously like the frontend (almost does). I assume this is what causes the ECONNREFUSED errors.

2 Likes

No, no alternatives, I’m running yarn rw dev. I also tried running two separate processes for web and api.
About the idea you mentioned, it could be. The behavior of the application is supposed to create or delete a folder when the relative mutation is called; is there a way to instruct the watcher to ignore a specific folder? And in production, is it something that would happen anyway?

That might be the problem then. I’m unsure if you can disable the watcher from causing this issue. But in theory, it should not be a problem if you run your app without the watcher.

Can you try to run the following: yarn rw build (builds both the web and api packages) and then yarn rw serve api and yarn rw server web in two separate terminals.

This should build and run your app as a production environment.

I tried building it and serving it, however it is generating an error about the server answering with HTML instead of JSON. This is the error:

Unexpected token '<', "<!doctype "... is not valid JSON

ServerParseError: <!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" type="image/png" href="/favicon.png"/><script defer="defer" src="/static/js/runtime-app.70c5d647.js"></script><script defer="defer" src="/static/js/app.f6e4450b.js"></script><link href="/static/css/app.eeff1d7c.css" rel="stylesheet"></head><body><div id="redwood-app"></div></body></html>

I also can confirm that the problem is the file creation in development; I tried turning off the folder creation and it is working as expected.

I do want to point out for others in the thread that as @razzeee and @joconor have pointed out, there is another way to refresh certain data. I think it is actually more powerful and easier to use as well. I’m not sure if they do the same thing under the hood, but I have been using refetchQueries more lately rather than collecting and passing around the refetch object from useQuery().

In order to use refetchQueries you have to import the particular query that was used to retrieve the data that you want to update (follow the info @joconor linked at Creating a Comment Form | RedwoodJS Docs) Then all you have to do is pass that query to the refetchQueries parameter of the mutation you are calling. I think of it a bit like React context, in that you don’t have to constantly pass around a specific object in order to do something in a child component. Just import the specific query you want to update, and pass it in refetchQueries. Done. Then any data in any component that depends on that query data is updated.

I would be curious to hear from some of the RWJS devs if this is good practice or if I’m making it overly-complex. What I’ve been doing recently, based on GraphQL query best practices - Apollo GraphQL Docs, is start to limit each cell’s query to only what is needed in that cell or its immediate dependents - rather than any and every possible dependent. My queries were looking like the example on that page. If I have a particular model that is complex, and a component 2-3 layers below the parent, then I’ll use another cell at that 2-3 layer lower level to retrieve those additional related aspects of the model - which will not always be present on every instance. My queries are smaller, and then when certain related data updates, I only need to refresh those related queries - not the entire big complex model, which would then lead every component in the hierarchy to also re-render. Lots of wasted data transfer and processing to change potentially one single value.

1 Like

I too think that’s the way to go and I think that also mostly leads to the app not having to have a state library integrated as you can do most things just in time.

But I want to emphasize again, that refetching is the bad thing to do in my oppinion. If you can get away with updating the cache from the mutation result, that’s what you should do. Refetching breaks animations for me for e.g.

1 Like

@GRawhideMart I am tempted to ask for a thread title change to more accurately reflect the issue of ‘api crashing on file deletions’… but it feels like the thread has taken a life of its own more along the lines of using `refetchQueries’ :smiley: It feels like the refetchQueries discussion has more pull than your actual problem! It would be strange to change the title, when a lot of the discussion is around fetching issues. So, I am not requesting that, just making the observation :slight_smile:

Anyway, some thoughts on your issue about file watching and creation/deletion. I’m not sure if setting this config value would prevent the watcher from watching that folder, thus not crashing. But, is it necessary to manage files on the local machine serving the app? I may be wrong, but it seems that on each new deploy, the api folder would be back to square one. This would causing you to lose any files/folders created or deleted on the previous deploy. I am not sure this is desired, unless you deploy once and never touch it again. What about using a storage service to host the folders/files and hook whatever sdk up with your functions?

That makes a lot of sense. As I’ve spent more time looking at query re-fetching, I’ve also been looking at how to best manage the cache to minimize round trips as much as possible.

Are you doing your caching explicitly with Apollo’s in memory cache? The bits I’ve read about caching here on the RWJS site suggest that it’s tricky to get it to behave the way you’d like. Any suggested resources for digging further in? The more complex our app gets, the more I’d like to get ahead of these kinds of optimizations now.

Yeah, you are actually right, I should probably close this thread, as the refetchQuery() solution actually works and, as it turns out, that was not the problem.

I will try the solution you mentioned about setting the watcher at VSCode level, however if this doesn’t work I’m assuming that in production this error should not happen, right?
About your perplexities, yes; the application requires those folders to be created and to persist; luckily, A) I don’t foresee many deploys, B) the folders are populated by the end user (I can’t explain the details) with data that is not very heavy, so it can be backed up. This is because the RW application is a part of a larger application in which there is also a websocket message exchange included, and another part of the application requires the folders to be there and to be called in exactly that way.

As I mentioned above, I found reading Apollo Cache is Your Friend, If You Get To Know It (2023) very valuable.

It’s mostly weird, when you do weird things, like change more then just the entity you just updated. But you can get around that, if you have your graphql endpoint return the correct changeset. That might be weird in some other circumstances.

1 Like