Conversation
…utes and improved state handling
|
@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. |
|
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. |
|
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.) |
|
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 <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 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? |
|
Haven’t forgotten about this. Just busy with work. Will try to get back to you soonish |
…ter layout consistency
…e it form the Panel internals
…oupState in persist.ts
|
any update? |
|
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. |
|
Full disclosure: Because this is such a large change, and it would be a backwards breaking one (if we have to change |
|
I just managed to implement this part too
here is the commit isBatak@170b782
I'm amazed at how well this turned out! |
feat: move css vars to panelGroup for better performance
|
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:
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 |
|
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: So we’d need to keep some kind of stable order reference for these parts to work. |
My latest implementation of |
|
RE: the
I think we may be able to come up with a heuristic that supports this without needing to require both RE: |
| * | ||
| * This file is the source of truth for the persist logic. | ||
| * It gets minified and transformed by minify-persist.ts during the build. | ||
| */ |
There was a problem hiding this comment.
Hey @isBatak– clarifying question about this as I'm thinking about how best to port it to #528
How does the timing for this actually work?
My understanding of server rendered React was that static HTML would be sent to the client and rendered/painted before "hydration" (or any other JavaScript) actually runs. Wouldn't this injected script be too late to actually prevent the layout flicker then?
There was a problem hiding this comment.
The idea is to rely on this behavior: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#blocking
When the browser parses the HTML sent from the server, it will stop at the script tag and execute the inlined JS. The JS then reads the localStorage values and injects the global CSS variables to avoid needing suppressHydrationWarning.
Since the browser can’t render without parsing the DOM, rendering is indirectly blocked.
There was a problem hiding this comment.
I see. Thanks for sharing that information.
I guess that’s actually a pretty important thing for users to consider then, since that could potentially delay the initial paint significantly in some case cases?
There was a problem hiding this comment.
That’s definitely something for users to consider. There’s always a trade-off.
That’s why I went with a single PersistScript per page — it only blocks parsing once. It reads all the relevant localStorage entries and injects them in one go, and then the CSS picks up the variables it needs during the render phase.
I’ve also been thinking about adding an option to specify which autoSaveIds to inject, since localStorage may contain entries from other routes. Something like:
export function PageA() {
return (
<>
<PersistScript autoSaveIds={['a1', 'a2', 'a3']} />
<PanelGroup autoSaveId="a1">
{/* ... */}
<PanelGroup autoSaveId="a2">
{/* ... */}
<PanelGroup autoSaveId="a3">
{/* ... */}
</PanelGroup>
</PanelGroup>
</PanelGroup>
</>
);
}export function PageB() {
return (
<>
<PersistScript autoSaveIds={['b1']} />
<PanelGroup autoSaveId="b1">
{/* ... */}
</PanelGroup>
</>
);
}|
How are you thinking about auto-save groups with dynamic layouts? These groups may save multiple combinations of panel configurations, e.g. {
"center,left,right":{"left":20,"center":30,"right":30},
"left,right":{"left":40,"right":60}
}How would the injected script know which CSS variables to write in this kind of scenario? the panels won't yet have registered, so the parent group wouldn't know whether there will be two or three panels. |
|
I haven’t thought deeply about this case. But I think the solution is to store which panel was removed in localStorage. By reading which panel is removed from localStorage, the script can know which layout it should load. |
|
Hmm. We could also store timestamps with layouts and restore the most recent one, but… I’m not sure it’s safe to assume that the most recent combination of panels will necessarily be the one to render on a new page load. Panels could be based on eg the size of the viewport. |
|
TBH even if I switched the persistence mechanism to cookies instead of |
This is probably the best option I can think of. I could change the API to be more explicit (e.g. I do find myself considering whether it would be easier to use cookies to store and retrieve the most recent layout. The whole injected blocking script that needs to be precompiled and minified as a separate build step bit is fairly awkward. |
|
Commit 199a897 adds this behavior to the v4 rewrite, restoring the most recently saved Group layout and assuming that will probably almost always be the one we're rendering. I still need to decide how/if to support custom I've also decided, for now at least, that |
This will always bundle the blocking script. I would rather go with the akward API then always bundle the script whether it's used or not. |
Maybe it's best to leave this decision to the user and design a low level API that allows any type of storage. For example: Ark UI splitter is doing something similar https://ark-ui.com/docs/components/splitter |
I don't think IndexedDB should be a target for whatever solution this library supports, since operations are asynchronous. I think that lifting the save/restore logic out of Group and into a hook might be a good approach though, since it leaves things fully in user land to customize. My main concern is the potential for confusion or conflicts between group-default-layout and panel-default-size. I think the documentation would have to be very clear that:
|
|
Slept on this and have decided that I don't like that API direction. Something with this general shape seems ideal from an ergonomics/DevX perspective: <Group autoSave id="my-group">
...
</Group>Though more flexible, having to use something like this throughout an app with a lot of resizable panels would be annoying: const { defaultLayout, onLayoutChange } = useDefaultLayout({
groupId: "my-group",
panelIds: ["left", "right"]
});
<Group
defaultLayout={defaultLayout}
id="my-group"
onLayoutChange={onLayoutChange}
>
...
</Group>Injecting render blocking scripts isn't ideal. On the other hand, layout shift sucks and I'm not sure why you'd ever consider it an acceptable trade off in an SSR context. I can think of a few possible options here I guess.
I'm really feeling fairly torn about this. |
|
In some ways, I quite like separating persistence logic from rendering logic. I'm not sure how the proposal above (the new hook and I guess I could punt on that and say: either use a sync storage API (like cookies) or use Suspense Further more I guess if the storage continues to include enough metadata to pick out the most recently updated layout, then Group could also extract panel keys from that layout and use those keys for the initial CSS variables it renders. If I limit the amount of data saved to only include the most recent layout, then it might be less likely to run into cookie size limits. It's probably mostly fine to not separately remember different combinations of panel layouts for any given group. 🤔 Seems like the sort of thing that probably cause frequent confusion and GitHub issue questions though. I would prefer a more out-of-the-box solution. |
|
Okay after a lot of back and forth, I'm now leaning toward this: Docs example here: |
|
I skimmed the changes, and I really like the direction this is going. |
|
Lovely. Thanks for taking a look and sharing feedback! Just published my branch to NPM as react-resizable-panels@4.0.0-alpha.0 |
|
Going to go ahead and close this in favor of #528. Not sure how soon I'll release that (hopefully before the end of the year) but I'm pretty committed to the rewrite at this point. Thanks for all of the feedback and testing you've done on that PR! |


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
examples/nextjs) to test and demonstrate server-side rendering behaviorNew Component:
PersistScriptPersistScript.tsxcomponent that renders an inline<script>tag in each panelPanel Component Updates
Panel.tsto renderPersistScriptinside each panel whenautoSaveIdis providedsuppressHydrationWarningattribute to panel elements to handle the dynamic script injectiondata-panel-orderattribute to enable script to match panels with their saved stateStorage Format Changes
number[]toPanelLayoutItem[]:serialization.tsto handle both old and new formats for backward compatibilityorderto reliably match saved sizes even when panels are reorderedBreaking change
orderprop is required for this feature to work properlyTODO
suppressHydrationWarningrequirement - Find alternative approach (e.g., global CSS variables scoped by groupId + order:--panel-size-<groupId>-<order>)Handle customstorageprop - Decide what to do when users provide custom storage implementationOpen Questions
number[]format indefinitely?storageprop interact with the persistence script?Recordings