#Somewhat Recursive Hook Causes Infinite Loading on Save

8 messages · Page 1 of 1 (latest)

hearty violet
#

I want to create a relationship in my Customer collection where the spouse should reference each other when set, so I came up with this hook:

      hooks: {
        beforeChange: [
          async ({ data, value, previousValue, req: { payload } }) => {
            if (!data || !data.id) return value;
            if (
              value &&
              previousValue &&
              getEntityId(value) === getEntityId(previousValue)
            ) {
              return value;
            }

            if (value) {
              await payload.update({
                collection: "customers",
                data: {
                  spouse: data,
                },
                id: isEntity(value) ? value.id : value,
              });
            } else {
              await payload.update({
                collection: "customers",
                data: {
                  spouse: null,
                },
                where: {
                  spouse: { equals: data.id },
                },
              });
            }

            return value;
          },
        ] satisfies FieldHook<
          TypedCollection["customers"],
          TypedCollection["customers"]["spouse"]
        >[],
      },

I tried it both as a beforeChange and afterChange hook, but no matter which one when saving the spouse the saving loads forever which leads me to believe that it's recursively calling the hook but I'm not sure why since I have the getEntityId(value) === getEntityId(previousValue) guard?

hushed mountain
#

Ahh a classic infinite loop issue—I have inadvertently done this many times.

You had the right idea with the guard, but it's not checking the right thing. What is actually happening:

  1. Save Customer A with spouse = Customer B → hook fires, updates Customer B's spouse to Customer A

  2. That update triggers Customer B's spouse hook, where value = Customer A and previousValue = null (B had no spouse before)

  3. null !== Customer A → guard doesn't trigger → hook fires again, updates Customer A's spouse to Customer B

  4. Resulting in an infinite loop

#

You could trying using req.context

async ({ data, value, previousValue, req, req: { payload } }) => {
  // Break the recursion — this update was triggered by the spouse sync itself
  if (req.context?.isSpouseSync) return value

  if (value) {
    await payload.update({
      collection: 'customers',
      id: isEntity(value) ? value.id : value,
      data: { spouse: data.id },
      context: { isSpouseSync: true }, // propagates to the triggered hook
      req,
    })
  } else {
    await payload.update({
      collection: 'customers',
      data: { spouse: null },
      where: { spouse: { equals: data.id } },
      context: { isSpouseSync: true },
      req,
    })
  }

  return value
}
#

this should allow you to drop the guard entirely actually

hearty violet
#

I actually fiddled around with context too but didn't realize req had to be passed! That was what was missing to be able to do req.context?.isSpouseSync.

#

Thank you for your help @hushed mountain!

hushed mountain
#

No problem!