Привет, данная статья является продолжением вот этого поста, где я решил начать серию небольших учебных пособий по созданию UI-Kit. Там описан метод использования контекста для создания темы приложения. Сегодня я хочу дополнить приложение еще одним контекстом, который поможет нам показывать компоненты UI, основываясь на девайсе юзера.

image

В данном примере я также буду использовать модуль create-react-context.

Дерево задания будет идентичным с предыдущим. Назовем новую папку media-context.

media-context/
   MediaConsumer/
      MediaConsumer.js
      index.js
    MediaProvider/
      MediaProvider.js
      index.js
  constants.js 
  context.js
  index.js

Быстренько разберемся с index'ами.

media-context/index.js
export { MediaProvider } from './MediaProvider';
export { MediaConsumer } from './MediaConsumer';

theme-context/MediaConsumer/index.js
export { MediaConsumer } from './MediaConsumer';

theme-context/MediaProvider/index.js
export { MediaProvider } from './MediaProvider';

theme-context/context.js
Как и ранее, создаем createContext (модуль нпм).

import createContext from 'create-react-context';

const { Provider, Consumer } = createContext();

export { Provider, Consumer };

Импортируем, деструктурируем и экспортируем полученные компоненты.

media-context/constants.js
Переменные сохраним тут.

export const devices = ["mobile", "tablet", "desktop", "native"];
export const defaultDevice = "mobile";

В адаптивном подходе юзер в основном использует 4 вида девайсов — настольный, планшет, мобильный и нативное приложение (native). Другие возможные устройства: смарт-тв, часы и т.п. В данном примере будем использовать только mobile & desktop.

theme-context/MediaProvider/MediaProvider.js
Как мы уже знаем, в React.Context доступен Provider, который «кормит» консюмеров контекстом. Он позволяет им слушать и реагировать на изменения контекста.

Props Провайдераdevice и touchable. Напомню, что все значения переданные в value провайдера станут доступными для консюмеров.

import React from "react";
import PropTypes from "prop-types";

import { Provider } from "../context";
import { devices, defaultDevice } from "../constanst";

function MediaProvider(props) {
  const device = devices.includes(props.device)
    ? props.device
    : props.defaultDevice;
  return <Provider value={{ ...props, device }}>{props.children}</Provider>;
}

MediaProvider.propTypes = {
  device: PropTypes.oneOf(devices),
  touchable: false,
  children: PropTypes.node
};

MediaProvider.defaultProps = {
  device: defaultDevice,
  touchable: false
};

export { MediaProvider };

theme-context/MediaConsumer/MediaConsumer.js

Описание функциональности консюмера можно найти в предыдущей статье, но если вкратце, то Consumer реагирует на контекст и возвращает новый компонент.

В данном случае мы передаем два параметра в children: вид девайса и является ли устройство сенсорным.

import React from "react";
import PropTypes from "prop-types";
import { defaultTo } from "lodash";

import { devices, defaultDevice } from "../constanst";
import { Consumer } from '../context';

function MediaConsumer(props) {
  return (
    <Consumer>
      {media => props.children({
        touchable: defaultTo(media.touchable, props.defaultTouchable),
        device: defaultTo(media.device, props.defaultDevice),
      })}
    </Consumer>
  )
}

MediaConsumer.propTypes = {
  defaultTouchable: PropTypes.bool,
  defaultDevice: PropTypes.oneOf(devices),
  children: PropTypes.func.isRequired
};

MediaConsumer.defaultProps = {
  defaultDevice,
  defaultTouchable: false
};

export { MediaConsumer };

Теперь в нашем приложении доступен новый контекст, давайте им воспользуемся.

import React from "react";
import ReactDOM from "react-dom";
import { isMobile } from "react-device-detect";
import cx from "classnames";
import { ThemeConsumer, ThemeProvider } from "./theme-context";
import { MediaConsumer, MediaProvider } from "./media-context";

import "./styles.css";

