I have been on it for a while now, researching useRef, forwardRef just to be able to add the Skip Navigation Link to my React project. Since multiple sources say that ElementById and querySelector are not the correct way to do it, I ended up forwarding a ref to ALL my routes. Then on each page I forward the ref further to a RefMain component. It all seems like a massive complication for a small accessibility feature.
const App = () => {
const beginningOfMainRef = useRef(null);
const handleFocusMain = () => beginningOfMainRef.current.focus();
return (
<Theme>
<GlobalStyle />
<SkipNavigationButton onClick={handleFocusMain}>
Skip Navigation Links
</SkipNavigationButton>
<Header />
<Routes>
<Route path="/" element={<HomePage ref={beginningOfMainRef} />} />
<Route
path="/distributors"
element={<DistributorsPage ref={beginningOfMainRef} />}
/>
<Route
path="/contacts"
element={<ContactsPage ref={beginningOfMainRef} />}
/>
</Routes>
<Footer />
</Theme>
);
};
export default App;
Than in each page:
const AboutPage = forwardRef((props, ref) => {
return (
<RefMain ref={ref}>
<h1>hi, i'm about</h1>
</RefMain>
);
});
export default AboutPage;
And finally:
const RefMain = forwardRef((props, ref) => {
return (
<Main>
<BeginningOfMain tabIndex="-1" ref={ref}>
{props.title} main content
</BeginningOfMain>
{props.children}
</Main>
);
});
export default RefMain;
This is of course a simplified example with a reduced number of Pages. Looking at the routes with passed ref, they are anything, but dry. Am I missing some neat trick here? I tried using useRef within the RefMain component, but that left me unable to pass the ref to the handler function higher in the tree (App.js).
Since you apply RefMain
in each routed component it would better to abstract it as a layout route component instead of a wrapper component around each page's content. Instead of rendering the children
prop it renders an Outlet
component for nested routes to render their content into.
const RefMain = forwardRef((props, ref) => {
const [title, setTitle] = React.useState();
return (
<Main>
<BeginningOfMain tabIndex="-1" ref={ref}>
{title} main content
</BeginningOfMain>
<Outlet context={{ setTitle }} />
</Main>
);
});
export const RefWrapper = ({ children, title }) => {
const { setTitle } = useOutletContext();
React.useEffect(() => {
setTitle(title);
}, [title]);
return children;
};
export default RefMain;
const AboutPage = () => {
return (
<h1>hi, i'm about</h1>
);
}
export default AboutPage;
You can use another ref and div
element to set the focus back to the top of the page when the page location updates.
const App = () => {
const topRef = useRef();
const beginningOfMainRef = useRef(null);
const location = useLocation();
useEffect(() => {
topRef.current.scrollIntoView();
topRef.current.focus();
}, [location.pathname]);
const handleFocusMain = () => beginningOfMainRef.current.focus();
return (
<Theme>
<div ref={topRef} tabIndex={-1} />
<GlobalStyle />
<SkipNavigationButton onClick={handleFocusMain}>
Skip Navigation Links
</SkipNavigationButton>
<Header />
<Routes>
<Route element={<RefMain ref={beginningOfMainRef} />}>
<Route path="/" element={<HomePage />} />
<Route
path="/distributors"
element={(
<RefWrapper title="Distributors">
<DistributorsPage />
</RefWrapper>
)}
/>
<Route
path="/contacts"
element={(
<RefWrapper title="Contacts">
<ContactsPage />
</RefWrapper>
)}
/>
</Route>
</Routes>
<Footer />
</Theme>
);
};
export default App;
I didn't stand up any sandbox to test this but it is my belief it should be close to the desired behavior you describe. If there are issues let me know and I'll work on a live demo.