Как стать автором
Обновить
13
0
Сергей @redyuf

Программист веб и не только

Отправить сообщение
1. Контакты есть на гитхабе мола, регулярно в чатик приходят люди (правда и уходят также)
2. Мол — это челенж в упорстве и ломки привычных стереотипов, те, кому нужно — разобрались. Через чатик довольно сложно помочь, особенно, если человека бомбит от автора mol, от каждой детали фреймворка и он оставляет эту затею на следующий день.
3. Сборщик в mol был написан видимо одним из первых и левой пяткой, косяков там много, есть задачи на его рефакторинг, нет ресурсов.
4. Не так давно я его немного отрефачил, теперь null-ошибки, которые у тебя тогда возникали, должны пофикситься, некоторые ошибки стали информативнее. Ни я, ни Дима работу над мол прекращать не собираемся, хоть и в пол силы. Баг-репорты приветствуются, мы всегда на них реагируем.
Это частный случай, сахар над import. Типа, давайте в коде вместо:
(async () => {
  const [a, b, c] = await Promise.all([
    import('./a.mjs'),
    import('./b.mjs'),
    import('./c.mjs')
  ]);
  console.log(a, b, c);
})();
будем писать
import a from './a.mjs'
import b from './b.mjs'
import c from './c.mjs'
console.log(a, b, c);

Это удобно для подгрузки динамических ресурсов. Но, например, такое уже не сделать:
function diskUsage(dir) {
    return wait(fs.readdir(dir)).reduce((size, name) => {
        const sub = join(dir, name);
        const stat = wait(fs.stat(sub));
        if (stat.isDirectory()) return size + diskUsage(sub);
        else if (stat.isFile()) return size + stat.size;
        else return size;
    }, 0);
}

function printDiskUsage(dir) {
    console.log(`${dir}: ${diskUsage(dir)}`);
}

run(() => printDiskUsage(process.cwd()))
    .then(() => {}, err => { throw err; });

Добавлять сахар проще, чем вводить новую идиому в рантайм, это как сделать однопоточный js многопоточным, со всеми вытекающими.
Но проблему разделения мира на синхронную и асинхронную части никак не решить на async/await. Нельзя из синхронной функции вызывать асинхронную и работать с результатом.

Да, это «colored function» problem.
Проблема эта решается введением новой идиомы, волокон (fibers), которые по ряду причин не хотят пока добавлять в браузеры. Аналогии из других языков: go/goroutines, java/loom.
Мое личное мнение (я никому его не навязываю), что типы — это в целом зло.
По вашему проверка контракта и автокомплит — зло? Вот когда типы вставляют всюду где нужно и не нужно, не используют всю мощь их выведения, это пожалуй зло, но не типы как таковые.
С типами получается более сложный код, отсюда растут сложности с отладкой.
Гораздо больше примеров, когда отсутствие типов вызывает сложности с отладкой, т.к. целый класс ошибок проявляется потом. Код на typescript простой, никто не заставляет писать аннотации на каждый чих:
var a = 2
var b = '1'
console.log(a + b)

Поэтому я люблю Perl. Вот такое поведение очень хорошее:
Преобразование чего-либо в строку лучше делать явно, во избежание WAT-эффектов. Например, в js если там a = undefined, то вставится строка с undefined.
проблема еще в том, что многие путают два понятия: типизация и валидация параметров. Это — радикально разные вещи.
Чем же они так сильно разные? Тайпчекер вполне делает валидацию (т.е. проверку) передаваемых аргументов на соответствие контракту. Суть в формальном языке описания контракта, пост/предусловий, значительной части бизнес требований. Важно, что б ide и чекеры поддерживали этот язык, тогда целый класс ошибок отлавливается сразу на этапе написания кода.

А если через шину пускать только слабые связи, то напротив получается в разы меньше кода, там где раньше его было много.
Смотря с чем сравнивать, вызов логики через шину и события все-равно длиннее, чем прямой вызов метода с передачей параметров.
По вашему, это короче и понятнее
<button  data-event-click="кнопка" data-event-arg="красная">
    Текст на кнопке
</button>

чем это?
<button onClick={() => store.color = 'red'}>
    Текст на кнопке
</button>

