👀

型安全なメディアクエリライブラリを作った

2021/10/04に公開

TypeScript製のメディアクエリライブラリ「medi-q」

https://github.com/Karibash/medi-q
https://www.npmjs.com/org/medi-q

既存のメディアクエリライブラリに不満があったので自作しました。
不満点は後述するとして、まずmedi-qの使い方から解説していきます。

medi-qの解説

medi-qパッケージ郡はモノレポで開発しており、ビルドツールにpreconstructを使用しています。
preconstructはライブラリの開発に重点を置いたビルドツールで、有名な所だとemotionchangesets等で使用されています。
自前でrollup等の構築をしなくても良いのでライブラリを開発する際はオススメです。

@medi-q/core

medi-qパッケージ郡のコアとなるパッケージです。
createMediQ関数を使用する事でメディアクエリを生成する為のヘルパー関数を生成できます。
また内部ロジックに@karibash/pixel-unitsを使用している為、不正な単位は指定出来ません。

import { BreakPoints, createMediQ } from '@medi-q/core';

const breakPoints: BreakPoints = {
  tiny: '400px',
  small: '600px',
  medium: '800px',
  large: '1000px',
};

const mediQ = createMediQ(breakPoints);

ヘルパー関数へと下記のような構文で文字列を渡すことによりメディアクエリ文字列を生成出来ます。

const maxSmall = mediQ('max-small');
console.log(maxSmall);
// -> (max-width: 37.5rem)

const minSmall = mediQ('min-small');
console.log(minSmall);
// -> (min-width: 50rem)

const minSmallAndMaxMedium = mediQ('min-small-and-max-medium');
console.log(minSmallAndMaxMedium);
// -> (min-width: 37.5rem) and (max-width: 50rem)

内部ロジックにTemplateLiteralTypesを使用している為、ヘルパー関数へと不正な構文を渡す事は出来ないようになっています。
またcreateMediQの第二引数に任意の単位を指定する事により、breakPointsを任意の単位へと変換する事が可能です。
デフォルトではremへと変換するようになっています。

const mediQ = createMediQ(breakPoints, 'px');

@medi-q/react

medi-qをReactで使用する為のパッケージです。
トップレベルのコンポーネントからMediQProviderを用いてmediQを下位レベルのコンポーネントへと受け渡します。

app.tsx
import React from 'react';
import { BreakPoints, createMediQ } from '@medi-q/core';
import { MediQProvider } from '@medi-q/react';

const breakPoints: BreakPoints = {
  tiny: '400px',
  small: '600px',
  medium: '800px',
  large: '1000px',
};

const App: React.FC = () => {
  return (
    <MediQProvider mediQ={createMediQ(breakPoints)}>
      ...
    </MediQProvider>
  );
};

export default App;

任意の下位レベルコンポーネントにてuseMediQフックを使用出来るようになります。

page.tsx
import React from 'react';
import { useMediQ } from '@medi-q/react';

const Page: React.FC = () => {
  const isLessThanSmall = useMediQ('max-small');
  const isGreaterThanMedium = useMediQ('min-medium');
  const isBetweenSmallAndMedium = useMediQ('min-small-and-max-medium');
  return (
    <div>
      {isLessThanSmall && <div>isLessThanSmall</div>}
      {isGreaterThanMedium && <div>isGreaterThanMedium</div>}
      {isBetweenSmallAndMedium && <div>isBetweenSmallAndMedium</div>}
    </div>
  );
};

export default Page;

またデフォルトでは閾値のkeyはtiny, small, medium, largeの4種になっていますが、下記のような型定義ファイルを作成する事により任意のkeyへと変更可能です。

medi-q.d.ts
import '@medi-q/core';

declare module '@medi-q/core' {
  export interface BreakPoint {
    xs: string;
    sm: string;
    md: string;
    lg: string;
  }
}

@medi-q/emotion, @medi-q/styled

