Skip to content

Client-Side updates

Superglue applications are primarily server-driven, but there are times when you need to update state on the client side without making a server request. This is where useUpdateFragment and useUpdateContent come in.

When to Use Client Side Updates

Common scenarios include:

  • Optimistic updates - Update UI immediately, sync with server later
  • Form state management - Handle user input before submission
  • UI interactions - Toggle states, expand/collapse sections

useUpdateFragment Hook

The useUpdateFragment hook returns a setter function that lets you update any fragment by its ID:

import React from 'react'
import { useContent, useUpdateFragment } from '@thoughtbot/superglue'

function ShoppingCart() {
  const content = useContent()
  const update = useUpdateFragment()

  const addItem = (product) => {
    update(content.cart, (cartDraft) => {
      cartDraft.items.push({
        id: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      })
      cartDraft.totalCost += product.price
      cartDraft.itemCount += 1
      })
    }

  const cart = content.cart

  return (
    <div>
      <h2>Cart ({cart.itemCount} items)</h2>
      {cart.items.map(item => (
        <CartItem key={item.id} item={item} />
      ))}
      <p>Total: ${cart.totalCost}</p>
    </div>
  )
}

Fragment References

The update function's first parameter can be either a string ID or a fragment reference object:

const update = useUpdateFragment()

// Using string ID
update('userCart', (cartDraft) => {
cartDraft.totalCost += 10
})

// Using fragment reference object
const cartRef = { __id: 'userCart' }
update(cartRef, (cartDraft) => {
  cartDraft.totalCost += 10
})

// Both approaches update the same fragment

This flexibility is especially useful when working with fragment references passed between components:

import React from 'react'
import { useUpdateFragment } from '@thoughtbot/superglue'

function PostCard({ postRef }) {
  const update = useUpdateFragment()

  const markAsRead = () => {
    // postRef is { __id: 'post_123' }
    update(postRef, (postDraft) => {
        postDraft.read = true
    })
  }

  return <button onClick={markAsRead}>Mark as Read</button>
}

Immutable updates with Immer

The update function takes a fragment identifier and an updater function that receives an Immer draft:

const update = useUpdateFragment()

update(toFragmentRef('userCart'), (cartDraft) => {
  cartDraft.items.push(newItem)        // Direct mutation (safe)
  cartDraft.totalCost += newItem.price // Direct assignment (safe)
})

Behind the scenes, Superglue takes the updated draft and uses that for the fragment's next state.

Nested Fragment Updates

Fragments are composable and can contain references to other fragments. If you need to update a nested fragment, you can update them using nested update calls.

import React from 'react'
import { useContent, useUpdateFragment} from '@thoughtbot/superglue'

function PostList() {
  const content = useContent()
  const update = useUpdateFragment()

  const updateFirstPost = (content) => {
    // content.posts is a fragment reference like {__id: 'postList'}
    update(content.posts, (draftList) => {
      // draftList[0] is a fragment reference like { __id: 'post_123' }
      update(draftList[0], (firstPostDraft) => {
        firstPostDraft.title = "Updated Title"
        firstPostDraft.featured = true
      })
    })
  }

  const posts = content.posts()

  return (
    <div>
      {posts.map((postRef, index) => (
        <PostCard key={index} postRef={postRef} />
      ))}
      <button onClick={updateFirstPost}>
        Feature First Post
      </button>
      </div>
  )
}

useUpdateContent Hook

The useUpdateContent hook lets you update any page's content by its pageKey:

import { useUpdateContent } from '@thoughtbot/superglue'

function MyComponent() {
  const updateContent = useUpdateContent()

  const handleClick = () => {
    updateContent('/posts', (draft) => {
      draft.title = "Updated Title"
    })
  }

  return <button onClick={handleClick}>Update</button>
}

Optimistic Updates with Server Sync

For optimistic updates, combine client-side updates with server requests:

import React, { useContext } from 'react'
import { useContent, useUpdateFragment, toFragmentRef, NavigationContext } from '@thoughtbot/superglue'

function LikeButton({ postId }) {
  const content = useContent()
  const update = useUpdateFragment()
  const { remote } = useContext(NavigationContext)

  const toggleLike = async () => {
    // Optimistic update
    update(toFragmentRef(`post_${postId}`), (postDraft) => {
      postDraft.liked = !postDraft.liked
      postDraft.likeCount += postDraft.liked ? 1 : -1
    })

    try {
      // Sync with server
      await remote(`/posts/${postId}/toggle_like`, { method: 'POST' })
    } catch (error) {
      // Revert on error
      update(`post_${postId}`, (postDraft) => {
        postDraft.liked = !postDraft.liked
        postDraft.likeCount += postDraft.liked ? 1 : -1
      })
    }
  }

  const post = content.post

  return (
    <button onClick={toggleLike}>
      {post.liked ? '❤️' : '🤍'} {post.likeCount}
    </button>
  )
}