Pull to refresh

Comments 32

Очень хорошая статья, по ходу чтения появилось несколько вопросов:


1) А backend данные тоже отдает "нормализированными"? Могли бы вы привести пример данных, которые сервис возвращает SPA
2) Смотрели ли вы в сторону Relay или других подобных решений? Что о них думаете?
3) Разработка с redux требует написания большого количества подобного кода. Используете ли какие-нибудь инструменты, которые упрощают жизнь?


Я в своем относительно крупном проекте пришел к очень похожему способу использования редукса, как описано у вас.

Спасибо!

1) А backend данные тоже отдает «нормализированными»? Могли бы вы привести пример данных, которые сервис возвращает SPA


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

Например, на странице сотрудников, если ее запрашивать по ajax бек вернет информацию только о сотрудниках:
[
    {
        id: 1,
        name: "Артем",
        tests: [10]
    }
]


На странице тестов получаем тесты в аналогичном формате. Нам остается только преобразовать массив в объект, чтобы иметь быстрый доступ:

employees: {
	status: COMPLETE,
	list: [
		{
			id: 1, 
			name: ‘Георгий’,
			testResults: [123]
		}
	]
},
tests: {
	123: {
		id: 123,
		score: 5
		speed: 146,
		description: ‘...’
		...

	}
}


2) Смотрели ли вы в сторону Relay или других подобных решений? Что о них думаете?


Да, смотрели. Перед стартом проекта, рассматривали разные библиотеки (redux, flux-овские, relay). Остановились на redux, так как он гибкий, легкий и позволяет подстроить систему под наши нужды.

3) Разработка с redux требует написания большого количества подобного кода. Используете ли какие-нибудь инструменты, которые упрощают жизнь?


Да, согласен. Это особенно заметно при написании функционала, задачи которого «возьми с сервера, положи в стор» (и забудь :).
На текущий момент это у нас не вызывало боли или неудобств при разработке. Мы используем небольшие самописные утилиты, которые немного упрощают написание кода. Из более глобального — пока решили не брать.

Спасибо за ответ :) Я примерно к тому же тоже пришел. Предполагаю, что graphql+relay делают очень похожие вещи, только нормализацию переносят с бэкенда на фронт.


Relay у меня не зашел, Хотя концептуально очень классная штука, но уж больно неповоротливая и негибкая. Для HelloWorld отлично работает, но шаг влево или вправо и все — удачи в поисках решений

Relay у меня не зашел, Хотя концептуально очень классная штука, но уж больно неповоротливая и негибкая. Для HelloWorld отлично работает, но шаг влево или вправо и все — удачи в поисках решений


Это и послужило главной причиной выбора redux. Мы вольны строить архитектуру так, как это будет удобна нам.

я предпочитаю даже с redux делать нормализацию на фронте, не смотря на то, что бекенд пишу я же) Для нормализации использую Normalizr


тоже смотрел в сторону Relay, но он действительно выглядит неповоротливым. конечно, это палка о двух концах: endless boilerplate (redux) vs do as I tell you (relay), ничего нового

У вас во всех примерах запрос выглядит так


  fetch(`/employees/${id}`)
    // Считываем ответ сервера
    .then(checkStatus)
    // Парсим
    .then(result => result.json())

Вы действительно каждый раз вызываете проверку статуса и разбор json?


Почему не пользуетесь библиотеками, которые делают это за вас? Например, с axios код заменится на такой


axios.get('/employees/${id}')
Для статьи такой код, в отличие от использования библиотеки понятен и прост.

Но да, у нас эта цепочка используется во многих местах, так как у нас нет особой боли от этих трех строк промисов.
не холивара ради, а можно поподробнее про axios vs fetch?

Да, конечно. Начнем с того, что window.fetch — нативная штука.

Если рассмотреть axios, то в нем немало функционала, в котором мы не нуждаемся сейчас. На текущий момент времени нам более чем хватает нативной реализации\полифила + пары функций, одна из которых checkStatus (который обрабатывает статус ответа, делает throw при ошибке).

Поэтому нам просто нет нужды нести к себе в проект эту библиотеку.

fetch — это встроенный в браузер API.
Axois, также как и Superagent, или ajax-часть jQuery — это сторонние библиотеки.


Даже новое-удобное fetch-API все равно вынуждает делать много разных вещей руками


  1. Проверять статус сервера
  2. Парсить json, причем в fetch функция .json еще и асинхронная, что добавляет проблем
  3. Аналогично для отправки, нужно руками вызывать JSON.stringify() для тела POST-запроса
  4. Чтобы работать с куками, нужно выставить специальный флаг
  5. Нельзя отметить отправленный запрос

В общем, если у вас в проекте нет никакой ajax-экзотики, а вы просто передаете json на сервер и обратно, то лучше использовать какую-нибудь библиотеку. Fetch слишком абстрактный и вокруг него все равно придется городить велосипед вспомогательных функций.


А какую именно библиотеку выбрать — это уже на ваше усмотрение.

И еще в разделе про обработку ошибок в actions не понял, почему нельзя написать один try-catch на всех


export function loadEmployee(id) {
    return async dispatch => {
                dispatch(fetchEmployee());
        try {
            const res = await fetch(`/employees/${id}`);
            checkStatus(res);
            json = await getJson(res);
                    dispatch(receiveEmployee(json));
        } catch () {
            dispatch(releaseEmployee());
                    dispatch(errorRecord(t('employee.error')));       
        }
    };
} 
В статье как раз и описывалось, почему мы используем один try-catch на всех :) Поэтому вы абсолютно правы