А вот если например из за неправильного названия события просто не показывается количество человек в чате, но чат работает — то напротив. Спокойно исправляем и не спешим :
Важно увидеть кол-во человек, а пользователь видит 0 и считает, что все ок, т.к. явно не только в compile-time, но и в run-time явного обозначения ошибки не будет. Суть типизации в отсутствии ошибок с аргументами в сборке как класса. Они не пройдут дальше этапа написания кода.

Также можно изолировать ошибки в компонентах, если там что-то сбойнет — нарисовать на месте вывода кол-ва человек крестик, тогда хотя бы будет видно что что-то пошло не так, а остальные части останутся работоспособными.
как раз просто. Когда модули практически не зависят друг от друга, навигироваться требуется в пределах всего лишь модуля :)
Почему только в пределах модуля? Как раз отлаживать и читать код приходится с взаимосвязями с другими частями. Проблемы чаще возникают именно в связях, а не в самих модулях. Логично, что слабая связанность, да еще на строковых константах, тут ухудшает навигацию.

А как же интерфейсы и типизация? Как ide поможет проверить, есть ли сообщение с названием, на которое мы подписываемся или соответствуют ли получаемые/передаваемые параметры контракту. А что будет в случае рефакторинга такой архитектуры, смены названия эвента?

Используя «системную шину» событий на строковых константах, написать сложное приложение то можно, а вот рефачить с комфортом уже нельзя, по причине отсутствия контракта.

В том же редаксе тоже есть шина и события, но там формат событий (экшенов) такой, что легко приворачивается типизация с паттерн-матчингом и это еще имеет смысл.
А так есть IoC, интерфейсы, которыми достигается приемлемый уровень связанности, но с сохранением поддерживаемости кода.

система не ломается в целом, вот например Ваше:
Если ничего не происходит по нажатию кнопки из-за неправильного названия события, это тоже значит «сломалась», поведение то не соответствует ожидаемому. Смысл в нахождении ошибок как можно раньше, а отсутствие контракта препятствуют этому.

крешится при отсутствии в глобальном неймспейсе Button,
мое data-event=«button» data-event-arg=«red» не крешится при отсутствии обработчика события button.
Так может и лучше, если оно сразу крешнится, а еще лучше если typescript подскажет вам ошибку на этапе написания в ide, чем просто ничего не произойдет по нажатию кнопки где-то на проде, когда вместо button, напишут buton.

На мой взгляд, если делать что-то для сложных архитектур, то закладываться на типы надо сразу. Вроде автор vue пожалел, что не выбрал typescript изначально, т.к. потом сложнее оказалось прикручивать типы.

Да и зачем сейчас этот олдскул времен jquery? Если надо что-то простенькое, то можно и на DOM-api слабать, не сложнее будет. Для вашей задачи слабые связи то и не нужны вроде, стороннему программисту проще прокликать мышкой по явным связям и изучить части, чем вникать в систему эвентов и самому догадываться что куда приходит.
А почему решили с нуля написать, а не взять например text-mask? С нуля пилить такое под кучу фреймворков и платформ — неблагодарное занятие. Вроде кажется легко, но есть множество подводных камней, как например совместимость с мобилками, из-за которых что-нибудь, да сработает не так.

Почему такое грустное заключение, какие ожидания у вас не оправдались? Вы хорошую работу проделали, хотя бы ради собственного опыта.
Абсолютно универсальный. Вы не сможете придумать ни одного случая, когда бы он не сработал.
Оба способа можно заставить работать в любом случае. Важно количество копипаста, сцепленность компонент, возможность кастомизации поведения по-умолчанию без рефакторинга. Здесь пример на catch будет работать также, а кода будет меньше. Не надо async/await try/catch в сервисе, достаточно в Loading обернуть то, на месте чего мы хотим показать крутилку или ошибку.
Как раз все наоборот. Логика загрузки данных и обработки ошибок — в одном месте, логика рендера — в другом. И они никак не связаны. В рендере вообще даже нет никакой информации о том, что кто-то что-то загружает, что могут быть какие-то ошибки и т.п. — туда на вход подаются просто данные и все.
Рендер это функция, исключение там вполне может быть, также как и конструкции, связанные с их обработкой, обработка ошибок и статусов тесно связана с uix. То, как представлена эта обработка — в виде условий и переменных или в виде catch-подобных структур, зависит от задач, развития инструментов и требуемой масштабируемости. Никаких особых преимуществ у переменных с условиями тут нет.
Другое дело у вас — внутри рендера мы знаем о загрузке, о том что там может быть поймана ошибка и т.д.
В случае примера с mobx тоже знает, в SignIn. Мы все-равно это знание где-то разместить должны, в виде if (user.loading) или еще как. В случае с ексепшенами механизм более гибкий, т.к. появляется возможность разместить это знание где-то выше, не вдаваясь в детали дочерних компонент (наличие User в каком-то SignIn).
Компонент на мобх будет работать даже в том случае, если вообще никакой загрузки данных нет. Ваш — не будет.
С чего вы взяли, все прекрасно будет работать.
В том и прелесть, что (в отличии от вашего варианта) не надо! Компонент не знает ни о каких деталях user.
Если вы про SignIn, то нет разницы, он также знает:
if (user.loading) {
         return <div className="loading" />;
      }
      if (user.error) {
         return <div className="error">user.error</div>;
      }
