Skip to content

Conversation

@isBatak
Copy link

@isBatak isBatak commented Oct 19, 2025

This PR explores an approach to eliminate layout shift during SSR hydration by persisting panel sizes using inline scripts that execute before React hydration.

Changes Added

New Workspace Project

  • Added a Next.js example project (examples/nextjs) to test and demonstrate server-side rendering behavior
  • This allows testing the SSR persistence script in a real-world SSR environment

New Component: PersistScript

  • Added PersistScript.tsx component that renders an inline <script> tag in each panel
  • The script reads saved panel sizes from localStorage and applies them via CSS variables before React hydration

Panel Component Updates

  • Modified Panel.ts to render PersistScript inside each panel when autoSaveId is provided
  • Added suppressHydrationWarning attribute to panel elements to handle the dynamic script injection
  • Added data-panel-order attribute to enable script to match panels with their saved state

Storage Format Changes

  • Breaking Change: Updated localStorage format to include panel order information
  • Changed layout signature from number[] to PanelLayoutItem[]:
    type PanelLayoutItem = {
      order: number;
      size: number;
    };
  • Updated serialization.ts to handle both old and new formats for backward compatibility
  • The script uses panel order to reliably match saved sizes even when panels are reordered

Breaking change

  • order prop is required for this feature to work properly

TODO

  • Minify script source - The inline script should be minified to reduce the HTML size
  • Remove suppressHydrationWarning requirement - Find alternative approach (e.g., global CSS variables scoped by groupId + order: --panel-size-<groupId>-<order>)
  • Add comprehensive tests - Test SSR scenarios, storage format migration
  • Handle custom storage prop - Decide what to do when users provide custom storage implementation

Open Questions

  1. Should we maintain backward compatibility with the old number[] format indefinitely?
  2. What's the best way to detect SSR context to conditionally render the script?
  3. How should custom storage prop interact with the persistence script?

Recordings

Before After
https://github.com/user-attachments/assets/dd77c32e-9a43-4405-8243-56f6abe87421 https://github.com/user-attachments/assets/9397d169-e872-49c0-8b09-962704941521

@vercel
Copy link

vercel bot commented Oct 19, 2025

@isBatak is attempting to deploy a commit to the Brian Vaughn's projects Team on Vercel.

A member of the Team first needs to authorize it.

@bvaughn
Copy link
Owner

bvaughn commented Oct 20, 2025

This seems like a promising idea, but it also seems like it will require a lot of testing and validating to reduce causing regressions. Server rendering is unfortunately not something I use often, so I don't really feel comfortable championing this feature and leading that testing.

@bvaughn
Copy link
Owner

bvaughn commented Oct 20, 2025

I wonder: is there a more incremental/opt-in approach, where panels be updated to read the size value, but rely on external code to actually set it initially for the SSR case? (Obviously we could document how to do that.)

@isBatak
Copy link
Author

isBatak commented Oct 21, 2025

Thanks, that makes sense. Yeah I agree SSR adds some risk so keeping it opt-in sounds like a good idea.

We can remove the logic from Panel and make it composable instead, something like:

<PanelGroup autoSaveId="persistence1" direction="horizontal">
  <Panel defaultSize={20} minSize={10} id="panel1" order={1}>
    <PanelPersistScript autoSaveId="persistence1" panelId="panel1" />
    <div>left</div>
  </Panel>
</PanelGroup>

I’ll rename PersistScript to PanelPersistScript so it’s clearer what it’s for.

For the persistence format change, I added a fallback so older data should still work fine.

On testing: if all client tests pass we should be safe from regressions, and I can add a few more for the fallback logic. For SSR I might try adding one Playwright e2e test with the Next.js example repo, does that sound ok?

@bvaughn
Copy link
Owner

bvaughn commented Oct 22, 2025

Haven’t forgotten about this. Just busy with work. Will try to get back to you soonish

@ybelakov
Copy link

any update?

@bvaughn
Copy link
Owner

bvaughn commented Oct 30, 2025

No. Work has been very busy the past several days and honestly I don't have the mental bandwidth to really think through the ramifications of a change of this size right now. Sorry.

If you're in a hurry to get this functionality shipped, you can always do a forked release for now. That's definitely the fastest path.

@bvaughn
Copy link
Owner

bvaughn commented Oct 31, 2025

Full disclosure: Because this is such a large change, and it would be a backwards breaking one (if we have to change order to a required prop) I'm going to try to find some time to re-think the API at a higher level. There may be other changes I'd like to make too.

@isBatak
Copy link
Author

isBatak commented Nov 2, 2025

