Fix: callOnce fn Must Be a Function in Nuxt

Error message:
`callOnce` fn must be a function: {fn}
composables 2025-01-25

What Causes This Error?

This error occurs when you pass something other than a function to callOnce(). The first argument must be a callable function.

The Problem

<script setup>
// ❌ Wrong - passing a value, not a function
callOnce('analytics', analyticsData)

// ❌ Wrong - passing undefined
const handler = undefined
callOnce('setup', handler)

// ❌ Wrong - passing a promise directly
callOnce('data', fetch('/api/data'))
</script>

The Fix

Pass a Function

<script setup>
// ✅ Correct - passing a function
callOnce('analytics', () => {
  initAnalytics()
})

// ✅ Correct - async function
callOnce('setup', async () => {
  await setupApplication()
})

// ✅ Correct - named function
const initializeApp = () => {
  console.log('App initialized')
}
callOnce('init', initializeApp)
</script>

Understanding callOnce

callOnce ensures a function runs only once during the application lifecycle, even with HMR or navigation.

<script setup>
// Runs only once, even if component remounts
callOnce('tracking', () => {
  console.log('This runs only once!')
  initializeTracking()
})
</script>

Common Patterns

Analytics Initialization

<script setup>
// ✅ Correct - wrap initialization in function
callOnce('analytics', () => {
  if (process.client) {
    gtag('config', 'GA_MEASUREMENT_ID')
  }
})
</script>

One-time API Call

<script setup>
// ✅ Correct - async function for data fetching
const config = ref(null)

callOnce('load-config', async () => {
  const data = await $fetch('/api/config')
  config.value = data
})
</script>

Third-party SDK

<script setup>
// ✅ Correct - initialize SDK once
callOnce('chat-widget', () => {
  if (process.client) {
    loadIntercom({
      app_id: 'YOUR_APP_ID'
    })
  }
})
</script>

With Key Parameter

<script setup>
const userId = computed(() => user.value?.id)

// ✅ Function with unique key per user
callOnce(`user-setup-${userId.value}`, () => {
  setupUserPreferences(userId.value)
})
</script>

Error Scenarios

Variable That Might Be Undefined

<script setup>
// ❌ Handler might be undefined based on condition
const handler = someCondition ? initHandler : undefined
callOnce('init', handler)  // Error if undefined!

// ✅ Always provide a function
callOnce('init', () => {
  if (someCondition) {
    initHandler()
  }
})
</script>

Accidental Value Instead of Function

<script setup>
const result = computeSomething()  // Returns a value

// ❌ Wrong - passing the result, not a function
callOnce('compute', result)

// ✅ Correct - wrap in function
callOnce('compute', () => {
  return computeSomething()
})
</script>

Promise Instead of Async Function

<script setup>
// ❌ Wrong - passing a promise
callOnce('fetch', fetch('/api/data'))

// ✅ Correct - async function that returns promise
callOnce('fetch', async () => {
  return await fetch('/api/data')
})
</script>

Async callOnce

<script setup>
// ✅ Can await callOnce with async function
await callOnce('setup', async () => {
  const config = await $fetch('/api/config')
  applyConfig(config)
})

// Code here runs after setup is complete
console.log('Setup done')
</script>

Conditional Execution Inside

<script setup>
// ✅ Put conditions inside the function
callOnce('conditional-init', () => {
  if (process.client && someFeatureEnabled) {
    initFeature()
  }
})

// ❌ Don't conditionally call callOnce (might skip registration)
if (someFeatureEnabled) {
  callOnce('init', initFeature)  // Inconsistent behavior!
}
</script>

TypeScript

<script setup lang="ts">
// ✅ Type-safe callOnce
callOnce<void>('analytics', () => {
  initAnalytics()
})

// ✅ With return type
const result = await callOnce<{ status: string }>('status', async () => {
  return { status: 'initialized' }
})
</script>

Difference from Other Patterns

vs onMounted

<script setup>
// onMounted - runs every time component mounts
onMounted(() => {
  console.log('Runs on every mount')
})

// callOnce - runs only once ever
callOnce('once', () => {
  console.log('Runs only once')
})
</script>

vs Regular Function Call

<script setup>
// Regular call - runs on every navigation/HMR
initAnalytics()  // Might run multiple times

// callOnce - guaranteed single execution
callOnce('analytics', () => {
  initAnalytics()  // Runs once
})
</script>

Debugging

<script setup>
callOnce('debug-test', () => {
  console.log('callOnce executed at:', new Date().toISOString())
  return 'completed'
})
</script>

Quick Checklist

  • First argument is a function, not a value
  • Use arrow function or named function
  • Wrap async operations in async function
  • Don’t pass undefined or null
  • Put conditions inside the function, not outside
  • Use unique key for different call contexts