IDX - это библиотека для доступа к произвольно вложенным свойствам объекта JavaScript, которые могут иметь значение NULL. По крайней мере, так это описано в документации. Что это означает на самом деле и почему это такая большая проблема в JavaScript? И почему это так важно при работе с GraphQL?

Все началось некоторое время назад. Мы решили запретить использование ненулевых полей в наших типах вывода GraphQL. Это означает, что все наши поля вывода допускают значение NULL. Есть только одно исключение - непрозрачные идентификаторы не могут быть нулевыми. Я по-прежнему считаю это одним из лучших решений, но есть один огромный недостаток: теперь все может быть нулевым. Сюрприз… :))

Почему поля, допускающие значение NULL? Главный аргумент - обратная совместимость (BC). Вы можете изменить поле, допускающее значение NULL, на поле, не допускающее значения NULL, если хотите, но не наоборот, потому что это будет разрыв BC. Но большим преимуществом является поведение GraphQL. Рассмотрим следующий запрос:

{
  allLocations(term: "PRG") {
    edges {
      node {
        locationId
        name         # <- this field will fail
      }
      cursor
    }
  }
}

Он может нормально выйти из строя, если поле name допускает значение NULL. По сути, он вернет null вместо фактического значения. Но мы все еще можем без проблем достичь locationId. Ситуация была бы совсем другой, если бы мы объявили поле thename как не допускающее значения NULL. Весь node будет нулевым, потому что мы не можем вернуть null вместо имени, и мы просто выбросим другие поля, такие как locationId, даже если они абсолютно нормальные.

Работа с полями, допускающими значение NULL

С самого начала я был создателем GraphQL API, но не совсем пользователем. Поэтому, когда у меня появилась возможность глубже изучить использование GraphQL, я начал ощущать недостатки этого подхода. По сути, самая большая проблема заключается в том, что вам нужно проверять каждое поле, чтобы узнать, существует оно или нет. И это болезненно - особенно для действительно больших наборов данных. Но я знал, что это было правильное решение, и дело было только в том, чтобы найти правильные инструменты. Представьте себе следующий объект:

const props = {
  user: {
    name: 'string',
    friends: [
      {
        user: {
          name: 'string',     // <- we want to access this field
        },
      },
    ],
  },
};

По сути, это объект, представляющий пользователя с друзьями, содержащий один и тот же объект (рекурсивным способом). Каждое поле может иметь значение NULL (или просто отсутствовать), так как же нам получить имя первого друга? Первый подход будет примерно таким:

if (props.user != null) {
  if (props.user.friends != null) {
    if (props.user.friends[0] != null) {
      if (props.user.friends[0].user != null) {
        console.log(props.user.friends[0].user.name);
      }
    }
  }
}

Вы не можете получить доступ к полю напрямую (по крайней мере, не в текущей версии JS), потому что путь может быть пустым или неопределенным где-то в пути. Есть несколько способов написать это, но всегда будет много уродливого кода. Второй подход - использовать что-то вроде Lodash, потому что каждому программисту лень писать эти условия:

console.log(
  _.get(props, 'user.friends[0].user.name')
);

Этот код выглядит намного лучше. Это совершенно верно и легко читается. Однако есть одна проблема. Вам нужно использовать эту волшебную строку в качестве второго аргумента функции, что делает ее очень хрупкой. Инструменты рефакторинга IDE и предложения не будут работать, и, что наиболее важно, не будут работать и типы потоков. По сути, все после этого вызова функции может быть сломано, и вы не узнаете об этом. Я спросил своего коллегу, что он делал с удалением Lodash из одного проекта, над которым он работал:

Как и ожидалось, наибольшим преимуществом было обнаружение неправильных типов Flow. Я предполагаю, что эти неправильные типы Flow возникли, когда мы реорганизовали компоненты на более мелкие фрагменты ...

Очень сложно придерживаться контракта, который вы написали в волшебной строке где-то в коде, особенно если этот фрагмент кода должен работать все время или тихо терпеть неудачу. Чтобы решить эту проблему, вам нужно написать что-то более сложное:

