hook is not a function

default discord avatar
snailedlt
last month
34

I'm trying to set the

name

field of a collection based on the title of a relation field... Related to this discussion:

https://payloadcms.com/community-help/discord/is-it-possible-to-populate-field-with-the-title-of-a-relation



However now I'm trying to do it while using the

payload-next-demo

as a starting point. When I try to create a collection in the admin UI and hit save I get the error:

hook is not a function

, and the collection is not saved.



I assume the source of the error is this hook file:

https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/collections/PriceZones/hooks/beforeChange.ts#L11

Which is used here:

https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/collections/PriceZones/index.ts#L16-L18

because it's mocked with webpack in the payload.config.ts file:

https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/payload.config.ts#L28-L37

It's worth noting that the error also occurs when the hook file looks like this:


import { CollectionBeforeChangeHook } from 'payload/types'

export const BeforeChangeHook: CollectionBeforeChangeHook = async ({
  data,
}) => {
  return {
    ...data // just return the data without modifications
  }
}


I don't understand the error though, nor how to fix it... So I hope someone's able and willing to help out 🙂




Full error:


[13:42:23] ERROR (payload): TypeError: hook is not a function
    at eval (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:111:22)   
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async create (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:109:5)
    at async handler (webpack-internal:///(api)/./node_modules/@payloadcms/next-payload/dist/handlers/[collection]/index.js:83:33)
  • discord user avatar
    jesschow
    Payload Team
    last month

    Hey @snailedlt - I'll take a look at this for you, spinning up your repo now then I'll let you know what I find

  • default discord avatar
    snailedlt
    last month

    Wow, thank you for going above and beyond to troubleshoot for me!


    The error can be found on the latest commit of the

    make_adjustments_for_gv

    branch

  • default discord avatar
    farhansyah
    last month

    i have faced the same error recently. However, instead of using return {...data} just don't return anything and it'll work. If you want to change the data. just use data.x = something or siblingData.xx = something



    I am not sure if there's a change

  • default discord avatar
    snailedlt
    last month

    That's good to know, and in most cases that will work fine. I need to set the name though, so my return looks like this:


      return {
        ...data,
        name: 'newName'
      }

    Here I think I need to pass

    ...data

    before

    name

    so it doesn't override the existing data, or is it possible to change the name without returning it?

  • default discord avatar
    farhansyah
    last month

    you can use siblingdata

  • default discord avatar
    snailedlt
    last month

    How would that look? I'm unfamiliar with the syntax

  • default discord avatar
    farhansyah
    last month
    async ({ siblingData }) => {
        siblingData.name = "new name"
    
        // You can also do your own validation
    
        if(something){
          siblingDate.name = ""
        }
    },
  • default discord avatar
    snailedlt
    last month

    interesting. So this should work then right?


    import { CollectionBeforeChangeHook } from 'payload/types'
    import getPayloadClient from '../../../payloadClient';
    
    export const BeforeChangeHook: CollectionBeforeChangeHook = async ({
      data, // incoming data to update or create with
      siblingData,
    }) => {
      const payload = await getPayloadClient();
      const range = await payload.findByID({ collection: 'km-ranges', id: data.range })
      const priceDescription: string = await data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
      siblingData.name = range.name + ' - ' + data.price + ' ' + priceDescription
    }


    But I get this error:


    Property 'siblingData' does not exist on type '{ data: Partial<any>; req: PayloadRequest; operation: CreateOrUpdateOperation; originalDoc?: any; context: RequestContext; }'.ts(2339)
    (parameter) siblingData: any
    No quick fixes available
    image.png
  • discord user avatar
    jesschow
    Payload Team
    last month

    @farhansyah is correct about reassigning your data - it does not seem to be the cause of your issue though, am still looking into it

  • default discord avatar
    snailedlt
    last month

    Yeah siblingData isn't available in the

    CollectionBeforeChangeHook

    type:


    export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
        data: Partial<T>;
        req: PayloadRequest;
        /**
         * Hook operation being performed
         */
        operation: CreateOrUpdateOperation;
        /**
         * Original document before change
         *
         * `undefined` on 'create' operation
         */
        originalDoc?: T;
        context: RequestContext;
    }) => any;


    sorry, here's the implementation:


    type CollectionBeforeChangeHook<T extends TypeWithID = any> = (args: {
        data: Partial<T>;
        req: PayloadRequest;
        operation: CreateOrUpdateOperation;
        originalDoc?: T;
        context: RequestContext;
    }) => any
  • default discord avatar
    farhansyah
    last month

    Ah, you are using CollectionHook. Sorrry, siblingData only exist on FieldHook

  • default discord avatar
    snailedlt
    last month

    Ahh, makes sense. Thanks for the info still, every bit helps 🙂

  • default discord avatar
    farhansyah
    last month

    But it should be similar, need to try to use whatis in the CollectionHook, but instead of returning data, you need to assign data.. that's the difference.

  • default discord avatar
    snailedlt
    last month

    I guess for my usecase I could just use

    data.name

    to edit the name field, instead of editing

    name

    directly:


    data.name = range.name + ' - ' + data.price + ' ' + priceDescription

    However, like jesschow said, that still gives the same error for me :/

  • default discord avatar
    farhansyah
    last month


    i tried using beforeValidate hook, this work for me..



    are you user data.range exist ? it is not in the field

    image.png
    image.png
  • default discord avatar
    snailedlt
    last month

    I changed the name to

    kmRange

    a while back, but forgot to change it here. Thanks for the heads up



    Gonna try this now



    Okey this works as long as I don't need to use the payload local API.


    If I use the payload local API directly like this I get a webpack error:


    import type { CollectionConfig } from 'payload/types'
    import payload from "payload"
    
    export const PriceZones: CollectionConfig = {
    // The rest of the collection
      hooks: {
        beforeChange: [async ({data}) => {
          const range = await payload.findByID({ collection: 'km-ranges', id: data.kmRange })
          const priceDescription: string = await data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
    
          data.name = range.name + ' - ' + data.price + ' ' + priceDescription}],
      },
    }

    [12:21:29] ERROR (payload): TypeError: payload__WEBPACK_IMPORTED_MODULE_0__.default.findByID is not a function
        at PriceZones.hooks.beforeChange (webpack-internal:///(api)/./payload/collections/PriceZones/index.ts:22:85)
        at eval (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:111:22)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async create (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:109:5)
        at async handler (webpack-internal:///(api)/./node_modules/@payloadcms/next-payload/dist/handlers/[collection]/index.js:83:33)


    If I use the localAPI via getPayloadClient() I get an fs error:


    import type { CollectionConfig } from 'payload/types'
    import getPayloadClient from '../../payloadClient';
    
    export const PriceZones: CollectionConfig = {
      // The rest of the collection
      hooks: {
        beforeChange: [async ({data}) => {
          const payload = await getPayloadClient();
          const range = await payload.findByID({ collection: 'km-ranges', id: data.kmRange })
          const priceDescription: string = await data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
    
          data.name = range.name + ' - ' + data.price + ' ' + priceDescription}],
      },
    }

    //payloadClient.ts
    import { getPayload } from "payload/dist/payload";
    import type {Payload} from "payload"
    import config from './payload.config';
    
    if (!process.env.MONGODB_URI) {
      throw new Error('MONGODB_URI environment variable is missing')
    }
    
    if (!process.env.PAYLOAD_SECRET) {
      throw new Error('PAYLOAD_SECRET environment variable is missing')
    }
    
    /**
     * Global is used here to maintain a cached connection across hot reloads
     * in development. This prevents connections growing exponentially
     * during API Route usage.
     *
     * Source: https://github.com/vercel/next.js/blob/canary/examples/with-mongodb-mongoose/lib/dbConnect.js
     */
    let cached = (global as any).payload
    
    if (!cached) {
      cached = (global as any).payload = { client: null, promise: null }
    }
    
    export const getPayloadClient = async (): Promise<Payload> => {
      if (cached.client) {
        return cached.client
      }
    
      if (!cached.promise) {
        cached.promise = await getPayload({
          // Make sure that your environment variables are filled out accordingly
          mongoURL: process.env.MONGODB_URI as string,
          secret: process.env.PAYLOAD_SECRET as string,
          config: config,
        })
      }
    
      try {
        cached.client = await cached.promise
      } catch (e) {
        cached.promise = null
        throw e
      }
    
      return cached.client
    };
    
    export default getPayloadClient;


    Error:


    - error ./node_modules/atomically/dist/utils/fs.js:4:0
    Module not found: Can't resolve 'fs'
    Did you mean './fs'?
    Requests that should resolve in the current directory need to start with './'.
    Requests that start with a name are treated as module requests and resolve within module directories (node_modules, D:\code\test_projects\next-payload-gv).
    If changing the source code is not an option there is also a resolve options called 'preferRelative' which tries to resolve these kind of requests in the current directory too.
    
    https://nextjs.org/docs/messages/module-not-found
    
    Import trace for requested module:
    ./node_modules/atomically/dist/index.js
    ./node_modules/conf/dist/source/index.js
    ./node_modules/payload/dist/utilities/telemetry/index.js
    ./node_modules/payload/dist/utilities/telemetry/events/serverInit.js
    ./node_modules/payload/dist/payload.js
    ./payload/payloadClient.ts
    ./payload/collections/PriceZones/index.ts
    ./payload/payload.config.ts
    ./node_modules/payload/dist/admin/Root.js
    ./app/(payload)/admin/page.tsx
  • default discord avatar
    farhansyah
    last month

    are you sure you are using the id: data.kmRange correctly?



    payload.findbyId uses uniqueId like mongodb ObjectID value, unique string or number.



    If yes, are the document you are looking for exist?



    if you are intending to use just the values from the 'not yet created' doc, you can just get the value from the data object itself.



    const priceDescription: string = data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
    data.name = data.name + ' - ' + data.price + ' ' + priceDescription
  • discord user avatar
    jesschow
    Payload Team
    last month

    Hey @snailedlt - I've got an alternative solution for you, is it alright if I open a PR on your repo?

  • default discord avatar
    snailedlt
    last month

    Yes, that would be highly appreciated!



    Thank you very much!



    Not 100% sure if it's the correctway to do it, but it did work earlier (see this thread:

    https://discord.com/channels/967097582721572934/1134115313437384876

    ). Yes the document exists.


    The intention is to use the name field of the kmRange releation, which afaik can't be retrieved other than through the local API. I'm very possibly wrong here though, so feel free to correct me 🙂

  • default discord avatar
    farhansyah
    last month

    ah, then you can't use find by ID, since it is not the unique ID for the document.. you can use payload.find() for that



    You'll get an array of result. Then check if the doc exist

  • default discord avatar
    snailedlt
    last month

    But it worked earlier though. Why can't it be used here?



    To be clear, this is the code that worked:

    https://discord.com/channels/967097582721572934/1134115313437384876/1134480156187181238

    Inside a project based on the ecommerce template... while it doesn't work inside the project based on next-payload-demo. So the issue isn't the localapi method itself... but I'll wait and see what jess comes up with! 😄

  • default discord avatar
    farhansyah
    last month

    you can try printing the result, whether you get all the values correctly, such as 'data.kmRange' and 'range'

  • default discord avatar
    snailedlt
    last month

    it errors before it can do anything.


    Even this errors out:


    import { CollectionBeforeChangeHook } from 'payload/types'
    
    export const BeforeChangeHook: CollectionBeforeChangeHook = async ({
      data, // incoming data to update or create with
    }) => {
      console.log("test");
    
    }
  • default discord avatar
    farhansyah
    last month

    haha

  • discord user avatar
    jesschow
    Payload Team
    last month

    I didn't have permission to open a PR on your repo, so here are the two files I changed:


    1. PriceZones > index.ts


    2. KMRanges.ts



    I think the best way to achieve what you're looking for is to move everything to a virtual field. So, I have removed the collection hook and added hooks to the

    name

    fields in your

    price-zones

    and

    km-ranges

    collections. As you and @farhansyah said, we do not need to return any data, simply reassigning it will work. I have also changed your Local API request to a REST API request, with the structure of your project I believe the local API won't work.



    Try replacing your two files with the ones I sent and let me know if it works for you!

  • default discord avatar
    notchr
    last month

    Jess always saving the day!

  • default discord avatar
    snailedlt
    last month

    Awesome, testing it right away!



    Awesome, it works perfectly!


    I would like if possible to only use the local api, could you tell me a little more about why that might not be possible?



    I am able to use the local api in the frontend like this btw:

    https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/app/(site)/page.tsx
    import getPayloadClient from "../../payload/payloadClient";
    
    
    export default async function Home() {
      const payload = await getPayloadClient();
    
      const kmRanges = await payload.find({
          collection: "km-ranges",
        });
      return (
        <div>
          <h1>Ranges</h1>
          <p>{JSON.stringify(kmRanges)}</p>
        </div>
      );
    }
  • discord user avatar
    jesschow
    Payload Team
    last month

    I'm not super familiar with how the

    getPayloadClient()

    works so I did some digging and you are correct - that should be fine to use, you might need to add it to your webpack aliases thought.



    Also, I just tried this method of using the local API and it works:


    const getFullTitle: FieldHook = async ({ siblingData, data, req: { payload } }) => {
      if (data) {
        try {
          const kmRanges = await payload.find({
              collection: "km-ranges",
            });
    
          if (kmRanges) {
            const priceDescription: string = data.priceType === 'fixed' ? 'nok' : 'nok/km'
            siblingData.name =`${kmRanges.docs[0].name} - ${data.price} ${priceDescription}`
          }
        } catch (err) {
          console.log(err)
        }
      }
    };


    With hooks, you can de-structure

    payload

    directly from the

    req

    object.

  • default discord avatar
    snailedlt
    last month

    Ohh that's great! I'm gonna be using that from now on. Again thanks a bunch for all the time and debugging spent helping!



    Minor detail: Had to change it to findByID to get the correct kmRanges.name. Might be useful to know for others in the future 🙂


    import type { CollectionConfig } from 'payload/types'
    import { FieldHook } from 'payload/types';
    
    const getFullTitle: FieldHook = async ({ siblingData, data, req: { payload } }) => {
      if (data) {
        try {
          const kmRanges = await payload.findByID({ collection: 'km-ranges', id: data.kmRange });
    
          if (kmRanges) {
            const priceDescription: string = data.priceType === 'fixed' ? 'nok' : 'nok/km'
            siblingData.name =`${kmRanges.name} - ${data.price} ${priceDescription}`
          }
        } catch (err) {
          console.log(err)
        }
      }
    };
  • discord user avatar
    jesschow
    Payload Team
    last month

    Happy to help!

  • default discord avatar
    snailedlt
    last month

    I found a bug



    Sorting the name column doesn't work. Sorting any of the other columns work

    image.png
    image.png
  • discord user avatar
    jesschow
    Payload Team
    last month

    Shoot, this is because a virtual field doesn't get saved to the database so there is nothing to sort

    on

    . You can remove the whole

    beforeChange

    hook if you don't mind this value getting stored

  • default discord avatar
    snailedlt
    last month

    gotcha, thanks



    But still, the name column should probably be sortable even if it's a virtual field?



    In an ideal world I mean 🙂

  • discord user avatar
    jarrod_not_jared
    Payload Team
    last month

    It's not possible. When you sort other columns it sorts the documents from the DB, it would be strange if these behaved differently and only sorted the ones that you see on screen. I do agree that it might be nice to come up with a way to mark fields as virtual and hide the sort icons, or show a different icon to denote why it is not sortable.

Open the post
Continue the discussion in Discord
Like what we're doing?
Star us on GitHub!

Star

Connect with the Payload Community on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.