-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert exports to server components
- `ScriptElement` renders the script only if it has not been rendered onto the DOM already. It does this by using the prop `isGlobal` and a serverside variable. This removes the need for React Context entirely - `Balancer` is now partially serverside, rendering its script and `ClientBalancer`. Any code that doesn't explicitly have to run on the client was moved to the server
- Loading branch information
Showing
3 changed files
with
226 additions
and
198 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
'use client' | ||
|
||
import React from 'react' | ||
import {SYMBOL_KEY, SYMBOL_OBSERVER_KEY} from './constants' | ||
import type {BalancerProps} from "./index"; | ||
|
||
declare global { | ||
interface Window { | ||
[SYMBOL_KEY]: RelayoutFn | ||
} | ||
} | ||
|
||
export interface WrapperElement extends HTMLElement { | ||
[SYMBOL_OBSERVER_KEY]?: ResizeObserver | undefined | ||
} | ||
|
||
type RelayoutFn = ( | ||
id: string | number, | ||
ratio: number, | ||
wrapper?: WrapperElement | ||
) => void | ||
|
||
export const relayout: RelayoutFn = (id, ratio, wrapper) => { | ||
wrapper = | ||
wrapper || document.querySelector<WrapperElement>(`[data-br="${id}"]`) | ||
const container = wrapper.parentElement | ||
|
||
const update = (width: number) => (wrapper.style.maxWidth = width + 'px') | ||
|
||
// Reset wrapper width | ||
wrapper.style.maxWidth = '' | ||
|
||
// Get the initial container size | ||
const width = container.clientWidth | ||
const height = container.clientHeight | ||
|
||
// Synchronously do binary search and calculate the layout | ||
let lower: number = width / 2 - 0.25 | ||
let upper: number = width + 0.5 | ||
let middle: number | ||
|
||
if (width) { | ||
while (lower + 1 < upper) { | ||
middle = Math.round((lower + upper) / 2) | ||
update(middle) | ||
if (container.clientHeight === height) { | ||
upper = middle | ||
} else { | ||
lower = middle | ||
} | ||
} | ||
|
||
// Update the wrapper width | ||
update(upper * ratio + width * (1 - ratio)) | ||
} | ||
|
||
// Create a new observer if we don't have one. | ||
// Note that we must inline the key here as we use `toString()` to serialize | ||
// the function. | ||
if (!wrapper['__wrap_o']) { | ||
;(wrapper['__wrap_o'] = new ResizeObserver(() => { | ||
self.__wrap_b(0, +wrapper.dataset.brr, wrapper) | ||
})).observe(container) | ||
} | ||
} | ||
|
||
export const RELAYOUT_STR = relayout.toString() | ||
|
||
const IS_SERVER = typeof window === 'undefined' | ||
const useIsomorphicLayoutEffect = IS_SERVER | ||
? React.useEffect | ||
: React.useLayoutEffect | ||
|
||
interface ClientBalancerProps extends BalancerProps { | ||
id: string | ||
// `as` and `ratio` are required in the client component | ||
as: BalancerProps['as'], | ||
ratio: BalancerProps['ratio'], | ||
} | ||
export const ClientBalancer: React.FC<ClientBalancerProps> = ({ | ||
id, | ||
as: Wrapper, | ||
ratio, | ||
children, | ||
...props | ||
}) => { | ||
const wrapperRef = React.useRef<WrapperElement>() | ||
|
||
// Re-balance on content change and on mount/hydration. | ||
useIsomorphicLayoutEffect(() => { | ||
if (wrapperRef.current) { | ||
// Re-assign the function here as the component can be dynamically rendered, and script tag won't work in that case. | ||
;(self[SYMBOL_KEY] = relayout)(0, ratio, wrapperRef.current) | ||
} | ||
}, [children, ratio]) | ||
|
||
// Remove the observer when unmounting. | ||
useIsomorphicLayoutEffect(() => { | ||
return () => { | ||
if (!wrapperRef.current) return | ||
|
||
const resizeObserver = wrapperRef.current[SYMBOL_OBSERVER_KEY] | ||
if (!resizeObserver) return | ||
|
||
resizeObserver.disconnect() | ||
delete wrapperRef.current[SYMBOL_OBSERVER_KEY] | ||
} | ||
}, []) | ||
|
||
return ( | ||
<> | ||
<Wrapper | ||
{...props} | ||
data-br={id} | ||
data-brr={ratio} | ||
ref={wrapperRef} | ||
style={{ | ||
display: 'inline-block', | ||
verticalAlign: 'top', | ||
textDecoration: 'inherit', | ||
}} | ||
suppressHydrationWarning | ||
> | ||
{children} | ||
</Wrapper> | ||
</> | ||
) | ||
} | ||
|
||
// As Next.js adds `display: none` to `body` for development, we need to trigger | ||
// a re-balance right after the style is removed, synchronously. | ||
if (!IS_SERVER && process.env.NODE_ENV !== 'production') { | ||
const next_dev_style = document.querySelector<HTMLElement>( | ||
'[data-next-hide-fouc]' | ||
) | ||
if (next_dev_style) { | ||
const callback: MutationCallback = (mutationList) => { | ||
for (const mutation of mutationList) { | ||
for (const node of Array.from(mutation.removedNodes)) { | ||
if (node !== next_dev_style) continue | ||
|
||
observer.disconnect() | ||
const elements = | ||
document.querySelectorAll<WrapperElement>('[data-br]') | ||
|
||
for (const element of Array.from(elements)) { | ||
self[SYMBOL_KEY](0, +element.dataset.brr, element) | ||
} | ||
} | ||
} | ||
} | ||
const observer = new MutationObserver(callback) | ||
observer.observe(document.head, {childList: true}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const SYMBOL_KEY = '__wrap_b' | ||
export const SYMBOL_OBSERVER_KEY = '__wrap_o' |
Oops, something went wrong.