Full disclosure: Because this is such a large change, and it would be a backwards breaking one (if we have to change order to a required prop) I'm going to try to find some time to re-think the API at a higher level. There may be other changes I'd like to make too.

here are my latest changes made on my own forked repo, I fixed the required order thing, now it's auto assigned and saved in local storage, so user don't need to set it explicitly anymore.
UPDATE: just realised that order auto assigning is already merged into this branch here

also I moved the panel size css var to PanelGroup (related to this comment #520 (comment)), this makes render cheaper and we only need one PersistScript per group now.
https://github.com/user-attachments/assets/73d27239-ad53-4750-8848-bc4653e6d872

next thing I wanna try is render just one PersistScript for all local storage entries and move css vars to :root. This will remove need for suppressHydrationWarning on PanelGroup. The idea is to use a fallback var for flex-grow like flex-grow: var(--panel-${order}-size, var(--panel-${autoSaveId}-${order}-size)); where --panel-${autoSaveId}-${order}-size is injected to :root by the PersistScript.

also what if we merge this into a beta branch and release it as 3.1.0-beta.1, I’d like to avoid publishing another npm package cause it's hard to find a good name.

@bvaughn
Copy link
Owner

bvaughn commented Nov 2, 2025 via email

@isBatak
Copy link
Author

isBatak commented Nov 2, 2025

I just managed to implement this part too

next thing I wanna try is render just one PersistScript for all local storage entries and move css vars to :root. This will remove need for suppressHydrationWarning on PanelGroup. The idea is to use a fallback var for flex-grow like flex-grow: var(--panel-${order}-size, var(--panel-${autoSaveId}-${order}-size)); where --panel-${autoSaveId}-${order}-size is injected to :root by the PersistScript.

here is the commit isBatak@170b782

  • only one PersistScript is required and it can be anywhere in the DOM
  • all suppressHydrationWarning attributes have been removed, they’re no longer needed
  • CSS variables are injected in the :root using the new CSSStyleSheet() API here

I'm amazed at how well this turned out!

@bvaughn
Copy link
Owner

bvaughn commented Nov 7, 2025

Wanted to share a quick update here. I'm only able to work on this a little each day because my day job is pretty intense right now, but I think I can make a few nice API simplifications in my version 4 branch:

  • Min/max panel size constraints can be controlled entirely with CSS classes/styles
  • Resize handle elements will be optional; the parent group will treat panel edges as drag handles
  • Won't need order props anymore; parent group can determine order based on element rects

I'm not totally sure how the CSS variable stuff fits into that yet but I assume your work in this branch has plenty of useful information there. (Thanks!)

I'm thinking if autoSize is enabled for a group, and it detects an SSR environment (???) maybe it can just automatically render the persist script...but I'll need to give that some more thought.

@bvaughn bvaughn mentioned this pull request Nov 7, 2025
@isBatak
Copy link
Author

isBatak commented Nov 7, 2025

Thanks for the update! Totally understand about the limited time, I appreciate you still making progress on this.

About the “no order props” change, my implementation relies heavily on the order number. I use it as a unique identifier for each panel together with autoSaveId. We can’t just replace order with id because useId generates non-constant values, so anything saved to local storage could change after a refresh.

For example:
• --panel-${order}-size is created by the PanelGroup, which needs to know each panel’s order.
• --panel-${autoSaveId}-${order}-size is created by the PersistScript, which reads the order from local storage and uses autoSaveId from props.

So we’d need to keep some kind of stable order reference for these parts to work.

@isBatak
Copy link
Author

isBatak commented Nov 7, 2025

I'm thinking if autoSize is enabled for a group, and it detects an SSR environment (???) maybe it can just automatically render the persist script...but I'll need to give that some more thought.

My latest implementation of PersistScript actually reads all local storage entries and injects the variables into the global CSS, so it works independently from PanelGroup. That means we can have multiple PanelGroup compositions but only one PersistScript, and it could even live in the document head.

@bvaughn
Copy link
Owner

bvaughn commented Nov 7, 2025

RE: the order prop– I'd like to question that a little more. It seems plausible that:

  1. During the initial render (server side) we can assume panels will render in order; conditional rendering won't yet have had time to mess up the ordering.
  2. After mount/on update, the Group can infer panel order using bounding rects.

I think we may be able to come up with a heuristic that supports this without needing to require both order and id props. That's always felt a bit awkward to me (and does occasionally trip people up).

RE: PersistScript– I think we might still accomplish the goal of one script per page using some kind of module level flag. (The library already relies on module level state for things like pointer event tracking because it's more efficient for drags that impact multiple Groups.) I think it's a nicer API if users don't have to remember to render an additional tag for autoSave to work properly in some cases.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants