Theme-ui: [Feature Request]: Responsive values outside of `sx` prop

Created on 9 Aug 2019  ยท  13Comments  ยท  Source: system-ui/theme-ui

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} />;
}
enhancement help wanted

Most helpful comment

Thanks! I could certainly see the value in adding an optional hook package for using window.matchMedia with the theme object

All 13 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mxstbr picture mxstbr  ยท  3Comments

moshemo picture moshemo  ยท  3Comments

folz picture folz  ยท  3Comments

VinSpee picture VinSpee  ยท  3Comments

Everspace picture Everspace  ยท  3Comments