В примере два try-catch, чтобы показать, что обрабатывая ошибки рендеринга и серверных запросов раздельно — код обработчика ошибок у нас все равно одинаковый в подавляющем большинстве случаев.
Спасибо за статью, некоторые моменты прямо очень пригодятся в скором времени :-)

Но вот вопрос:
export default compose(
    connect(
        state => state.location,
        {loadManagers}
    ),
    title('managers', 'title.base'),
    composeRestricted({user: true})
)(ManagersPage);

Я правильно понимаю, что здесь в итоге мы получим матрешку из 4х компонентов вложенных друга?
Отлично, что статья вам поможет :)

Да, мы получим матрешку. Поэтому, разделяя компоненты и добавляя декораторы, об этом стоит помнить и не «заигрываться».
А ведь graphql уже production ready…
Добавить apollo-cleint и apollo-server, немного подшаманить — и вот у вас на клиенте в redux уже есть готовый json…
Ох уж мне этот прогресс…
А что вы делаете, если вызов одного экшена должен повлиять на данные в другом, например изменить значение опции в другом состоянии модуля, если state единый, вы просто при обработке события в редьюсере выполните .set() (immutable), а если они разделены то как тогда жить? Может вы думали над такой проблемой и нашли так сказать `best practice` или удобство в изменении таких значений.
Может быть я неточно понял суть вопроса, тогда будет круто, если уточните его.

Отвечу на тот вопрос, который понял)
Здесь есть два пути.

Первый способ: один редьюсер — много разных типов action (которые относятся к разным объектам)

В редьюсерах (в импортах) прописываем список action types, на которые реагирует редьюсер.
Например, есть редьюсе progress, который отображает статус загрузки страницы (прелоадер по сути)

В этом редьюсере мы подписываемся на большое количество событий — это и загрузка сотрудников (FETCH_EMPLOYEES) и загрузка тестов (FETCH_TESTS) и т. д. Соответственно по COMPLETE_EMPLOYEES, COMPLETE_TESTS прелоадер завершается.

Второй способ: один редьюсер === один тип action (который относится только к одному объекту)

На примере того же прогресса — заводим отдельные события START_PROGRESS\END_PROGRESS. И перед загрузкой сотрудников\тестов и после их загрузки диспатчатся в том числе и эти action.

Мы у себя в разработке приняли первый вариант.
1) Он избавляет от последовательного вызова лишнего набора action.
2) Проблем между неявной связностью не получаем, так как зависимости явно прописываются в импортах (а если нужны для редьюсера кастомные данные, то тогда создаем отдельные action, да)

Второй способ от связности данных нас все равно не спасает, так как связность будет или на уровне action types, либо в логике action creators.
Вы правильно поняли вопрос. Спасибо за ответ
Если бы использовали Mobx то ваш проект состоял бы из каталогов (`models` или `stores`) и `components`. Проекты крупнее среднего, на Redux превращаются адовый бардак, с кучей размазанного по директориям кода. Поддерживать такое — головная боль. К чёрту, даже читать про такое головная боль.
Mobx — это еще один инструмент, где в моделях мы и храним состояние.

Замечу, что redux больше об идеологии и подходе к разработке. На такой идеологии можно строить и MobX приложение.

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

Спорить, почему redux\relay\mobx\angular\backbone\super-puper-framework-3000, а не что-то иное можно долго. Каждая из сторон будет в чем-то права, но в чем-то нет.

Поддержка проекта на redux это совсем не сложно. Головной болью не страдали :) И решили поделиться тем, как можно хорошо построить приложение, избежав лишних телодвижений.

Выбрав redux в качестве основного инструмента и идеологии по работе с состоянием нашего приложения мы не пожалели.

Рассматривали ли TypeScript в качестве основного языка? Насколько я знаю он используется в HH. Или даже Flow? И если да, почему остановились на ES2015?
В HH не используется TypeScript :)

На es2015 остановились просто потому, что хотим использовать нативный JavaScript. От типизации посчитали, что существенного выигрыша\профита не получим.
прекрасный автокомплит в ide и строгость кода :)
+ статический анализ и самодокументируемый код

А HH.ru я перепутал с tutu.ru :) напридумывали созвучных названий
Спасибо за статью! Мне сейчас приглянулся способ модулирования утками, не рассматривали его? если да, то почему не понравился?
Способ интересный, спасибо за ссылку!

Не натыкался на него)
Кстати, правильно ли я понимаю что судя по коду контейтера, после его отображения и запроса данных через XHR, компонент еще крутит индикатор загрузки? никак не боролись с задержкой транзишина на новый роут до подгрузки критичных данных? например redux-async-connect
код
if (!this.props.firstLoad) {
this.props.loadManagers(this.props.location);
}
Кстати, правильно ли я понимаю что судя по коду контейтера, после его отображения и запроса данных через XHR, компонент еще крутит индикатор загрузки?


Перед его рендерингом, делаем запрос за данными. В этот момент в сторе появляется информация, что нужен прелоадер: https://habrahabr.ru/company/hh/blog/310524/#comment_9820312

Поэтому, нет, не боролись, так как сейчас нас поведение устраивает
Что насчет использования react-helmet, вместо декорирования компонент?
Главный поинт в том, что react-helmet решает только работу с head документом. Декораторы же могут использоваться для значительно большего спектра задач (в статье, например, разобрано 4 способа использования).

Если же вы о декораторе title, который был приведен в качестве одного из примеров, то аналогично вопросу axios vs http — у react-helmet много функций, в которых мы не нуждаемся. В таком случае лучше сделать маленькую «утилиту»\компонент, который решит потребности. А если требования изменятся, и нужен будет больший набор возможностей, то окей, перейдем.
Sign up to leave a comment.