Если про вышележащий — не знает и там и там.
Так выше этот пример привели. Вы все не можете его повторить в нормальном виде.
Я повторил самым первым примером, кода получилось меньше и имхо он вполне нормальный. Потом вы добавили доп. условий по кастомизации, что-то вроде: часть ошибок обработать в SignIn, а часть передать на обработку выше. На ексепшенах это просто сделать без лишних пропсов и рефакторинга.
Ваш рендер знает о загрузке данных, о запросе к апи, о наличии ошибках, занимается их перехватом и обработкой.
Не надо передергивать. О запросах к апи знает модель, также как и в mobx. Рендер знает о том, что что-то в этом поддереве компонент может загружаться и надо нарисовать на его месте загрузку и обработать ошибки, если внизу их никто не обработал.
Он знает о том, что signin делает запрос к апи. А не должен. Рендер в примере мобх этого всего не знает.
Да о чем вы вообще, компонент SignIn в mobx знает и error/loading и о экшене signIn. В моем примере как раз может не знать, это знание может быть выше, хоть в корневом компоненте. Действует принцип — все, что не обработано внизу — попадает наверх.
В шаблонах не должно быть никаких трайкотчей. В этом суть. У вас они есть. А не должно быть.
Есть класс задач, которые удобнее решать исключениями или их имитацией. А должно/не должно, это выбор здравого смысла и архитектора. Разработчики реакта озаботились этой проблемой, если вы еще не посмотрели выступление Дэна про suspense api, советую посмотреть.
вариант с mobx вполне себе универсальный,
Нет, не универсальный, более того, он нарушает инкапсуляцию и приводит к сильному зацеплению компонент.

Что б показать лоадер, надо обратиться к деталям User. Если SignIn упрятан на 10 уровней на странице, а лоадер надо нарисовать на главной. То этот User.loading придется запросить на главной.

Если в дочерних компонентах много загружаемых данных, а лоадер надо показать один, то сторы придется пробрасывать на вышележащую страницу и там комбинировать из множества loading/error один.

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

Данные User могут попадать в компонент не напрямую, а через computable — тогда придется либо пробрасывать error/loading, либо сам User этому компоненту ради отлова статусов.

продвигаемого вами вырвиглазия
А вы покажите аналогичный пример на mobx c теми условиями, которые вы напридумывали. С паттерн-матчингом, передачей обработки другому лоадеру по условию и т.д. Тогда вместе и посмеемся.

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

Реакт в целом, это не чистое SRP. Контексты и провайдеры в jsx, state в компонентах, логика в хуках никому не мешает пока, идут на компромисс. В некоторых случаях управляюший jsx читается лучше, чем if-ы с try/catch ами разбрасывать по шаблонам.
Принцип я описал — «как на ексепшенах», остальное — дело техники и конкретных требований. Никто не даст готового 100% универсального решения.
Например, в js нет нормального паттерн матчинга, но можно эмулировать его на мапе.
class Loader extends React.Component {
  state = {}
  componentDidCatch(error) {
    this.setState({error})
  }
  render() {
    const {error, match, children} = this.props
    if (!error) return <children/>

    const handler = match.find(rec => error instanceof rec[0])
    if (handler) return handler[1]

    throw error
  }
}

