Skip to content

Commit 57e1ec8

Browse files
authored
Improve SSR for Tab component (#1155)
* improve SSR for Tabs * update changelog
1 parent 2aaa293 commit 57e1ec8

File tree

3 files changed

+32
-18
lines changed

3 files changed

+32
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Forward the `ref` to all components ([#1116](https://github.com/tailwindlabs/headlessui/pull/1116))
1313
- Ensure links are triggered inside `Popover Panel` components ([#1153](https://github.com/tailwindlabs/headlessui/pull/1153))
14+
- Improve SSR for `Tab` component ([#1155](https://github.com/tailwindlabs/headlessui/pull/1155))
1415

1516
## [Unreleased - @headlessui/vue]
1617

packages/@headlessui-react/src/components/tabs/tabs.tsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { focusIn, Focus } from '../../utils/focus-management'
2626
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
2727
import { useSyncRefs } from '../../hooks/use-sync-refs'
2828
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
29+
import { useLatestValue } from '../../hooks/use-latest-value'
2930

3031
interface StateDefinition {
3132
selectedIndex: number | null
@@ -103,6 +104,9 @@ let TabsContext = createContext<
103104
>(null)
104105
TabsContext.displayName = 'TabsContext'
105106

107+
let TabsSSRContext = createContext<MutableRefObject<number> | null>(null)
108+
TabsSSRContext.displayName = 'TabsSSRContext'
109+
106110
function useTabsContext(component: string) {
107111
let context = useContext(TabsContext)
108112
if (context === null) {
@@ -147,14 +151,14 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
147151

148152
let tabsRef = useSyncRefs(ref)
149153
let [state, dispatch] = useReducer(stateReducer, {
150-
selectedIndex: null,
154+
selectedIndex: typeof window === 'undefined' ? selectedIndex ?? defaultIndex : null,
151155
tabs: [],
152156
panels: [],
153157
orientation,
154158
activation,
155159
} as StateDefinition)
156160
let slot = useMemo(() => ({ selectedIndex: state.selectedIndex }), [state.selectedIndex])
157-
let onChangeRef = useRef<(index: number) => void>(() => {})
161+
let onChangeRef = useLatestValue(onChange || (() => {}))
158162

159163
useEffect(() => {
160164
dispatch({ type: ActionTypes.SetOrientation, orientation })
@@ -164,12 +168,6 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
164168
dispatch({ type: ActionTypes.SetActivation, activation })
165169
}, [activation])
166170

167-
useEffect(() => {
168-
if (typeof onChange === 'function') {
169-
onChangeRef.current = onChange
170-
}
171-
}, [onChange])
172-
173171
useEffect(() => {
174172
if (state.tabs.length <= 0) return
175173
if (selectedIndex === null && state.selectedIndex !== null) return
@@ -225,15 +223,19 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
225223
[state, dispatch]
226224
)
227225

226+
let SSRCounter = useRef(0)
227+
228228
return (
229-
<TabsContext.Provider value={providerBag}>
230-
{render({
231-
props: { ref: tabsRef, ...passThroughProps },
232-
slot,
233-
defaultTag: DEFAULT_TABS_TAG,
234-
name: 'Tabs',
235-
})}
236-
</TabsContext.Provider>
229+
<TabsSSRContext.Provider value={typeof window === 'undefined' ? SSRCounter : null}>
230+
<TabsContext.Provider value={providerBag}>
231+
{render({
232+
props: { ref: tabsRef, ...passThroughProps },
233+
slot,
234+
defaultTag: DEFAULT_TABS_TAG,
235+
name: 'Tabs',
236+
})}
237+
</TabsContext.Provider>
238+
</TabsSSRContext.Provider>
237239
)
238240
})
239241

@@ -414,6 +416,11 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
414416
ref: Ref<HTMLElement>
415417
) {
416418
let [{ selectedIndex, tabs, panels }, { dispatch }] = useTabsContext('Tab.Panel')
419+
let SSRContext = useContext(TabsSSRContext)
420+
421+
if (SSRContext !== null && selectedIndex === null) {
422+
selectedIndex = 0 // Should normally not happen, but in case the selectedIndex is null, we can default to 0.
423+
}
417424

418425
let id = `headlessui-tabs-panel-${useId()}`
419426
let internalPanelRef = useRef<HTMLElement>(null)
@@ -428,7 +435,8 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
428435
}, [dispatch, internalPanelRef])
429436

430437
let myIndex = panels.indexOf(internalPanelRef)
431-
let selected = myIndex === selectedIndex
438+
let selected =
439+
SSRContext === null ? myIndex === selectedIndex : SSRContext.current++ === selectedIndex
432440

433441
let slot = useMemo(() => ({ selected }), [selected])
434442
let propsWeControl = {

packages/playground-react/pages/tabs/tabs-with-pure-tailwind.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ export default function Home() {
4040
</Switch>
4141
</Switch.Group>
4242

43-
<Tab.Group className="flex w-full max-w-3xl flex-col" as="div" manual={manual}>
43+
<Tab.Group
44+
className="flex w-full max-w-3xl flex-col"
45+
as="div"
46+
manual={manual}
47+
defaultIndex={2}
48+
>
4449
<Tab.List className="relative z-0 flex divide-x divide-gray-200 rounded-lg shadow">
4550
{tabs.map((tab, tabIdx) => (
4651
<Tab

0 commit comments

Comments
 (0)