const nullPattern = /^null | null$|^[^(]* null /;
const undefinedPattern = /^undefined | undefined$|^[^(]* undefined /;

const read = (input, accessor) => {
  try {
    return accessor(input);
  } catch (error) {
    if (error instanceof TypeError) {
      if (nullPattern.test(error)) {
        return null;
      } else if (undefinedPattern.test(error)) {
        return undefined;
      }
    }
    throw error;
  }
};

console.log(
  read(props, _ => _.user.friends[0].user.name)
);

Это немного сложнее, но идея проста: сначала попробуйте получить доступ к имени пользователя на props, а если это не сработает, просто верните null или undefined в зависимости от того, какая ошибка произошла. Итак, поздравляю - теперь вы понимаете, как работает IDX, потому что я просто скопировал код из репозитория IDX. Просто замените имя функции read на idx, и все готово. Вроде, как бы, что-то вроде…

IDX в производстве

IDX идет еще дальше. Реализация, которую я показал выше, предназначена только для разработки, но есть также плагин Babel, который преобразует вызовы IDX в более производительный и менее сложный код:

var _ref;
(_ref = props) != null
  ? (_ref = _ref.user) != null
    ? (_ref = _ref.friends) != null
      ? (_ref = _ref[0]) != null
        ? (_ref = _ref.user) != null ? _ref.name : _ref
        : _ref
      : _ref
    : _ref
  : _ref;

Да, это, по сути, первый фрагмент кода, который я написал в этой статье, но сгенерированный автоматически. Таким образом, мы фактически возвращаемся к лучшей реализации, но с красивым интерфейсом (один вызов функции IDX).

Я также пытался сравнить производительность Lodash и IDX, но это не имеет особого смысла. Спектакль очень похож. Мне пришлось вызывать функции более миллиона раз, чтобы увидеть действительно существенные различия. На этом этапе Lodash полностью теряет его, потому что он становится очень экспоненциальным, но для меня это не большой аргумент. Самым большим плюсом для IDX является совместимость с Flow (или Typescript).

Будь осторожен

IDX иногда может быть очень сложным. Особенно со значениями по умолчанию. С Lodash это просто, потому что вы можете использовать последний аргумент в качестве значения по умолчанию. Но вам нужно сделать что-то вроде этого с IDX:

const name = idx(props, _ => _.user.friends[0].user.name) || ''

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

{name && <Component />}

Это работает с name равным null или undefined, но не с пустой строкой, потому что в JS:

('' && 'whatever') === ''

Это, вероятно, нормально в обычном React.js, но незаконно в React Native. Так что будьте осторожны и пишите правильные условия или значения по умолчанию… :)

Также непросто ответить на вопрос «что мне делать, если я не получаю данные?». И нет простого ответа. Иногда можно просто оставить его пустым, но иногда необходимо выдать ошибку или отобразить сообщение об ошибке, объясняющее сбой. Это действительно зависит от ситуации.

Не используйте его везде, пожалуйста

Люди склонны использовать IDX везде. Но в контексте GraphQL это иногда не имеет смысла. Я часто вижу такой код:

const id = idx(props, _ => _.id)

В чем смысл, правда? Вы можете просто написать:

props && props.id

И даже в этом может быть необходимость, если вы уверены, что будете получать props каждый раз. Я знаю, иногда он используется как механизм безопасности для предотвращения проблемы с пустыми строками (на всякий случай), но вы никогда не получите пустую строку при доступе к вложенным объектам в GraphQL. То же самое и для сверхглубоких вложенных объектов. Если вам нужно позвонить:

idx(props, _ => _.user.friends[0].user.cars[0].engine.valves)

Может, пора разложить компонент на несколько компонентов? Обычно вы получаете только то, что вам нужно для вашего компонента, и поэтому вам не нужно получать доступ к 5 уровням объекта.

Это не только для GraphQL

Собственно, в репозитории IDX вы не найдете ни единого предложения о GraphQL. Как я писал в самом начале:

IDX - это библиотека для доступа к произвольно вложенным свойствам объекта JavaScript, которые могут иметь значение NULL.

Вот и все. Он просто отлично подходит для клиентских библиотек GraphQL, и я очень рекомендую его. Спасибо, Ли Байрон, за то, что показал мне эту библиотеку во время GraphQL Summit 2017 в Сан-Франциско. Надеюсь, этот небольшой трюк сэкономит вам еще несколько часов при создании клиентских приложений с использованием (не только) GraphQL!