Skip to content

Merge Props

Merge props combine new data with existing client-side data instead of replacing it. This is useful for features like load-more lists, infinite scroll, chat messages, notifications, and updating nested objects.

Prop merging only applies during Inertia visits and partial reloads. Full page visits replace the page props entirely.

Shallow Merge

Use sails.inertia.merge() for shallow merging. By default, arrays are appended at the prop root:

js
messages: sails.inertia.merge(() => newMessages)

Append and Prepend

Use .append() or .prepend() when you need to control where new items go:

js
// Append at the root level
messages: sails.inertia.merge(() => olderMessages).append()

// Prepend at the root level
notifications: sails.inertia.merge(() => newestNotifications).prepend()

You can also target nested arrays while replacing the rest of the object. This is the common shape for paginated data:

js
users: sails.inertia.merge(() => paginatedUsers).append('data')

Target several nested arrays by passing an array or object:

js
dashboard: sails.inertia
  .merge(() => dashboardData)
  .append(['activities', 'notifications'])
  .prepend('announcements')

Matching Items

When items have stable IDs, use matchOn so Inertia updates existing items instead of appending duplicates:

js
users: sails.inertia
  .merge(() => paginatedUsers)
  .append('data', {
    matchOn: 'id'
  })

For several nested arrays, pass a path-to-field map:

js
dashboard: sails.inertia
  .merge(() => dashboardData)
  .append({
    'users.data': 'id',
    messages: 'uuid'
  })

Deep Merge

Use sails.inertia.deepMerge() when the entire nested object should be recursively merged:

js
settings: sails.inertia.deepMerge(() => updatedSettings)

For nested arrays inside a deep merge, add .matchOn():

js
chat: sails.inertia.deepMerge(() => chatState).matchOn('messages.id')

Example: Chat Messages

js
// api/controllers/chat/load-messages.js
module.exports = {
  inputs: {
    before: { type: 'number' } // Load messages before this timestamp
  },

  exits: {
    success: { responseType: 'inertia' }
  },

  fn: async function ({ before }) {
    const messages = await Message.find({
      createdAt: { '<': before }
    })
      .sort('createdAt DESC')
      .limit(20)

    return {
      page: 'chat/index',
      props: {
        // Prepend older messages above the existing list
        messages: sails.inertia.merge(() => messages).prepend()
      }
    }
  }
}

Example: Settings Update

js
// api/controllers/settings/update-notifications.js
module.exports = {
  inputs: {
    emailNotifications: { type: 'boolean' }
  },

  exits: {
    success: { responseType: 'inertia' }
  },

  fn: async function ({ emailNotifications }) {
    const settings = await Settings.updateOne({
      user: this.req.session.userId
    }).set({
      notifications: { email: emailNotifications }
    })

    return {
      page: 'settings/index',
      props: {
        // Deep merge preserves other settings
        settings: sails.inertia.deepMerge(() => settings)
      }
    }
  }
}

Merge vs Replace

Operationprops.itemsmerge(() => items)
BehaviorReplaces arrayAppends to array
Use caseFull page loadLoad more / infinite scroll

Frontend Integration

Your frontend handles merge props automatically. The Inertia client merges the data before updating your component.

vue
<script setup>
defineProps({
  messages: Array // Automatically merged with existing messages
})
</script>

<template>
  <div v-for="message in messages" :key="message.id">
    {{ message.content }}
  </div>
  <button @click="loadMore">Load More</button>
</template>

For comprehensive infinite scroll with pagination metadata, see Infinite Scroll.

Resetting Props

When filters change, ask Inertia to reset the merged prop before loading the next payload:

js
router.reload({
  only: ['users'],
  reset: ['users']
})

All open source projects are released under the MIT License.