class SignIn extends React.Component {
  render() {
    const { user } = this.props;
    return <Loader match={[
      [MyError1, 'My error 1'],
      [MyError2, 'My error 2'],
      [Promise, 'my loading'],
    ]}>{() =>
      user.token
        ? <div className="logged_in">Logged in</div>
        : <LoginForm loginAction={user.signIn} />
    }</Loader>
  }
}

Замаскировать try/catch под jsx несложно.
class Loader extends React.Component {
  state = {}
  componentDidCatch(error) {
    this.setState({error})
  }
  render() {
    const {error, onLoading, onError, children} = this.props
    if (error intanceof Promise) return onLoading
    if (error intanceof Error) return onError
    return children()
  }
}
class SignIn extends React.Component {
    render() {
        const { user } = this.props;
        return <Loader onLoading="Custom onLoading="..." onError="Error">{() =>
            user.token
              ? <div className="logged_in">Logged in</div>
              : <LoginForm loginAction={user.signIn} />
        }</Loader>
    }
}


Был доклад про suspense api, где в примерах все очень похоже.
Не люблю абстрактные примеры в вакууме, насколько я понял началось все с примера на mobx, который кстати не будет нормально работать, т.к. token после обновления страницы пропадет. А на атомах аналогичный будет:
////////////////// MODEL //////////////////
class User {
   @mem token: AuthToken = null;
   @mem options: AuthOptions
   @mem get token(): string {
       throw api.signIn(this.options)
   }
   @action signIn(options: AuthOptions) {
       this.options = options
   }
}

////////////////// VIEW //////////////////
@observer class SignIn extends React.Component<{ user: User }> {
   render() {
      const { user } = this.props;
      try {
          if (user.token) {
             return <div className="logged_in">Logged in</div>
          }
      } catch (error) {
          if (error instanceof Promise) return <div>Custom loading...</div>
          return <div>Custom error: {error.stack}</div>
      }
      return <LoginForm loginAction={user.signIn} />
   }
}

Над try/catch можно наворотить хелперы, вроде jsx-оберток, думаю, принцип понятен.
Был бы более предметный разговор, если б сделали полноценный пример в fiddle.
Примерно так:
function loader(child, loading) {
  try {
    return child()
  } catch(error) {
    if (error instanceof Error) throw error
    return loading
  }
}

const ImgView = connect(({store}) => {
  return <p>
    {loader(
      () => <img src={store.game.url} />,
      <div>Custom loading...</div>
    )}
  </p>
})

fiddle с кастомной обработкой

Для сравнения fiddle с общей обработкой

Главное тут обработка статусов загрузки и ошибок как исключений. В альфе react 16.4 suspense api сделали похоже.
Участок кода, loading/error которого надо обрабатывать, обертывается в try/catch.
На мой взгляд, tree там не главное. Его можно воспринимать как опциональный мета-язык описания наследования и патчей.

И без tree части mol дают автоматизацию многих вещей: наследование стилей, обработка loading/error по-умолчанию, упрощение работы с асинхронностью — код, выглядит как синхронный, без async/await и промисов, с возможностью retry в случае ошибки, типобезопасные контексты.
В каждом подходе есть свои плюсы и минусы. Есть тенденция (в том же ангуларе) модели тоже делать частью зависимостей. Отсюда возникает проблема — как создавать инстансы с соотвествующим жизненным циклом. Как красиво отделять общее от частного, причем общее может быть для всего приложения и для группы компонент и а частное — для одного. Можно выделить 2 стратегии:

1. Все по-умолчанию общее, если для зависимостей в поддереве не сказано обратное (общепринятый подход).
class Some extends Component {
  getChildContext() {
    return { color: 'test' }
  }
}

Это хорошо работает, когда все зависимости — синглтоны или в них нет стейта. А частное возможно только внутри вью-моделей (React.Component или mol_view). На частное DI и контексты не работают, инициализировать модели надо в компонентах, прокидывать их части надо через пропсы в реакте или через make-конструкторы в mol_view. Компонент функцией уже быть не может в этом случае. Если используется какой-нить mobx или атомы, то надо подумать об упаковке значений, перед передачей их дочерним компонентам, ради исключения паразитного автотрекинга.

