I have a component that takes a size prop that cannot be set using the sx prop, which I want to pass a responsive value based on the current breakpoint.
Currently I've made a makeshift solution that reads theme.breakpoints and uses window.matchMedia to match the current breakpoint.
I think that theme-ui could benefit by having something similar.
import { useThemeUI } from "theme-ui";
function matchBreakpoint(breakpoint) {
const test = window.matchMedia(`(max-width: ${breakpoint})`);
return test.matches ? true : false;
}
function getCurrentBreakpointIndex(breakpoints) {
let matchedIndex = breakpoints.findIndex(matchBreakpoint);
if (matchedIndex === -1) {
matchedIndex = breakpoints.length - 1;
}
return matchedIndex;
}
const useResponsiveValue = responsiveValues => {
const { theme: { breakpoints } } = useThemeUI();
const responsiveIndex = getCurrentBreakpointIndex(breakpoints);
// @TODO: update on window resize event
return responsiveValues[responsiveIndex];
};
// ...
const SomeComponent = () => {
const size = useResponsiveValue([10, 20, 30]);
return <OtherComponent size={size} />;
}
Thanks! I could certainly see the value in adding an optional hook package for using window.matchMedia with the theme object
I also ran into needing this! Taking some of the code @worldeggplant posted, as a starting point, this is what I ended up with:
import { useState, useEffect, useCallback } from 'react';
import { useThemeUI } from 'theme-ui';
const useResponsiveValue = array => {
const {
theme: { breakpoints },
} = useThemeUI();
const getValue = useCallback(() => {
let index = breakpoints.findIndex(
breakpoint => window.matchMedia(`(max-width: ${breakpoint})`).matches,
);
if (index === -1) {
index = breakpoints.length - 1;
}
return array[index];
}, [array, breakpoints]);
const [value, setValue] = useState(getValue);
useEffect(() => {
const onResize = () => {
const newValue = getValue();
if (value !== newValue) {
setValue(newValue);
}
};
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, [array, breakpoints, getValue, value]);
return value;
};
export default useResponsiveValue;
@jxnblk I'd be happy to make a PR for it, but I'm unsure where it should live. My first thought was just in theme-ui/hooks.js and then exported from the theme-ui package?
@dburles That'd be great! My initial thought is to have it in a separate package, maybe @theme-ui/match-media, which would keep the core bundle size down for apps that don't need to use the hook
Also note, instead of listening to window resize, I think the match media listeners could be used - e.g. https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/addListener
Also note, instead of listening to window resize, I think the match media listeners could be used - e.g. https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/addListener
That was actually my first approach, but it seems it's actually simpler this way. I'm definitely open for revisions on it.
Also, how do you go about setting up a new package in the repo?
You can create a folder and run yarn init -y to create the basic package, then run yarn at the root level of the repo to add it as a workspace. It'll also need a publishConfig, which you can look at some of the other packages to compare, but can help out on the PR if anything is unclear
You can create a folder and run yarn init -y to create the basic package, then run yarn at the root level of the repo to add it as a workspace. It'll also need a publishConfig, which you can look at some of the other packages to compare, but can help out on the PR if anything is unclear
Great, thanks!
I think I'll hold off just for the moment as I haven't quite settled on the API.
I've found in practise that two hooks might make sense, useBreakpointIndex and useResponsiveValue (which then is just const useResponsiveValue = array => array[useBreakpointIndex()]).
useBreakpointIndex is useful for simply observing changes to the breakpoints to alter state or props.
Also, useResponsiveValue might optionally accept a function to provide the theme object, useResponsiveValue(theme => [theme.space[2], theme.space[3]]) or perhaps that and an optional theme field/key: useResponsiveValue([2, 3], 'space')? ๐
I might see how these work out in practise and let them mature a little before committing on a particular API. Welcome to any thoughts you might have @worldeggplant @jxnblk.
@jxnblk I've started working on adding the @theme-ui/match-media package, however, in testing it out I've ran into a problem around context. useThemeUI is returning an empty theme object when the hook is consumed in an external application, however the same hook imported locally works as expected. I'm not exactly sure how the hook loses context.
@dburles If you're seeing empty context, 9 times out of 10 that means there are multiple instances of @emotion/core installed (though it could be something else). I'd suggest double checking by running npm ls or yarn list
Hmm it's possible, (maybe?) as I'm working within Storybook, which itself uses emotion.
verso-components$ npm ls @emotion/core
@verso/[email protected] /Users/dave/Projects/verso-components
โโโ @emotion/[email protected]
โโโฌ @storybook/[email protected]
โ โโโฌ @storybook/[email protected]
โ โโโ @emotion/[email protected] deduped
โโโฌ @storybook/[email protected]
โโโฌ @storybook/[email protected]
โโโฌ @storybook/[email protected]
โโโฌ @storybook/[email protected]
โโโฌ [email protected]
โโโ @emotion/[email protected] deduped
To illustrate the problem again:
This loses context:
import { useResponsiveValue } from '@theme-ui/match-media';
This works:
import useResponsiveValue from '../hooks/useResponsiveValue';
Is @theme-ui/match-media being imported in this monorepo? This repo doesn't use Storybook at all, and @theme-ui/match-media hasn't been published to npm yet...
Sorry I probably should have been a bit clearer, the project in which I am trying the hook out uses SB and the match-media package Iโve setup in the monorepo is npm linked, Iโll see if I can dig into the problem a bit more today
I think npm link seems to be the cause, as I have also encountered it while working on a component library and attempting to link it to a project that consumes it. Both use theme-ui. The theme provider is provided by the component library. I haven't had time to dig into it further, but perhaps there's something going on here that's more obvious to you than I.
Using npm link with webpack (and Gatsby) tends to be kind of buggy, which is why we use Yarn workspaces in this repo -- not sure if that's something that might be related to the issues you're seeing. Let me know if you're still interested in working on this
Most helpful comment
Thanks! I could certainly see the value in adding an optional hook package for using
window.matchMediawith the theme object