Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SSR for breadcrumbs and collapsible-nav #3970

Merged
merged 8 commits into from
Sep 2, 2020
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## [`master`](https://github.com/elastic/eui/tree/master)

No public interface changes since `28.3.0`.
**Bug fixes**

- Fix server-side rendering of `EuiBreadcrumbs` ([#3970](https://github.com/elastic/eui/pull/3970))

## [`28.3.0`](https://github.com/elastic/eui/tree/v28.3.0)

Expand Down
18 changes: 12 additions & 6 deletions src/components/breadcrumbs/breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,13 @@ export const EuiBreadcrumbs: FunctionComponent<EuiBreadcrumbsProps> = ({
...rest
}) => {
const [currentBreakpoint, setCurrentBreakpoint] = useState(
getBreakpoint(window.innerWidth)
getBreakpoint(typeof window === 'undefined' ? 1024 : window.innerWidth)
);

const functionToCallOnWindowResize = throttle(() => {
const newBreakpoint = getBreakpoint(window.innerWidth);
const newBreakpoint = getBreakpoint(
typeof window === 'undefined' ? 1024 : window.innerWidth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I totally understand the intent of this PR, but I don't think setting this to a static (arbitrary) number creates expected behavior.

Instead, is there a way to only do all this breakpoint logic/dependency stuff if the window exists? That way SSR users can apply breakpoints via CSS without it getting overridden by this static number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not really because you're not supposed to conditionally call hooks like useState. However I could change the code to use e.g. Infinity or something so that getBreakpoint() returns undefined, which it can already do according to its type. What do you think? It's a (somewhat) less arbitrary value?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that will actually return undefined because any number that is not less than the largest breakpoint will return the value for the largest breakpoint, ie. xl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can swap it for -Infinity, I checked over how values derived from window.innerWidth are used and I think it'll be OK.

(Aside: this is a philosophical brain teaser - what is the size of a server-side window?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing window from functionToCallOnWindowResize should never happen in SSR. The method is called from useEffect which doesn't run in a non-browser environment. I believe only the change to currentBreakpoint's initial state is necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞 The breakpoint logic also doesn't seem to be working anymore at all (even just on our docs).

);
if (newBreakpoint !== currentBreakpoint) {
setCurrentBreakpoint(newBreakpoint);
}
Expand All @@ -200,11 +202,15 @@ export const EuiBreadcrumbs: FunctionComponent<EuiBreadcrumbsProps> = ({

// Add window resize handlers
useEffect(() => {
window.addEventListener('resize', functionToCallOnWindowResize);
if (typeof window !== 'undefined') {
window.addEventListener('resize', functionToCallOnWindowResize);

return () => {
window.removeEventListener('resize', functionToCallOnWindowResize);
};
return () => {
window.removeEventListener('resize', functionToCallOnWindowResize);
};
} else {
return () => {};
}
}, [responsive, functionToCallOnWindowResize]);

const breadcrumbElements = breadcrumbs.map((breadcrumb, index) => {
Expand Down
32 changes: 19 additions & 13 deletions src/components/collapsible_nav/collapsible_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,16 @@ export const EuiCollapsibleNav: FunctionComponent<EuiCollapsibleNavProps> = ({
maskProps,
...rest
}) => {
const innerWidth = typeof window === 'undefined' ? 1024 : window.innerWidth;

const [flyoutID] = useState(id || htmlIdGenerator()('euiCollapsibleNav'));
const [windowIsLargeEnoughToDock, setWindowIsLargeEnoughToDock] = useState(
window.innerWidth >= dockedBreakpoint
innerWidth >= dockedBreakpoint
);
const navIsDocked = isDocked && windowIsLargeEnoughToDock;

const functionToCallOnWindowResize = throttle(() => {
if (window.innerWidth < dockedBreakpoint) {
if (innerWidth < dockedBreakpoint) {
setWindowIsLargeEnoughToDock(false);
} else {
setWindowIsLargeEnoughToDock(true);
Expand All @@ -110,19 +112,23 @@ export const EuiCollapsibleNav: FunctionComponent<EuiCollapsibleNavProps> = ({

// Watch for docked status and appropriately add/remove body classes and resize handlers
useEffect(() => {
window.addEventListener('resize', functionToCallOnWindowResize);
if (typeof window === 'undefined') {
return () => {};
} else {
window.addEventListener('resize', functionToCallOnWindowResize);

if (navIsDocked) {
document.body.classList.add('euiBody--collapsibleNavIsDocked');
} else if (isOpen) {
document.body.classList.add('euiBody--collapsibleNavIsOpen');
}
if (navIsDocked) {
document.body.classList.add('euiBody--collapsibleNavIsDocked');
} else if (isOpen) {
document.body.classList.add('euiBody--collapsibleNavIsOpen');
}

return () => {
document.body.classList.remove('euiBody--collapsibleNavIsDocked');
document.body.classList.remove('euiBody--collapsibleNavIsOpen');
window.removeEventListener('resize', functionToCallOnWindowResize);
};
return () => {
document.body.classList.remove('euiBody--collapsibleNavIsDocked');
document.body.classList.remove('euiBody--collapsibleNavIsOpen');
window.removeEventListener('resize', functionToCallOnWindowResize);
};
}
}, [navIsDocked, functionToCallOnWindowResize, isOpen]);

const onKeyDown = (event: KeyboardEvent) => {
Expand Down