function renderSVG({ device, touchable }) {
  if (device === "mobile" && touchable) {
    return "https://image.flaticon.com/icons/svg/124/124114.svg";
  }

  if (device === "tablet" && touchable) {
    return "https://image.flaticon.com/icons/svg/124/124099.svg";
  }

  if (device === "desktop") {
    return "https://image.flaticon.com/icons/svg/124/124092.svg";
  }
}

function MyComponent() {
  const renderMyComponent = (theme, media) => {
    const myComponentClassName = cx("my-class", {
      "my-class-dark": theme === "dark",
      "my-class-light": theme === "light"
    });
    return (
      <div className="wrapperDiv">
        <object
          className={myComponentClassName}
          data={renderSVG(media)}
          type="image/svg+xml"
        />
      </div>
    );
  };
  return (
    <MediaConsumer>
      {media => (
        <ThemeConsumer>
          {theme => renderMyComponent(theme, media)}
        </ThemeConsumer>
      )}
    </MediaConsumer>
  );
}

function App() {
  return (
    <MediaProvider
      device={isMobile ? "mobile" : "desktop"}
      touchable={isMobile}
    >
      <ThemeProvider theme="light">
        <MyComponent />
      </ThemeProvider>
    </MediaProvider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Для использования медиаконтеста нам потребуется медиадетектор (для получения информации о девайсе юзера). В нашем приложении медиадетектора нет, поэтому возьмем его отсюда — react-device-detect, а именно флаг isMobile.

Как и с контекстной темой, мы обернем наш тестовый компонент в медиапровайдере. Значение пропсов, которые провайдер передаст своему консюмеру, будут зависть от устройства клиента. Зашел с мобильного, посылаем мобильную версию сайта, с компьютера — десктопную. Рабочий код можно посмотреть тут.

Чтобы увидеть разницу, зайдите по ссылке с настольного и мобильного браузера.

Все! Теперь наше приложение имеет два контекста, которые помогут нам создавать более разносторонние и гибкие компоненты в нашем UI-Kit. В следующем посту я попробую создать комплексный компонент с использованием обоих контекстов. Надеюсь, Вы найдете статью полезной.

Спасибо.

Комментарии (9)


  1. andres_kovalev
    14.08.2019 01:22
    +1

    Хабр все больше превращается в заметки о том как кто-то что-то сделал в стиле "чтобы не забыть". Почему бы Вам в своей заметке не указать, хотя бы какую-нибудь вводную, например, чем этот подход лучше media queries? Какие есть альтернативы и т.п.?


    1. Koneru
      14.08.2019 09:10

      Media queries не работает с css-in-js.


      1. polarlord
        14.08.2019 09:51

        В какой именно библиотеке они не работают? В styled components и emotion — работают превосходно.


        1. Koneru
          14.08.2019 10:00

          Да, вы правы, ошибся, тогда тоже интересует вопрос выше.

          Чем этот подход лучше media queries?


      1. tsepen
        14.08.2019 09:57

        Работает так же как и в css-in-css


    1. camelCaseVlad Автор
      14.08.2019 10:45

      Спасибо за комментарий. Сожалею, что статья показалась Вам неинтересной.

      Не возьмусь утверждать, какой метод лучше или хуже. Но вот Вам пример из личного опыта, где я и команда решили использовать адаптивный метод. Архитектор приложения в своем решении указал, что юзер должен получать бандл только с тем кодом, который необходим ему для сессии — js, css и html, т.е. не давать клиенту код для десктопа, если он зашел с телефона.


      1. aleki
        14.08.2019 12:05

        Не вижу никакой корреляции между вашим примером и статьёй.


    1. Carduelis
      14.08.2019 10:52

      Ну, по правде говоря, это может быть лучше тем, что мы можем отслеживать изменения не размера экрана, а размера самого компонента. Вот тут открываются уже поистинне широкие возможности по кастомизации внешнего вида компонента не в зависимости от экрана монитора, а в зависимости от места, которое может быть доступно для этого компонента.


      1. Carduelis
        14.08.2019 10:53

        Но вешая observer на каждый триггер reflow не такая производительная вещь, особенно, если речь о кроссбраузерности.