The Nuxt team introduced support for sending class instances from the server to the client in Nuxt 3.4.0 as an experimental feature & made it enabled by default in Nuxt 3.5.0
However, in my opinion, the documentation has been quite falling behind... As someone who hasn't had much experience with custom serializers, I am not so sure how to implement my own based on the example they provided in the release notes:
// example of a custom payload plugin from the Nuxt team
export default definePayloadPlugin(() => {
definePayloadReducer('BlinkingText', data => data === '<original-blink>' && '_')
definePayloadReviver('BlinkingText', () => '<revivified-blink>')
})
I have so many questions like:
After some experimentation, I came up with this plugin for my ApiResponse
class:
export default definePayloadPlugin(() => {
definePayloadReducer('ApiResponse', data => data instanceof ApiResponse && data.getReducedData())
definePayloadReviver('ApiResponse', data => new ApiResponse(...data))
})
I defined a method in the class, which returns the constructor parameters in an array:
getReducedData() {
return [this.data, this.model, this.responseMeta]
}
And I am using the following transform
method in my useFetch
wrapper:
transform: (data) => new ApiResponse(data, this.model, responseMeta),
This, however, results in the following error: 500 Cannot stringify a function
at flatten (./node_modules/devalue/src/stringify.js:102:15)
at flatten (./node_modules/devalue/src/stringify.js:60:39)
at flatten (./node_modules/devalue/src/stringify.js:166:43)
at flatten (./node_modules/devalue/src/stringify.js:166:43)
at flatten (./node_modules/devalue/src/stringify.js:60:39)
at stringify (./node_modules/devalue/src/stringify.js:178:16)
at renderPayloadJsonScript (./.nuxt/dev/index.mjs:12654:32)
at ./.nuxt/dev/index.mjs:12578:29
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
What am I doing wrong?
If I do the transform in the following way, it kinda works:
// in useFetch options:
transform: this.transformResponse
// the transformResponse function:
transformResponse(response, responseMeta = null) {
return new ApiResponse(response, this.model, responseMeta)
}
The data
returned from useFetch
is indeed instanceof ApiResponse
on the client in this case, however, the this
context in the callback is different so I have no way to access the model & as there is no way for me to pass the responseMeta
into the constructor.
I don't understand why the payloadPlugin seems to work in this second example and not in the first one... There is no mention of the definePayloadPlugin
, definePayloadReducer
& definePayloadReviver
functions in the documentation.
I figured it out. Let's go through the points one by one:
Are all of the payload plugins run every time a payload is sent from the server to the client and does only the one that returns a truthy value in the payloadReducer callback get used on the specific type?
Yes.
Does the name of the payloadReducer and the payloadReviver matter? Is it tied to something? Or can it be anything as long as the reviver & reducer have the same name?
As far as I'm aware, the name has nothing to do with the type - so it can be anything as long as the reducer & the reviver match.
Will the reviver automatically run on the client during hydration?
Yes.
Basically, I'm pretty sure that every payload plugin runs for every payload on the client during hydration.
Now let's discuss why my first solution didn't work:
In the ApiResponse
class, I instantiate a new ApiModel
subclass based on the model reference provided in the constructor. THIS ApiModel
reference is the "function that could not be stringified".
The version where I only provided a reference to the response transform function (transform: this.transformResponse
) worked because this.model
was undefined
in that context.
So it appears I can't pass classes in any form, whether they're instantiated or not... which does make sense, I suppose...
In the getReducedData
method, instead of passing the model reference, I return the model's name as a string.
getReducedData() {
return [this.data, createModelKey(this.model), this.responseMeta]
}
I then use that name in a map to instantiate the appropriate model on the client in the reviver, like this:
const classes = {
[createModelKey(ProductModel)]: ProductModel,
}
export default definePayloadPlugin(() => {
definePayloadReducer('ApiResponse', data => data instanceof ApiResponse && data.getReducedData())
definePayloadReviver('ApiResponse', (data) => {
const model = data[1] ? classes[data[1]] : null
return new ApiResponse(data[0], model, data[2])
})
})
Please let me know if there's anything I'm doing wrong or if there are better ways to approach this. 🙏 If possible, I would prefer not to add the model classes to the map manually...
I hope this helps someone!