forked from facebook/docusaurus
/
content.tsx
101 lines (88 loc) · 2.93 KB
/
content.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
useState,
useContext,
useEffect,
type ReactNode,
type ComponentType,
} from 'react';
import {ReactContextError} from '../../utils/reactUtils';
import useShallowMemoObject from '../../hooks/useShallowMemoObject';
// This context represents a "global layout store". A component (usually a
// layout component) can request filling this store through
// `NavbarSecondaryMenuFiller`. It doesn't actually control rendering by itself,
// and this context should be considered internal implementation. The user-
// facing value comes from `display.tsx`, which takes the `component` and
// `props` stored here and renders the actual element.
export type NavbarSecondaryMenuComponent<Props> = ComponentType<Props>;
/** @internal */
export type Content =
| {
component: NavbarSecondaryMenuComponent<object>;
props: object;
}
| {component: null; props: null};
type ContextValue = [
content: Content,
setContent: React.Dispatch<React.SetStateAction<Content>>,
];
const Context = React.createContext<ContextValue | null>(null);
/** @internal */
export function NavbarSecondaryMenuContentProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const value = useState({component: null, props: null});
return (
// @ts-expect-error: this context is hard to type
<Context.Provider value={value}>{children}</Context.Provider>
);
}
/** @internal */
export function useNavbarSecondaryMenuContent(): Content {
const value = useContext(Context);
if (!value) {
throw new ReactContextError('NavbarSecondaryMenuContentProvider');
}
return value[0];
}
/**
* This component renders nothing by itself, but it fills the placeholder in the
* generic secondary menu layout. This reduces coupling between the main layout
* and the specific page.
*
* This kind of feature is often called portal/teleport/gateway/outlet...
* Various unmaintained React libs exist. Most up-to-date one:
* https://github.com/gregberge/react-teleporter
* Not sure any of those is safe regarding concurrent mode.
*/
export function NavbarSecondaryMenuFiller<P extends object>({
component,
props,
}: {
component: NavbarSecondaryMenuComponent<P>;
props: P;
}): JSX.Element | null {
const context = useContext(Context);
if (!context) {
throw new ReactContextError('NavbarSecondaryMenuContentProvider');
}
const [, setContent] = context;
// To avoid useless context re-renders, props are memoized shallowly
const memoizedProps = useShallowMemoObject(props);
useEffect(() => {
// @ts-expect-error: this context is hard to type
setContent({component, props: memoizedProps});
}, [setContent, component, memoizedProps]);
useEffect(
() => () => setContent({component: null, props: null}),
[setContent],
);
return null;
}