CSSinJS(emotion, styled-components)にてmedi-qを使用する為のパッケージです。
@medi-q/reactの使用例とほぼ変わりません、違う点はMediQProviderの代わりにThemeProviderを使用している点のみです。

app.tsx
import React from 'react';
import { BreakPoints, createMediQ } from '@medi-q/core';
import { ThemeProvider } from '@medi-q/emotion';
// import { ThemeProvider } from '@medi-q/styled';

import { theme } from './theme';

const breakPoints: BreakPoints = {
  tiny: '400px',
  small: '600px',
  medium: '800px',
  large: '1000px',
};

const App: React.FC = () => {
  return (
    <ThemeProvider theme={theme} mediQ={createMediQ(breakPoints)}>
      ...
    </ThemeProvider>
  );
};

export default App;

styeld APIのthemeからmediQを呼び出す事でcssのメディアクエリが生成されます。
またuseMediQフックも問題なく使用できます。

page.tsx
import React from 'react';
import styled from '@emotion/styled';
import { useMediQ } from '@medi-q/react';

const Wrapper = styled.div`
  background: ${props => props.theme.background};

  // @media (max-width: 50rem)
  ${props => props.theme.mediQ('max-medium')} {
    background: blue;
  }
`;

const Page: React.FC = () => {
  const isLessThanSmall = useMediQ('max-small');
  const isGreaterThanMedium = useMediQ('min-medium');
  const isBetweenSmallAndMedium = useMediQ('min-small-and-max-medium');
  return (
    <Wrapper>
      {isLessThanSmall && <div>isLessThanSmall</div>}
      {isGreaterThanMedium && <div>isGreaterThanMedium</div>}
      {isBetweenSmallAndMedium && <div>isBetweenSmallAndMedium</div>}
    </Wrapper>
  );
};

export default Page;

既存のメディアクエリライブラリへの不満

Reactでメディアクエリを扱う場合、react-responsivereact-media等のライブラリを使う事になるかと思います。
ただ、これらのライブラリには下記のような機能が備わっていません。

閾値を管理する機能が無い

閾値となるブレークポイントを管理する機能が備わっていない為、下記のように別途定数を定義するファイルを用意してimportする必要があります。

config.ts
export const BreakPoint = {
  small: '300px',
  medium: '500px',
} as const;
export type BreakPoint = typeof BreakPoint[keyof typeof BreakPoint];
app.tsx
import React from 'react';
import { useMediaQuery } from 'react-responsive';
import { BreakPoint } from './config';

const App = () => {
  const isSmall = useMediaQuery({ query: `(max-width: ${BreakPoint.small})` });

  return (
    <div>
      {isSmall ? 'small' : 'medium'}
    </div>
  );
};

毎回これを手書きするの面倒くさいですよね。

CSSinJSとの相性が微妙

既存のライブラリはCSSinJSとの併用を前提としていない為、相性があまりよくありません。
CSSinJS側で扱う為の定数を別途定義するか、isMediumようなプロパティを渡すしかありません。

app.tsx
const breakpoints = {
  tiny: '@media (max-width: 25rem)',
  small: '@media (max-width: 37.5rem)',
  medium: '@media (max-width: 50rem)',
  large: '@media (max-width: 62.5rem)',
};

const Wrapper1 = styled.div`
  background: white;

  ${breakpoints.medium} {
    background: blue;
  }
`;

const Wrapper2 = styled.div<{ isMedium: boolan }>`
  background: ${props => props.isMedium ? 'blue' : 'white'};
`;

おわりに

機能要望や不具合等あればIssue/PRを提出していただければ対応致します!
拙い文章になってしまいましたが、最後まで読んでいただきありがとうございました。
レポジトリにスターを押して貰えると今後の開発の活力になりますのでよろしくお願いします!
https://github.com/Karibash/medi-q
https://github.com/Karibash/pixel-units

Discussion