Является ли connect() в листообразных компонентах признаком антипаттерна в Reaction +redux?

В настоящее время работаю над проектом "Реакция + Редукс".

Я также использую normalizr для обработки структуры данных и повторного выбора для сбора правильных данных для компонентов приложения.

Кажется, все работает хорошо.

Я нахожусь в ситуации, когда листообразному компоненту нужны данные из хранилища, и поэтому мне нужно connect() компонент для его реализации.

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

Book
    Chapters
        Chapter
            Comments
        Comments
    Comments

На разных уровнях приложения пользователи могут вносить свой вклад в контент и / или предоставлять комментарии.

Представьте, что я отрисовываю главу, в ней есть контент (и автор) и комментарии (каждый со своим контентом и автором).

В настоящее время я бы connect() а также reselect содержание главы на основе идентификатора.

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

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

Опять же, поскольку база данных нормализована с помощью нормализатора, я действительно получаю только основное содержимое и идентификатор пользователя автора комментария.

Теперь, чтобы отобразить что-то столь же простое, как значок автора, мне нужно использовать другой подключенный компонент для получения сведений о пользователе из имеющегося у меня идентификатора пользователя (как при отображении автора главы, так и для каждого отдельного автора комментария).

Компонент будет выглядеть примерно так:

@connect(
    createSelector(
        (state) => state.entities.get('users'),
        (state,props) => props.id,
        (users,id) => ( { user:users.get(id)})
    )
)
class User extends Component {

    render() {
        const { user } = this.props

        if (!user)
            return null
        return <div className='user'>
                    <Avatar name={`${user.first_name} ${user.last_name}`} size={24} round={true}  />
                </div>

    }
}

User.propTypes = {
    id : PropTypes.string.isRequired
}

export default User

И это, похоже, работает нормально.

Я попытался сделать обратное и отменить нормализацию данных на более высоком уровне, чтобы, например, данные главы встраивали данные пользователя напрямую, а не просто идентификатор пользователя, и передавали их непосредственно пользователю - но это только казалось, просто делает действительно сложные селекторы, и поскольку мои данные неизменны, они просто воссоздают объекты каждый раз.

Итак, мой вопрос, имеет ли листообразный компонент (как пользователь выше) connect() в магазин оказать знак анти-паттерна?

Я поступаю правильно или смотрю на это неправильно?

3 ответа

Решение

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

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

Границы модуля

Если вы разделили свое приложение на более мелкие модули, обычно лучше ограничить их взаимодействие небольшой поверхностью API. Например, скажем, что users а также comments в отдельных модулях, то я бы сказал, что это имеет больше смысла для <Comment> компонент для использования подключенного <User id={comment.userId}/> компонент, вместо того, чтобы захватывать пользовательские данные сам.

Принцип единой ответственности

Связанный компонент, который несет слишком большую ответственность, является запахом кода. Например, <Comment> Ответственность компонента может заключаться в том, чтобы получить данные комментария и отрисовать их, а также обработать взаимодействие с пользователем (с комментарием) в форме рассылки действий. Если ему нужно обрабатывать захват пользовательских данных и взаимодействие с пользовательским модулем, то он делает слишком много. Лучше передать соответствующие обязанности другому связанному компоненту.

Это также известно как проблема "жирного контроллера".

Спектакль

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

Redux оптимизирует подключенные компоненты, предполагая, что они чистые (т. Е. Если ссылки на объекты одинаковые, пропустите повторную визуализацию). Если вы подключите конечные узлы, то изменение состояния приведет только к повторной визуализации затронутых конечных узлов, пропуская много согласований. Это можно увидеть в действии здесь: https://github.com/mweststrate/redux-todomvc/blob/master/components/TodoItem.js

Повторное использование и тестируемость

Последнее, что я хочу упомянуть, это повторное использование и тестирование. Подключенный компонент не может быть использован повторно, если вам необходимо: 1) подключить его к другой части атома состояния, 2) передать данные напрямую (например, у меня уже есть user данные, так что я просто хочу чистый рендер). В том же ключе сложнее протестировать подключенные компоненты, потому что вам нужно сначала настроить их среду, прежде чем вы сможете их визуализировать (например, создать хранилище, передать хранилище в <Provider>, так далее.).

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

export const Comment = ({ comment }) => (
  <p>
    <User id={comment.userId}/>
   { comment.text }
  </p>
)

export default connect((state, props) => ({
  comment: state.comments[props.id]
}))(Comment)


// later on...
import Comment, { Comment as Unconnected } from './comment'

Я согласен с @Kevin. Он отвечает, что на самом деле это не анти-паттерн, но обычно есть лучшие подходы, которые облегчают отслеживание вашего потока данных.

Чтобы выполнить то, к чему вы стремитесь, не подключая листообразные компоненты, вы можете настроить селекторы для получения более полных наборов данных. Например, для вашего <Chapter/> компонент контейнера, вы можете использовать следующее:

export const createChapterDataSelector = () => {
  const chapterCommentsSelector = createSelector(
    (state) => state.entities.get('comments'),
    (state, props) => props.id,
    (comments, chapterId) => comments.filter((comment) => comment.get('chapterID') === chapterId)
  )

  return createSelector(
    (state, props) => state.entities.getIn(['chapters', props.id]),
    (state) => state.entities.get('users'),
    chapterCommentsSelector,
    (chapter, users, chapterComments) => I.Map({
      title: chapter.get('title'),
      content: chapter.get('content')
      author: users.get(chapter.get('author')),
      comments: chapterComments.map((comment) => I.Map({
        content: comment.get('content')
        author: users.get(comment.get('author'))
      }))
    })
  )
}

В этом примере используется функция, которая возвращает селектор специально для данного идентификатора главы, чтобы каждый <Chapter /> Компонент получает свой собственный запомненный селектор, если у вас их больше одного. (Несколько разных <Chapter /> компоненты, использующие один и тот же селектор, разрушат памятку). Я также разделен chapterCommentsSelector в отдельный селектор повторного выбора, чтобы он был запомнен, потому что он преобразует (фильтрует, в данном случае) данные из состояния.

В вашем <Chapter /> компонент, вы можете позвонить createChapterDataSelector(), который даст вам селектор, который предоставляет неизменную карту, содержащую все данные, которые вам понадобятся для этого <Chapter /> и все его потомки. Тогда вы можете просто пропустить опоры в обычном режиме.

Двумя основными преимуществами передачи реквизитов обычным способом React являются отслеживаемый поток данных и возможность повторного использования компонентов. <Comment /> Компонент, которому передаются реквизиты "content", "authorName" и "authorAvatar", прост в понимании и использовании. Вы можете использовать это в любом месте вашего приложения, чтобы отображать комментарии. Представьте, что ваше приложение показывает предварительный просмотр комментария во время его написания. С "тупым" компонентом это тривиально. Но если вашему компоненту требуется соответствующий объект в вашем хранилище Redux, это проблема, потому что этот комментарий может еще не существовать в хранилище, если он все еще пишется.

Тем не менее, может наступить момент, когда connect() компоненты дальше по линии. Одним из веских аргументов в этом случае будет, если вы обнаружите, что пропускаете тонну реквизита через компоненты среднего звена, которые в них не нуждаются, просто чтобы доставить их к месту назначения.

Из документов Redux:

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

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

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

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

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

Другие вопросы по тегам