Reputation: 4197
The free stacked application shell from Tailwind UI has a mobile menu the user can show on small screens:
<!--
Mobile menu, toggle classes based on menu state.
Open: "block", closed: "hidden"
-->
<div class="hidden md:hidden">
// mobile menu content
</div>
In React I've implemented this with a showMobileMenu
state variable:
<div className={`${showMobileMenu ? 'block' : 'hidden'} md:hidden`}>
// mobile menu content
</div>
If a user resizes the window (while the menu is visible) it disappears at medium
and re-appears when resizing back to small
. In this case I'd prefer to keep the menu hidden when resizing back to small
.
What is the best approach for achieving this?
My current solution is to just listen for window
resize
events and setting showMobileMenu=false
any time window.innerWidth >= 768
.
I'm a bit weary having responsive behavior managed by both JS and CSS but maybe this is typical. Just wondering if React Tailwind developers would approach this differently.
Upvotes: 1
Views: 1514
Reputation: 4197
After a few days I landed on the following approach for handling breakpoint changes declaratively and imperatively from JS.
Built on the react-responsive package I implemented a small library that provides a breakpoint context and hook (to handle the current breakpoint declaratively) and also posts a notification on breakpoint changes (to handle breakpoint changes imperatively).
If you have suggestions for improvement please let me know.
//
// breakpoints.tsx
//
import { default as React, ReactNode } from 'react'
import { useMediaQuery } from 'react-responsive'
export enum Breakpoint {
XS = 0,
SM = 640,
MD = 768,
LG = 1024,
XL = 1280,
XXL = 1536,
}
const allBreakpoints = Object.values(Breakpoint)
.filter((v) => typeof v === 'number')
.sort((a, b) => (a as number) - (b as number)) as Breakpoint[]
function activeBreakpoint(matches: boolean[]): Breakpoint {
const index = matches.lastIndexOf(true)
return allBreakpoints[index]
}
export type BreakpointEvent = CustomEvent<{ breakpoint: Breakpoint }>
function postBreakpointChangeEvent(breakpoint: Breakpoint): void {
window.dispatchEvent(
new CustomEvent('breakpointChange', {
detail: {
breakpoint,
},
})
)
}
const breakpointContext = React.createContext<Breakpoint>(Breakpoint.XS)
export function BreakpointProvider({ children }: { children: ReactNode }): JSX.Element {
const matches = allBreakpoints.map((bp: number, index: number) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useMediaQuery({ query: `(min-width: ${bp}px)` }, undefined, (macthes: boolean) => {
const breakpoint = activeBreakpoint(matches)
postBreakpointChangeEvent(breakpoint)
})
})
const breakpoint = activeBreakpoint(matches)
return <breakpointContext.Provider value={breakpoint}>{children}</breakpointContext.Provider>
}
export function useBreakpoint(): Breakpoint {
return React.useContext(breakpointContext)
}
//
// index.tsx
//
ReactDOM.render(
<React.StrictMode>
<BreakpointProvider>
<App />
</BreakpointProvider>
</React.StrictMode>,
document.getElementById('root')
)
//
// handle breakpoint changes imperatively
//
const handleBreakpointChange = (evt: Event) => {
if ((evt as BreakpointEvent).detail.breakpoint >= Breakpoint.MD) {
setMenuShowing(false)
}
}
React.useEffect(() => {
window.addEventListener('breakpointChange', handleBreakpointChange)
return () => window.removeEventListener('breakpointChange', handleBreakpointChange)
}, [])
//
// handle breakpoint changes declaratively.
// re-renders each time the breakpoint changes
//
const breakpoint = useBreakpoint()
if (breakpoint >= Breakpoint.MD) {
// render for medium and larger screens
} else {
// render for less than smaller screeens
}
Upvotes: 1