2. Я попытался развить идею, когда все по-умолчанию частное, если для поддерева не сказано обратное. Тогда не важно компонент в виде класса или функции. Система сама догадывается — вот это общая часть, вот эта общая для этой группы компонент, а эта — частная для такого-то компонента. Расчет на то, что более низкоуровневые зависимости обычно вызываются в корневых компонентах, либо регистрируются в корневом DI контейнере (router, fetcher, localStorage) т.к. в них надо передать окружение. Такую компромиссную стратегию я выбрал, т.к. не хотел перегружать DI конструкциями для управления этим добром, как в ангуларе (Self, SkipSelf, Host, provides). Вы правы в том, что тут некоторые вещи не очевидны, т.к. нет четкой грани между свой-чужой.

Живут два компонента со своими зависимостями (например, у каждой свой роутер), вдруг их владельцу она тоже понадобилась (например, параметр из роутера нужен для редиректа). И внезапно все 3 компонента начинают работать с общей зависимостью
Можно объявить их на уровне корневого компонента, можно унаследовать роутер во владельце и сказать Owner(router: OwnerRouter), тогда у него всегда будет свой экзепляр.
У нас есть два компонента, которые могут работать самостоятельно (карточка задачи и список задач). Если их просто отрендерить рядом, то у каждого из них будет свой набор зависимостей (модель, кеш данных и тп). В результате у нас получится параллельная загрузка одних и тех же данных несколько раз. Довольно странное поведение по умолчанию. Этот косяк надо ещё обнаружить, а обнаружив — влепить странный код с указанием зависимости в общем владельце, где мы её декларируем, но не используем.
Деоптимизация да, но по идее поведение не сломается, т.к. выше никто о модели не знает. В случае контекстов выбор не сильно лучше — либо мы декларируем модель TaskList в контексте корневого компоненте без использования, либо создаем в компоненте и пробрабрысаваем в пропсы, не используя мощь контекстов. Тут также можно сделать, в корневом заинжектить и в пропсы TaskView и TaskListView пробросить.
Как быть, когда во владельце нужна одна зависимость, в одном вложенном — та же, а в другом вложенном — уже другая? Пример — область сколлинга, которая провайдит позицию скролла и любой вложенный компонент может её получить. Если вложить два скролла друг в друга, то вложенные во внутренний скролл компоненты внезапно начнут получать позиции внешнего скролла.
Вроде нет принципиальной разницы с контекстами. Ситуация такая: A(scroll1) -> B(scroll1) -> C(scroll2). Как я в комменте выше писал, есть cloneComponent, который может переопределить зависимость для всего поддерева. В случае контекстов, также надо писать в B код, который заменит scroll1 на scroll2 для поддерева.
Я ставил под вопрос не саму инверсию, а публичность, излишнюю полноту контракта, жесткость зависимостей. Повторное использование кода достигается инверсией и без этого, с меньшим бойлерплейтом.

Ваш подход хорош отсутствием сторонних библиотек и поддержкой типизации. В небольших масштабах (компонент на гитхабе) результат будет лучше, т.к. пока к штатным контекстам реакта не прикрутили типы.
Вообще есть холивор между сторонниками whitebox (зависимости публичны, большинство зависимостей жесткие, при тестировании заменяются заглушками) и blackbox (зависимости могут быть приватные, зависимости мягкие и мокается только ввод-вывод: fetch, база и т.д.) подходов в тестировании. Статья на эту тему.

Стремление сделать зависимости компонента публичными, максимально через типы выразить предусловия всего, что нужно и не нужно для тестирования, как раз происходит из whitebox-подхода, когда все мокается. Переопределяемость детали для поддерева зависимостей нужна, но вот публичность деталей поддерева компонент — вопрос открытый.

Следуя whitebox, если быть честным до конца, то мы в наш компонент должны инжектить и React.createElement и все дочерние компоненты, предварительно описав их интерфейсы в props. Это было бы очень трудозатратно.

Для blackbox и мягких зависимостей, проблемы, излагаемые Симаном в приведенной вами статье, отходят на второй план. В случае тестирования компонент, ИМХО, это как раз удобнее. Публичность вообще всех деталей тут не нужна.

Ambiant context, как в примере винтажа вполне сгодится. Обратите внимание, this.$ там это не центральный реестр (часто под нечто на основе центрального реестра подразумевают service locator).

Повторюсь, на фронтенде типизация и переопределяемость для поддерева компонент важна, а вот публичность не особенно.
Допустим, в Service Locator сервисы получаются по имени интерфейса
Лучше путь в импортах + имя, только вычислять его в js, где может быть алиасинг и относительные пути, сложно. Нужны более жесткие соглашения по импортам тогда. Ссылаться через ambiant decorator можно так:
import _ from 'some'
import type {A} from 'some-lib/interfaces'

const map = new Map([
 [(_: A), new MyA()]
])

// transpiled to:
import type {A} from 'some-lib/interfaces'

const map = new Map([
 ['some-lib/interfaces/A', new MyA()]
])

Согласен, что есть потенциальная проблема.
В одном проекте норм, но чем больше масштаб, тем чаще могут возникнуть коллизии.
А как потом использовать TodosView?
Можно оборачивать в фабрику. Можно заменить React.createElement на свою реализацию с сервис локатором внутри, как я сделал.
Как передать ему другую реализацию сервиса?
Если только верхний компонент надо замочить, то напрямую, через фабрику. Если во всем поддереве зависимостей, то через конфигурацию di. Что вполне норм, т.к. в средах, где есть DI, объекты вручную обычно не создают.
const ClonedTodosView = clone(TodosView, [
  [TodosRepository, MyTodosRepository]
])
вот пример, демо
И где он создает для себя свои зависимости (где создается TodosRespository)?
В инстансе первого отображаемого компонента, который задекларировал зависимость. Родительские инстансы наследуются. Пока лучшей стратегии я не смог придумать.
Хелпер может только немного уменьшить бойлерплейт. В вашем примере, запись в services[prop] создаст утечку зависимостей, необходимых для компонента. Они будут жить и после его смерти, лучше уж клонировать services и передавать его чилдам.

Кстати как бороться с неуникальностью ключей в services?

Можете подробнее?
Экспериментируя с DI, я отказался от компонент классов. Описывая контекст во втором аргументе функций и генерируя через бабел из них метаданные, можно добиться примерно такого:
function TodosView(props, context: {todosRepository: TodosRespository}) {
  return <div>{todosRepository.todos.map( todo => <Todo todo={todo}/> )}</div>
}
TodosView.deps = [{todosRepository: TodosRespository}]

Реализация сложнее конечно чем у вас, но шаблонного кода в приложении меньше и c flow совместимо. Зависимости компонента живут вместе с ним. Класс — уникальный ключ.
Первые 2 пункта в сторону constructor injection как такового.
А какая разница? В случае вашего подхода такие же проблемы, только вместо сигнатуры конструктора сигнатура services.

Если их решить, то получится Service Locator…
Не обязательно. Можно разными способами попытаться уменьшить бойлерплейт. Например, генерацией метаданных из сигнатур конструкторов и выстраиванием на их основе DI.

services: $Logger = App.Instance;
А как это решит проблему, если синглтон App.Instance один на все компоненты? Все-равно в нем надо регистрировать зависимость.

Вот пример из статьи:
Я не нашел у вас автоматизации внедрения зависимостей. Каждую новую зависимость надо инжектить вручную. Для компонет-страниц повторяется аналогичная ситуация. В общем случае будет уже не так все просто:
const logger = new Logger()
const localStorage = new LocalStorage()
const fetcher = new Fetcher({logger, localStorage, baseUrl: '/api'})
const localizations = new Localizations({fetcher})

const services = { logger, localStorage, fetcher, localizations }

class TodosPage extends Component<{
  services: $Fetcher & $Localizations & $LocalStorage & {
    todoRepsitory?: TodoRepsitory; 
    todoFiltered?: TodoFiltered
}}> {

  todoRepsitory = this.props.services.todoRepository || new TodoRepository(this.props.services)

  services = {
    ... this.props.services,
    todoRepository: this.todoRepository,
    todoFiltered: this.props.services.todoFiltered || new TodoFiltered({...this.props.services, todoRepository: this.todoRepository})
  }
  render() {
    const {props, services} = this
    return <ul>{services.todoFiltered.todos().map(todo => ...)}</ul>
  }
}

class App extends Component<Services<Todos> & $Location> {
  render() {
    switch (this.props.services.location.get('page')) {
      case 'todos': return <TodosPage services={this.props.services}/>
    }
  }
}

Информация

В рейтинге
Не участвует
Откуда
Россия
Зарегистрирован
Активность