Skip to content

[Feature Request] Nuxt Render Composables (onSSR and onSSG) #21855

@Hebilicious

Description

@Hebilicious

Describe the feature

fix #13949

related issues
#14507
#21370

Motivation :

The nitro:config Nuxt hook is currently used to control the SSG behavior of a Nuxt application. While this works, this is quite limited as its not possible to pass SSG data from a nuxt hook (like nitro:config) to a component/page. Additionally, the API to define SSG/SSR logic that should happen within a page/component is limited to simple conditional checks on environment variables.

Currently, we can use nodejs process, as well as vues onServerPrefetch to define server-only logic. However, conditional logic with process is "heavy handed" and will be applied while pre-rendering AND ssr-ing, and process is technically semantically incorrect for non node runtimes. While this is the simplest approach, it also lacks flexibility.

onServerPrefetch is a "low level" utility and is coupled to the vue lifecycle hooks. This also wouldn't be the right place to add Nuxt specific logic, as this isn't available outside of setup functions.

Edit : @danielroe mentioned that there's also process.env.prerender that is set to true while pre-rendering. While this can be used to run some SSG only logic, it's still a little limited feature wise.

Proposal:

When we communicate and talk about rendering as developers, we use the terms SSG and SSR and these terms have become widely used and understood. This proposal introduce several new composable to express ssg/ssr logic :

  • onSSG can be used for SSG logic and runs at build time only. It can be used to control the nitro pre-renderer, as well as modifying the payload (the scenario of fetching n pages (such as products/blog articles) to pre-render a home page for example). Currently we have if(process.env.prerender){}, which would be similar to.

  • onSSR can be used as more explicit and semantically correct replacements of if(process.server){}

  • onClient can be used as more explicit and semantically correct replacements of if(process.client){}

These composable are flexible and could accept multiple arguments for advanced features such as caching.

We can provide additional typesafety (ie emit lint/typescript errors if some client side only stuff is used in onSSR)

We can use the argument of the first callback to access data in a natural way, such as payload data, nitro pre-renderer instance, hydration data etc

API Design :

These composable would accept a callback function as their first argument, which can be used to run logic.

onSSG(()=> {
console.log("This runs at build time only")
})

onSSR(()=> {
console.log("This runs every time the nuxt app is rendered server side")
})

onClient(() => {
console.log("This runs on client only")
})

Example :

Logic to fetch posts :

const getPosts = async () => {
    const endpoint = 'https://jsonplaceholder.typicode.com/posts'
    const response = await $fetch(endpoint)
    const articles = response.data.slice(0, 10);
    const slugs = articles.map(({ id }) => '/articles/' + id)
    return { articles, slugs }
}

Currently this has to happen within a nuxt hook, in nuxt.config or in a module.

export default defineNuxtConfig({
    hooks: {
        async 'nitro:config'(nitro) {
            if (nitro.dev) return
            const { slugs } = await getPosts();
            nitro.prerender.routes.push(...slugs)
        }
    }
})

With this proposal, this can happen in setup functions (or elsewhere, see additional thoughts).
Payload data can be passed down to other components while pre-rendering

articles/index.vue

<script setup>
onSSG(({ prerenderRoutes })=> {
const { slugs, articles } = await getPosts();
prerenderRoutes.push(...slugs)
return articles //This will be passed to the payload and keyed with the page name
})
</script>

articles/[id].vue

<script setup>
const data = useState("data", () => [])
const extra = useState("extra", () => "")
const ssg = useState("ssg", () => false)

onSSG(({ payload })=> {
const { params } = useRoute()
const preloadData = payload.articles.find(p => p.id === params) // Can access payload
data.value = preloadData 
const extraData = await $fetch(preloadData.something.url) 
extra.value = extraData
ssg.value = true
})

if(ssg.value === false){
// do something if not ssg
}
</script>

Additional thoughts

Alternative names

As @pi0 suggested a prefix different than on could be used :

useSSR(() => {}) 
renderSSR(()=>{})
nuxtSSR(()=>{})
inSSR(()=>{})

useSSG(() => {}) 
renderSSG(()=>{})
nuxtSSG(()=>{})
inSSG(()=>{})

useClient(() => {}) 
renderClient(()=>{})
nuxtClient(()=>{})
inClient(()=>{})

There's some alternative terminologies that could be used for the action itself.

  • SSR => Ssr, Server
  • SSG => Ssg, Prerender, Generate
  • Client => CSR, Csr, Hydrate, SPA, Spa

Early returns and conditionals

While this is api is more expressive it would be great to have the ability to do early returns and to support conditionals.

Currently ...

if(process.server) return
logic()

Without early returns

onClient(() => {
logic()
})

With an API that returns a boolean

if(onSSR()) return
logic()

// Alternatively we could also have something like this

if(isSSR()) // auto-imported helper
if(globalThis.isSSR) // global
if(globalThis.nuxtRender.isSSR) // verbose global

Availability outside of setup

If onSSR, onClient and onSSG are meant to "replace" process.server and process.client and process.env.prerender, they should be available outside of setup functions (in plugins or in modules for example).

Relationship with vue lifecycle hooks

The API is intentionally similar to the vue lifecycle hooks, but they are not the same and can be used together. Perhaps we should we use a different prefix.

onClient(() => {
onMounted(() => {
// This should work 
})
}) 

onSSG(() => {
onMounted(() => {
// this would never run and we could emit a TS error
})
}) 

useState and refs

The implementation might be too coupled to useState, and it could be incompatible with 3rd party libraries like Pinia/apollo/vue-query.
Would it be possible/desirable to make this work with refs ?

Caching

The 2nd argument of onSSG/onSSR could be used for caching :

onSSG(() => {
//logic
},
{ //caching settings/logic.
}) 

However caching is complicated and I don't want this proposal to be too verbose.

Additional information

  • Would you be willing to help implement this feature?
  • Could this feature be implemented as a module?

Final checks

Metadata

Metadata

Assignees

No one assigned
    No fields configured for Enhancement.

    Projects

    Status
    Discussing

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions