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 useSetFragment
comes 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
useSetFragment Hook¶
The useSetFragment
hook returns a setter function that lets you update any
fragment by its ID:
import React from 'react'
import { useContent, useSetFragment } from '@thoughtbot/superglue'
function ShoppingCart() {
const content = useContent()
const set = useSetFragment()
const addItem = (product) => {
set('userCart', (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 set function's first parameter can be either a string ID or a fragment reference object:
const set = useSetFragment()
// Using string ID
set('userCart', (cartDraft) => {
cartDraft.totalCost += 10
})
// Using fragment reference object
const cartRef = { __id: 'userCart' }
set(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 { useSetFragment } from '@thoughtbot/superglue'
function PostCard({ postRef }) {
const set = useSetFragment()
const markAsRead = () => {
// postRef is { __id: 'post_123' }
set(postRef, (postDraft) => {
postDraft.read = true
})
}
return <button onClick={markAsRead}>Mark as Read</button>
}
Immutable updates with Immer¶
The set
function takes a fragment identifier and an updater function that
receives an Immer draft:
const set = useSetFragment()
set('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 set
calls.
import React from 'react'
import { useContent, useSetFragment} from '@thoughtbot/superglue'
function PostList() {
const content = useContent()
const set = useSetFragment()
const updateFirstPost = (content) => {
// content.posts is a fragment reference like {__id: 'postList'}
set(content.posts, (draftList) => {
// draftList[0] is a fragment reference like { __id: 'post_123' }
set(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>
)
}
Optimistic Updates with Server Sync¶
For optimistic updates, combine client-side updates with server requests:
import React, { useContext } from 'react'
import { useContent, useSetFragment, NavigationContext } from '@thoughtbot/superglue'
function LikeButton({ postId }) {
const content = useContent()
const set = useSetFragment()
const { remote } = useContext(NavigationContext)
const toggleLike = async () => {
// Optimistic update
set(`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
set(`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>
)
}
Advanced Redux Scenarios¶
The combination of useSetFragment
, Fragments, and
useSetContent, would be able to handle most of your state
management needs. For even more advanced use cases, we have
conveniences for you if you decide to use Redux as
your state management solution.