Skip to content

Instantly share code, notes, and snippets.

@ben-rogerson
Last active March 29, 2024 07:50
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ben-rogerson/b4b406dffcc18ae02f8a6c8c97bb58a8 to your computer and use it in GitHub Desktop.
Save ben-rogerson/b4b406dffcc18ae02f8a6c8c97bb58a8 to your computer and use it in GitHub Desktop.
A breakpoint provider that syncs up with your screens in tailwind.config.js. Common use is to fully remove elements from the dom rather than hide them with css.
import React from 'react';
import { useMinScreen } from './minScreen';
const App = () => {
const { min } = useMinScreen();
return (
<>
{min`md` && <div>I'll show at md and up</div>}
{!min`lg` && <div>I'll show at up to lg</div>}
{min`sm` && !min`xl` && <div>I'll show between sm and xl</div>}
</>
);
};
export default App;
import { render } from 'react-dom';
import { theme } from 'twin.macro';
import App from './App';
import { MinScreenProvider } from './minScreen';
import { theme } from 'twin.macro';
render(
<MinScreenProvider screens={theme`screens`}>
<App />
</MinScreenProvider>,
document.getElementById('root'),
);
import React, { useState, useEffect, createContext, useContext } from 'react';
/**
* Screen provider and hook for Twin
*/
const defaultValue = {};
const ScreensContext = createContext(defaultValue);
const MinScreenProvider = ({ children, screens }) => {
const [queryMatch, setQueryMatch] = useState({});
useEffect(() => {
const mediaQueryLists = {};
let isAttached = false;
const mediaData = Object.entries(screens).map(([name, media]) => [
name,
`(min-width: ${media})`
]);
const handleQueryListener = () => {
const updatedMatches = mediaData.reduce(
(acc, [name]) => ({
...acc,
[name]: Boolean(
mediaQueryLists[name] && mediaQueryLists[name].matches
)
}),
{}
);
setQueryMatch(updatedMatches);
};
if (window && window.matchMedia) {
const matches = {};
mediaData.forEach(([name, media]) => {
if (typeof media !== 'string') {
matches[name] = false;
return;
}
mediaQueryLists[name] = window.matchMedia(media);
matches[name] = mediaQueryLists[name].matches;
});
setQueryMatch(matches);
isAttached = true;
mediaData.forEach(([name, media]) => {
if (typeof media !== 'string') return;
mediaQueryLists[name].addListener(handleQueryListener);
});
}
return () => {
if (!isAttached) return;
mediaData.forEach(([name, media]) => {
if (typeof media !== 'string') return;
mediaQueryLists[name].removeListener(handleQueryListener);
});
};
}, [screens]);
return (
<ScreensContext.Provider value={queryMatch}>
{children}
</ScreensContext.Provider>
);
};
const useMinScreen = () => {
const context = useContext(ScreensContext);
if (context === defaultValue)
throw new Error('useMinScreen must be used within a MinScreenProvider');
return { min: (size) => context[size] };
};
export { useMinScreen, MinScreenProvider };
@ben-rogerson
Copy link
Author

A provider is used to avoid excess rerenders. The rerender only happens when a breakpoint changes.

@mattrossman
Copy link

mattrossman commented Jan 17, 2021

Sharing for others, here's a similar hook that doesn't use a Provider. Not as efficient if you're using it in multiple components, but react-three-fiber messes with context so this is a simpler approach in my project.

// useBreakPoint.js

import { useLayoutEffect, useState } from 'react'
import { theme } from 'twin.macro'

export const useBreakpoint = (breakpoint) => {
  const [matches, setMatches] = useState()
  useLayoutEffect(() => {
    // Create the query in useLayoutEffect since Next.js doesn't recognize 'matchMedia' during SSR
    const minWidth = theme('screens')[breakpoint]
    const mediaQueryList = matchMedia(`(min-width: ${minWidth})`)

    // Set the initial value
    setMatches(mediaQueryList.matches)

    // Listen for changes to the query match
    const onChange = (e) => setMatches(e.matches)
    mediaQueryList.addEventListener('change', onChange)

    // Cleanup listener on unmount
    return () => mediaQueryList.removeEventListener('change', onChange)
  }, [breakpoint])
  return matches
}

Usage:

const lg = useBreakPoint('lg')
return lg && <p>You're on a large screen</p>

@lukesmurray
Copy link

lukesmurray commented Jan 21, 2021

Simple component to debug the current breakpoint. Displays the current breakpoint on the bottom left of the screen.

const DebugBreakPoint = () => {
  const { min } = useMinScreen();
  return (
    <span
      css={css`
        position: absolute;
        bottom: 4px;
        left: 4px;
        z-index: 10000;
        font-family: monospace;
        color: red;
        font-size: 16px;
      `}
    >
      {!min`sm` && <div>&lt; small</div>}
      {min`sm` && !min`md` && <div>small - md</div>}
      {min`md` && !min`lg` && <div>md - lg</div>}
      {min`lg` && !min`xl` && <div>lg - xl</div>}
      {min`xl` && !min`2xl` && <div>xl - 2xl</div>}
      {min`2xl` && <div>&gt; 2xl</div>}
    </span>
  );
};

Usage

const App = () => {
  const { min } = useMinScreen();

  return (
      <DebugBreakPoint/>
  );
};

@ben-rogerson
Copy link
Author

ben-rogerson commented Jan 29, 2021

Another alternative is to use the excellent utilities from madebywild, used in their starter template.

@DavithkbY
Copy link

Any typescript solution examples for this?

@ben-rogerson
Copy link
Author

Check out mezz - particularly syncing with tailwind

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment