Created
January 24, 2020 11:07
-
-
Save coryhouse/1dad68883620354023bd718f93f21093 to your computer and use it in GitHub Desktop.
Example App Layout React component
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
import React, { | |
isValidElement, | |
Suspense, | |
useState, | |
useRef, | |
useEffect | |
} from "react"; | |
import PropTypes from "prop-types"; | |
import { Route } from "react-router-dom"; | |
import H1 from "reusable/lib/H1"; | |
import IdleTimeout from "reusable/lib/IdleTimeout"; | |
import Alert from "reusable/lib/Alert"; | |
import SubNav from "./SubNav"; | |
import AlertContext from "./AlertContext"; | |
import styles from "./Layout.module.scss"; | |
import { useHistory } from "react-router"; | |
import HeroHeader from "./reusable/HeroHeader"; | |
import SubheadingContext from "./SubheadingContext"; | |
// Declare a route and wrap a component in the app's layout. | |
// Includes centralized alert system with a showAlert function exposed via context. | |
const Layout = ({ | |
path, | |
exact, | |
heading, | |
subheading, | |
menuIndex, | |
children, | |
scenarioSelector: ScenarioSelector | |
}) => { | |
const history = useHistory(); | |
// Stores an array of alerts to display below the subnav. | |
const [alerts, setAlerts] = useState([]); | |
// Allows to set subheading dynamically from the children context. | |
const [dynamicSubheading, setDynamicSubheading] = useState(); | |
// Either show dynamically set or default subheading if exists. | |
const currentSubheading = dynamicSubheading || subheading; | |
// Store an array of alert references. Each alert needs a ref so we can set focus it when it displays. | |
const alertsRef = useRef([]); | |
useEffect(() => { | |
// Clear alerts and subheading when the route changes. | |
const unlisten = history.listen(({ state = {} }, action) => { | |
// Reset dynamic subheading. | |
setDynamicSubheading(undefined); | |
// Skip clearing alerts if we explicitly tell it in a state object. | |
if (state.preserveAlerts) { | |
return; | |
} | |
setAlerts([]); | |
return () => unlisten(); // Clean up listen upon unmount | |
}); | |
}, [history]); | |
useEffect(() => { | |
alertsRef.current = alertsRef.current.slice(0, alerts.length); | |
// Now, focus the newest alert | |
// (The first in the array is newest since new ones are prepended to the array so that they display on top) | |
// For safety, assure the ref exists before attempting to focus it. | |
if (alerts.length > 0 && alertsRef.current[0]) { | |
alertsRef.current[0].focus(); | |
} | |
window.scroll(0, 0); | |
}, [alerts.length]); | |
function removeAlert(index) { | |
setAlerts(currentAlerts => { | |
const newAlerts = [...currentAlerts]; | |
newAlerts.splice(index, 1); | |
return newAlerts; | |
}); | |
} | |
return ( | |
<Route | |
exact={exact} | |
path={path} | |
render={props => { | |
return ( | |
<main className="meta-web-normal"> | |
{/* Wrap heading in H1 if passed in as string */} | |
{isValidElement(heading) ? heading : <H1>{heading}</H1>} | |
<SubheadingContext.Provider | |
value={{ setSubheading: setDynamicSubheading }} | |
> | |
<AlertContext.Provider value={{ setAlerts }}> | |
<SubNav activeIndex={menuIndex} /> | |
{currentSubheading ? ( | |
isValidElement(currentSubheading) ? ( | |
currentSubheading | |
) : ( | |
<HeroHeader>{currentSubheading}</HeroHeader> | |
) | |
) : ( | |
"" | |
)} | |
{alerts.map((alert, index) => ( | |
<Alert | |
key={index} | |
type={alert.type} | |
ref={el => (alertsRef.current[index] = el)} | |
onClose={() => removeAlert(index)} | |
className={styles.alert} | |
> | |
{alert.content} | |
</Alert> | |
))} | |
{/* Add React Router's props to the children composed inside the app layout. | |
This is wrapped in Suspense because it's lazy loaded by the parent component, | |
and without this the responsive nav renders in mobile mode initially. */} | |
<Suspense fallback={<></>}> | |
{React.cloneElement(children, props)} | |
</Suspense> | |
</AlertContext.Provider> | |
</SubheadingContext.Provider> | |
{ScenarioSelector && | |
process.env.REACT_APP_USE_SCENARIO_SELECTOR === "Y" && ( | |
<Suspense fallback={<></>}> | |
{/* Accept either a component reference or JSX */} | |
{isValidElement(ScenarioSelector) ? ( | |
ScenarioSelector | |
) : ( | |
<ScenarioSelector /> | |
)} | |
</Suspense> | |
)} | |
{/* Hide idle timeout in development since it's annoying */} | |
{process.env.NODE_ENV !== "development" && <IdleTimeout />} | |
</main> | |
); | |
}} | |
/> | |
); | |
}; | |
Layout.propTypes = { | |
/** Child component to compose within the layout's <main> section */ | |
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) | |
.isRequired, | |
/** React Router's exact prop. Applied to the embedded Route component. */ | |
exact: PropTypes.bool, | |
/** Page heading. Accept a string or a React element. */ | |
heading: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) | |
.isRequired, | |
/** Page subheading. Accept a string or a React element. */ | |
subheading: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), | |
/** React Router's path prop. Specifies the page's matching route. */ | |
path: PropTypes.string, | |
/** The associated tabIndex that should be styled as active when this page's route matches. */ | |
menuIndex: PropTypes.number, | |
/** The scenario selector for displaying different scenarios. */ | |
scenarioSelector: PropTypes.object | |
}; | |
Layout.defaultProps = { | |
exact: false | |
}; | |
export default Layout; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment