Skip to content

Conversation

@mattcosta7
Copy link
Contributor

@mattcosta7 mattcosta7 commented Dec 16, 2025

Summary

Adds custom persistence options to PageLayout.Pane's resizable prop, addressing #7311.

Problem

The current resizable={true} implementation reads from localStorage synchronously during render, causing React hydration mismatches in SSR environments. Users have no way to:

  1. Disable persistence entirely (avoid hydration issues)
  2. Use custom persistence (server-side, IndexedDB, sessionStorage, etc.)

Solution

Extend the resizable prop to accept configuration objects:

// Existing behavior - localStorage persistence (may cause hydration mismatch)
<PageLayout.Pane resizable={true} />

// NEW: No persistence - avoids hydration issues
<PageLayout.Pane resizable={{persist: false}} />

// NEW: Custom persistence - full control over storage
<PageLayout.Pane 
  resizable={{
    save: (width, {widthStorageKey}) => {
      // Save to server, IndexedDB, sessionStorage, etc.
    }
  }} 
/>

Additional Enhancement: Number Width Support

The width prop now accepts numbers in addition to named sizes:

// Before: needed full CustomWidthOptions object
<PageLayout.Pane width={{min: "256px", default: "350px", max: "600px"}} />

// After: just pass a number (uses minWidth prop and viewport-based max)
const [width, setWidth] = useState(defaultPaneWidth.medium)
<PageLayout.Pane 
  width={width} 
  resizable={{save: (w) => setWidth(w)}} 
/>

Why accept a number?

  1. Ergonomics for custom persistence - The primary use case involves React state. Without number support, you'd need to reconstruct a CustomWidthOptions object on every width change, creating friction and potential for bugs.

  2. Leverages existing constraints - Numbers use the existing minWidth prop (default 256px) and viewport-based max. This is what most consumers actually want.

  3. Consistency - Named sizes ("medium") already use these same constraints. Numbers behave identically, just with an explicit pixel value.

Potential risks considered

  • Ambiguity about constraints: Mitigated by JSDoc documentation explaining that numbers use minWidth prop and viewport-based max.
  • Invalid values (negative, NaN): Handled gracefully by existing clamping logic (Math.max(min, Math.min(max, width))).
  • Type complexity: Three type guards needed internally (isPaneWidth, isNumericWidth, isCustomWidthOptions), but this is hidden from consumers.

API Design Decisions

  1. Object config vs callback: Using {persist: false} and {save: fn} rather than a single callback allows clear opt-out of persistence without needing a no-op function.

  2. Static module-level persister: The localStorage persister is a static object rather than recreated in hooks, avoiding issues with hook rules and memoization.

  3. SaveOptions with widthStorageKey: Custom save functions receive the storage key, allowing consumers to namespace their persistence if needed.

New Exports

Types:

  • NoPersistConfig - {persist: false}
  • CustomPersistConfig - {save: (width, options) => void | Promise<void>}
  • SaveOptions - {widthStorageKey: string}
  • ResizableConfig - Union of all resizable options
  • PaneWidth - 'small' | 'medium' | 'large'
  • PaneWidthValue - PaneWidth | number | CustomWidthOptions

Values:

  • defaultPaneWidth - {small: 256, medium: 296, large: 320}

Testing

  • 63 unit tests for usePaneWidth hook
  • Stories for each configuration option
  • All existing PageLayout tests pass

Fixes #7311

@mattcosta7 mattcosta7 self-assigned this Dec 16, 2025
@changeset-bot
Copy link

changeset-bot bot commented Dec 16, 2025

🦋 Changeset detected

Latest commit: c490d05

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Dec 16, 2025
@github-actions
Copy link
Contributor

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the integration-tests: skipped manually label to skip these checks.

- Add onWidthChange prop to PageLayout.Pane for tracking width changes
- Support controlled mode by syncing internal state from width prop changes
- onWidthChange fires at drag end and double-click reset (not during drag)
- Both onWidthChange and persister.save fire when both are provided
- Add error handling for onWidthChange callback errors
- Add comprehensive tests for new functionality
- Replace Record<string, never> with explicit NoPersistConfig type
- {persist: false} is more self-documenting than {}
- Make save required on WidthPersister (cleaner type)
- Rename isResizableWithoutPersistence to isNoPersistConfig
- Export NoPersistConfig type
- Update tests for new API
- Add PaneWidthValue type (PaneWidth | number | CustomWidthOptions)
- When width is a number, use minWidth prop and viewport-based max
- Export PaneWidthValue type and isNumericWidth helper
- Add 'Resizable pane with number width' story
- Update changeset documentation
@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/8993

@primer-integration
Copy link

🔬 github-ui Integration Test Results

Check Status Details
CI ✅ Passed View run
Projects (Memex) ✅ Passed View run
VRT ✅ Passed View run

All checks passed! Your integration PR is ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PageLayout resizable is not SSR Safe

2 participants