Mike Vosseller
Mike Vosseller

Reputation: 4197

How to reset Tailwind mobile menu from JS when breakpoint is reached

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

Answers (1)

Mike Vosseller
Mike Vosseller

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

Related Questions