-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter4.tex
889 lines (582 loc) · 62.5 KB
/
chapter4.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
\chapter{Собираем все в кучу}
В предыдущей главе мы разобрались, как создавать переиспользуемые компоненты. Теперь мы можем поговорить о том, как заставить это компоненты эффективно взаимодействовать друг с другом.
Сильной стороной React является то, что он позволяет создавать сложные интерфейсы комбинирование маленьких, тестируемых компонент. Этот подход позволяет контролировать каждый аспект приложения.
В этой главе мы рассмотрим самые распространенные паттерны и инструменты для комбинирования компонент.
Мы обсудим следующие вопросы:
\begin{itemize}
\item Как компоненты коммуницируют друг с другом посредство передачи $props$ дочерним элементам
\item Как паттерн Контейнер и Представление помогает писать более поддерживаемый код
\item Проблему, которую пытались решить миксины (mixins), но не смогли
\item Улучшение структуры приложения с Компонентами Высшего порядка
\item Библиотеку recompose и ее встроенные функции
\item Как мы можем взаимодействовать с контекстом и как избежать сильной связности компонентов с ним
\item Паттерн Function as a Child и какую пользу он может принести
\end{itemize}
\section{Взаимодействие компонентов}
\textbf{Переиспользуемые компоненты} могут использоваться внутри множества других компонент в процессе разработки вашего приложения.
Небольшие компоненты с простым интерфейсом могут составлять более сложные компоненты, которые в свою очередь являются частью еще более сложных компонент и приложения в целом.
Мы уже неоднократно видели, как в React объединяются компоненты. Для этого достаточно описать структуру из вложенных компонент внутри метода $render$:
\begin{lstlisting}
const Profile = ({ user }) => (
<div>
<Picture profileImageUrl={user.profileImageUrl} />
<UserName name={user.name} screenName={user.screenName} />
</div>
)
Profile.propTypes = {
user: React.PropTypes.object,
}
\end{lstlisting}
Например вы можете создать компонент $Profile$ путем комбинирования компонентов $Picture$ для отображения изображения профиля и $UserName$ для имени пользователя.
Таким образом вам требуется всего нескольких строчек кода для добавления новых блоков интерфейса.
После того как вы объединили компоненты как на примере выше, вы можете передавать между ними данные, используя $props$.
Props - основной способ передачи данных от родительских компонент дочерним в React.
Когда компонент передает данные другому компоненту, он является \textbf{Владельцем (Owner)} этого компоненты, не зависимо от иерархической принадлежности каждого из них.
Например, в последнем примере $Profile$ не является непосредственным родителем $Picture$ (между ними еще тег $div$), но $Profile$ является владельцем $Picture$, так как передает ему данные через параметры (прим.пер. далее я все равно буду называть такие компоненты родительскими, просто в чуть более обобщенном значение).
\subsection{Children}
Есть специальный параметр \textbf{children}, который передается от родительского компонента дочерним и доступен в методе render.
В документации React говорится, что это \textit{непрозрачный (opaque)} параметр, так как он не несет никакой информации о том, что именно внутри него содержится.
Вложенные компоненты, определенные в методе $render$, обычно получают параметры через атрибуты в JSX (или через второй аргумент метода $createElement$).
Также компонент можно определить с вложенными компонентами, в этом случае они будут доступны для него через параметр $children$.
Представим, что у нас есть компонент $Button$, у которого есть параметр $text$, отвечающий за текст на кнопке:
\begin{lstlisting}
const Button = ({ text }) => (
<button className="btn">{text}</button>
)
Button.propTypes = {
text: React.PropTypes.string,
}
\end{lstlisting}
Этот компонент можно использовать следующим образом:
\begin{lstlisting}
<Button text="Click me!" />
\end{lstlisting}
Теперь предположим, что мы хотим использовать ту же самую кнопку с тем же className, но отображать внутри нее что-то более сложное чем просто текст.
Что если мы хотим, чтобы у нас были кнопки с текстом, кнопки с изображением и кнопки с текстом и заголовком?
В множестве случаем достаточным решением будет добавить множество параметров в компонент $Button$ или создать специализированные компоненты, например $IconButton$.
Однако, если мы понимаем, что $Button$ всего лишь обертка, которая должна отображать любое содержимое, то мы можем использовать параметр $children$.
Мы можем легко поправить предыдущий вариант $Button$, чтобы иметь возможность отображать любое содержимое:
\begin{lstlisting}
const Button = ({ children }) => (
<button className="btn">{children}</button>
)
Button.propTypes = {
children: React.PropTypes.array,
}
\end{lstlisting}
Теперь мы можем использовать любые компоненты внутри $Button$, они будут подставлены вместо $children$ в JSX.
Например мы можем создать кнопку с изображением и текстом внутри:
\begin{lstlisting}
<Button>
<img src="..." alt="..." />
<span>Click me!</span>
</Button>
\end{lstlisting}
В этом случае мы получим следующий HTML код:
\begin{lstlisting}
<button className="btn">
<img src="..." alt="..." />
<span>Click me!</span>
</button>
\end{lstlisting}
Это очень удобный способ, чтобы позволить компонентам принимать любые дочерние элементы и оборачивать их предопределенным образом.
Как вы могли заметить в предыдущем примере, мы определили параметр $children$ как массив, что значит, что можно передать любое количество элементов.
Но если мы передадим только один элемент, например:
\begin{lstlisting}
<Button>
<span>Click me!</span>
</Button>
\end{lstlisting}
то получим следующую ошибку:
\begin{quote}
\textbf{ Failed prop type: Invalid prop `children` of type `object` supplied to `Button`, expected `array`.} (Неверный тип параметра: неверный тип параметра 'children' с типом 'объект', переданный компоненту $Button$; ожидается 'массив' )
\end{quote}
Это происходим из-за того, что в случае с передачей одиночного элемента React оптимизирует выделение памяти и используем сам элемент вместо создания массива с одним элементом.
Мы можем легко это поправить, указав в propTypes не только массив, но и одиночный элемент:
\begin{lstlisting}
Button.propTypes = {
children: React.PropTypes.oneOfType([
React.PropTypes.array,
React.PropTypes.element,
]),
}
\end{lstlisting}
\section{Паттерн Контейнер и Представление}
В этой главе мы рассмотрим паттерн, который поможет сделать наш код еще чище и более поддерживаемым.
Как правило, React компоненты представляют собой сочетание \textbf{логики} и \textbf{отображения}.
Под логикой мы понимаем все, что не относится к UI, т.е. такие вещи как обращения к API сервера, преобразование данных и обработку событий.
А под представлением наоборот, ту часть, которая отвечает за создание элементов для UI. Прежде всего это содержимое метода $render$.
В React есть простой и мощный паттерн, \textbf{Контейнер и Представление (Container and Presentational)}, который помогает разделить по отдельным компонентам две эти составляющие.
Заодно посмотрим в этой главе, какая еще польза, помимо переиспользуемости компонент, может быть от разделения логики и представления.
Как всегда, начнем изучения паттерна с примера, в котором он используется.
Предположим, у нас есть компонент, который получает из API геолокации долготу и широту, а затем отображает их на экране.
Для начала создадим файл $geolocation.js$ и определим в нем компонент $Geolocation$:
\begin{lstlisting}
class Geolocation extends React.Component
\end{lstlisting}
В этом компоненте создадим конструктор для определения начального состояния и привязки обработчиков событий:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
latitude: null,
longitude: null,
}
this.handleSuccess = this.handleSuccess.bind(this)
}
\end{lstlisting}
Теперь в $componentDidMount$ мы можем осуществить вызов API:
\begin{lstlisting}
componentDidMount() {
if (navigator.geolocation){
navigator.geolocation.getCurrentPosition(this.handleSuccess)
}
}
\end{lstlisting}
После того, как компонент получит данные от сервера, их можно сохранить во внутреннее состояние компонента:
\begin{lstlisting}
handleSuccess({ coords }) {
this.setState({
latitude: coords.latitude,
longitude: coords.longitude,
})
}
\end{lstlisting}
И в конце концов мы можем отобразить высоту и широту на экране через метод $render$:
\begin{lstlisting}
render() {
return (
<div>
<div>Latitude: {this.state.latitude}</div>
<div>Longitude: {this.state.longitude}</div>
</div>
)
}
\end{lstlisting}
Важно заметить, что после первой отрисовки компонента значение долготы и широты равно $null$, так как запрос на данные асинхронен и лишь инициализируется в $componentDidMount$. В реальном проекте вы скорее всего захотите в этот момент показывать какой-то индикатор загрузки, что можно сделать с помощью условных операторов, подробно разобранных в Главе 2.
Но в целом в этом компоненте нет никаких проблем и он прекрасно работает.
Предположим, что мы работаем с дизайнером над UI составляющей компонента. Не было бы это хорошей идеей, создать компонент, состоящий только из UI части, чтобы иметь возможность быстрее обсудить ее с дизайнером.
Если мы отделим представление от логики, то мы сможем без проблем добавить его в документацию на основе \textbf{Storybook}, как мы делали в одной из предыдущих глав.
Как вы уже можете догадаться, использование паттерна Контейнер и Представление предполагает разделение компонента на два, с более четкой зоной ответственности у каждого.
Если быть более точным, то в Контейнере находится вся логика и работа с данными, а в Представлении создание элементов и минимум логики. Чаще всего представление может быть выражен функциональным компонентом.
Но это не значит, что в Представлении не может быть состояния вообще. В некоторых случаях, например при создании полей ввода, может быть уместнее хранить состояние в компоненте представления.
В случае нашего примера мы отображаем на экране лишь широту и долготу, поэтому мы воспользуемся функциональным компонентом для создания Представления.
Для начала переименуем наш компонент $Geolocation$ в $GeolocationContainer$:
\begin{lstlisting}
class GeolocationContainer extends React.Component
\end{lstlisting}
А также переименуем файл, содержащий этот компонент, из $geolocation.js$ в $geolocation-container.js$.
Такой вариант наименования не высечен в камне, но является наиболее распространенным в сообществе React. К компоненты Контейнера мы добавляем в конце $Container$, а компоненте Представления оставляем оригинальное имя.
Также нам нужно изменить реализацию метода $render$, заменив все содержимое отрисовкой одного компонента:
\begin{lstlisting}
render() {
return (
<Geolocation {...this.state} />
)
}
\end{lstlisting}
Таким образом, вместо отрисовки HTML элементов мы просто отображаем компонент Представления и передаем в него свое состояние.
В \textbf{состоянии} нашего компонента высота и широта, которые по умолчанию имеют значение $null$ и меняются на координаты пользователя через \textbf{обратный вызов (calback)} после вызова API.
Чтобы передать состояние целиком, мы используем спред оператор (spread operator), который избавляет нас от необходимости указывать параметры один за другим вручную.
Теперь создадим файл $geolocation.js$, в котором создадим компонент Отображения:
\begin{lstlisting}
const Geolocation = ({ latitude, longitude }) => (
<div>
<div>Latitude: {latitude}</div>
<div>Longitude: {longitude}</div>
</div>
)
\end{lstlisting}
Функциональный компонент -- очень лаконичный способ описания интерфейса. Чистые функции однозначно отображают состояние в набор элементов.
В нашем случае компонент принимает через $state$ долготу и широту и отображает их внутри $div$ элементов.
Мы хотим следовать лучшим практикам, поэтому определим необходимый и достаточный интерфейс для этого компонента:
\begin{lstlisting}
Geolocation.propTypes = {
latitude: React.PropTypes.number,
longitude: React.PropTypes.number,
}
\end{lstlisting}
Следуя паттерну Контейнер и Представление, мы создаем глупые компоненты, которые потом можно использовать в Style guide с искусственными данными.
Если в нашем приложении в другом месте будет предполагаться такой же визуальный компонент, то нам не придется создавать компонент с нуля. Нам будет достаточно создать новый Контейнер для существующего представления, например если нам нужно будет загрузить координаты из другого сервиса.
Также другим членам команды будет проще расширить логику в контейнере, например добавить обработку ошибок, не затрагивая представления.
Также можно создать временный компонент с отображением отладочной информации для скорейшей реализации логики.
Также такой подход позволяет разделить создание компонента между разными людьми, что особенно полезно в случае больших команд и итеративных процессов разработки.
Это очень простой в использовании и полезный на практике паттерн. В большой команде он способен значительно увеличить скорость разработки и поддерживаемость написанного кода.
Но с другой стороны, использование этого паттерна без явной необходимости может значительно увеличить количество файлов и размер кодовой базы.
Не стоит начинать делить все компоненты на два сломя голову. Чаще всего стоит начинать рефакторить компонент посредством разделения логики и представления, когда они начинают быть сильно связанными.
Например в нашем примере, мы предположили, что у нас может появиться другой источник данных, для которого мы и будем создавать отдельный компонент.
Не всегда можно однозначно понять, что должно быть в Контейнере, а что в Представлении. Следующий список утверждений должен помочь вам в сложной ситуации:
Компонент Контейнера:
\begin{itemize}
\item Сосредоточен больше на поведении
\item Отображает компонент Представления
\item Выполняет асинхронные запросы к серверу и преобразует данные
\item Определяет обработчики событий
\item Создаются как наследуемые от React.Component классы
\end{itemize}
Компонент Представления:
\begin{itemize}
\item Сконцентрирован на визуальной составляющей
\item Отображает HTML разметку (и другие компоненты)
\item Получает данные от родительского компонента через $props$
\item Часто определяется через функциональные компоненты
\end{itemize}
\section{Mixins}
Компоненты отлично служат цели достижения переиспользуемости кода, но что если у нас появляется множество различных компонент, которые должны обладать общими чертами?
Очевидно, мы не хотим дублировать код, к счастью React предоставляет специальный инструмент для решения этой проблемы: \textbf{примеси (mixins)}.
В общем и целом примеси не рекомендуются к использованию, но все равно стоит знать, какие проблемы они решают и какие есть альтернативы.
Также есть не нулевая вероятность, что вас может занести на проект с кучей старого кода, где могут во всю применяться примеси, поэтому быть готовым к такому повороту лишним не будет.
Начать стоит с того, что примеси работают только с $createClass$, что является одной из причин предания их забвению.
Предположим, что вы используете $createClass$ и понимаете, что вам нужно написать один и тот же код в разные компоненты.
Например, вам нужно подписаться на событие изменения размера экрана и выполнять по нему какой-то код.
Собственно его можно написать один раз и передавать через примеси в любые компоненты. Посмотрим на примере кода.
Точкой соприкосновения компонента и примеси обычно выбирается $state$. Мы можем выделить в $state$ конкретное поле и использовать его и из компонента и из примеси. В остальном примесь описывается как обычный самостоятельный компонент.
Определим в нашей примеси начальное состояние с помощью метода $getInitialState$, в котором будет одно поле $innerWidth$:
\begin{lstlisting}
getInitialState() {
return {
innerWidth: window.innerWidth,
}
},
\end{lstlisting}
Теперь мы можем начать отслеживать изменения размера экрана, для чего подпишемся на соответствующее событие:
\begin{lstlisting}
componentDidMount() {
window.addEventListener('resize', this.handleResize)
},
\end{lstlisting}
Также мы хотим удалить этот обработчик события перед удалением компонента, чтобы избежать накопления неиспользуемых обработчиков в объекте $window$:
\begin{lstlisting}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
},
\end{lstlisting}
И осталось только создать функцию, которая будет вызваться на каждом изменении размера экрана.
В этой функции мы будем обновлять значение поля $innerWidth$ в $state$ актуальным значением, так что любой компонент, который использует эту примесь, будет перерисован как после собственного $setState$:
\begin{lstlisting}
handleResize() {
this.setState({
innerWidth: window.innerWidth,
})
},
\end{lstlisting}
Как видно из примера, создание примеси почти не отличается от создания обычного компонента.
Чтобы использовать эту примесь вместе с компонентом, достаточно добавить ее в массив $mixins$ внутри компонента:
\begin{lstlisting}
const MyComponent = React.createClass({
mixins: [WindowResize],
render() {
console.log('window.innerWidth', this.state.innerWidth)
...
},
})
\end{lstlisting}
С этого момента значение $innerWidth$ будет доступно не только в примеси, но и в компоненте, который будет перерисовывать при каждом обновлении состояния из примеси.
Само собой мы можем использовать одну и ту же примесь в множестве компонент, также и внутри одного компонента может использоваться сразу множество примесей.
Очень полезной особенностью примесей является то, что они обладают одинаковым с компонентами жизненным циклом, а также возможностью задать состояние по умолчанию.
Например, если мы используем $WindowResize$ в компоненте, в котором уже есть $componentDidMount$, то никаких коллизий не произойдет, и оба метода выполнятся.
Теперь посмотрим, в чем проблемы примесей и почему от них отказались. А в следующей части разберемся, как достигнуть такого же результата другими средствами.
Во-первых примеси часто используют внутренние функции для взаимодействия с компонентом.
Например, наша примесь $WindowResize$ может ожидать, что функция обратного вызова $handleResize$ будет реализована внутри компонента, что даст возможность разработчикам большую свободу в обработке изменения размера экрана.
Или наоборот, примесь хочет получать данные из компонента и дергает специальный метод, что-то вроде $getInnerWidth$. Само собой этот метод тоже должен быть реализован внутри компонента.
К сожалению, нет никакой возможности получить точный список методов, которые должны быть реализованы внутри компонента при добавлении примеси.
Такой подход очень сильно ухудшает поддерживаемость кода. Если компонент использует множество примесей, то при их удалении или изменении очень сложно выделить код, которые также может быть удален или требует модификации.
Также частая проблема -- конфликты имен. Очень часто примеси могут начать требовать функции или атрибуты с одинаковыми названиями. React без проблем разделяет вызовы методов жизненного цикла компонент, но совсем ничего не может сделать с вызовами пользовательских функций.
Таким образом примесям остается использовать внутреннее состояние компонент, что не очень хорошо, так как мы пытаемся наоборот сократить его использование с целью повышения переиспользуемости.
Помимо этого, может начать складываться ситуация, когда одни примеси начинают зависеть от других. Например, мы можем создать еще одну примесь \textbf{ResponsiveMixin}, которая будет получать размер экрана от \textit{WindowResize} и, в зависимости от значения, скрывать часть элементов.
Такая тесная связь примесей значительно усложняет отладку приложения и его масштабируемость.
\section{Компоненты высшего порядка}
В прошлой части мы посмотрели, как примеси помогают избежать дублирования кода при создании общего для компонент функционала, и какие проблемы это приносит.
Когда мы говорили о функциональном программировании в Главе 2, мы упоминали концепцию \textbf{Функций высшего порядка (Higher-order Functions, HoFs)}. Такая функция принимает аргументом другую функцию и возвращает ее с измененным поведением.
Посмотрим, можем ли мы применить этот подход к React компонентам и достигнуть цели переиспользования функционала множеством компонент.
В случае применения данной концепции к компонентам React они станут называться \textbf{Компонентами высшего порядка (Higher-order Components, HoCs)}
Структура любого HoC выглядит следующим образом:
\begin{lstlisting}
const HoC = Component => EnhancedComponent
\end{lstlisting}
Компонент высшего порядка -- это функция, которая принимает аргументом React компонент и возвращает его с расширенным функционалом.
Давайте начнем с простого примера, чтобы как это все выглядит на практике.
Предположим, что вам по какой-то причине необходимо добавить к множеству компонент один и тот же $className$. Никто не запрещает обойти все компоненты и в каждом поправить метод $render$, а можно создать один HoC, который решит нашу проблему:
\begin{lstlisting}
const withClassName = Component => props => (
<Component {...props} className="my-class" />
)
\end{lstlisting}
Если вы впервые встречаете эту концепцию, может быть не очевидно, как работает этот код, поэтому давайте детально разбираться, что тут происходит.
Мы определили функцию $withClassName$, которая принимает аргументом компонент $Component$ и возвращает другую функцию.
Эта созданная функция есть обыкновенный функциональный компонент, который принимает аргументом параметры \textit{props} и возвращает компонент \textit{Component}, передавая ему с помощью спред оператора все параметры и в дополнение к ним параметр \textit{className} со значением \textit{"my-class"}.
Чаще всего HoC передают параметры дальше через спред оператор. Это делается для того, чтобы HoC меньше зависел от изменения API компонента, а также чтобы только добавлять новое поведение, а поведение самого компонента затрагивать минимально.
Это очень простой пример, который скорее всего никогда не пригодился бы в реальном проекте, но на нем мы посмотрели как выглядит HoC и как его можно создать.
Теперь посмотрим, как $withClassName$ можно использовать с другими компонентами.
Прежде всего создадим компонент, который принимает в параметрах $className$ и добавляет его к $div$ элементу:
\begin{lstlisting}
const MyComponent = ({ className }) => (
<div className={className} />
)
MyComponent.propTypes = {
className: React.PropTypes.string,
}
\end{lstlisting}
Но вместо того, чтобы использовать этот компонент напрямую, мы передадим его созданному ранее HoC'у, и по сути получим новый компонент:
\begin{lstlisting}
const MyComponentWithClassName = withClassName(MyComponent)
\end{lstlisting}
Оборачивая наш компонент в $withClassName$, мы гарантируем получение компонентом параметра $className$.
Давайте теперь попробуем сделать что-то более впечатляющее и переделаем примесь $WindowResize$ из предыдущей части в HoC, чтобы снова иметь возможность переиспользоввать ее в сферическом проекте в вакууме.
Напомним, что эта примесь создавала обработчик для отслеживания изменения размера экрана и сохраняла актуальное значение в поле $innerWidth$ внутри состояния компонента.
Основная проблема была в том, что примесь использовала $state$ компонента, чтобы передавать ему актуальные данные.
Это не очень хорошее поведение, так как могут возникнуть конфликты имен внутри состояния компонента.
Прежде всего создадим функцию, которая принимает аргументом компонент:
\begin{lstlisting}
const withInnerWidth = Component => (
class extends React.Component { ... }
)
\end{lstlisting}
Возможно вы обратили наименование HoC. Это распространенная практика начинать название с $with$, если HoC расширяет параметры, которые передаются компоненту.
Помимо этого, \textit{withInnerWidth} будет возвращать компонент в виде класса, а не функциональный аналог, так как нам потребуется использовать внутреннее состояние и методы жизненного цикла.
Посмотрим, как будет выглядеть возвращенный класс.
В конструкторе мы определим начальное состояние и привяжем функцию обработчика событий к создаваемому экземпляру класса:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
innerWidth: window.innerWidth,
}
this.handleResize = this.handleResize.bind(this)
}
\end{lstlisting}
Добавление и удаление обработчиков события изменения размера экрана и обновление внутреннего состояния аналогично уже реализованному в примеси:
\begin{lstlisting}
componentDidMount() {
window.addEventListener('resize', this.handleResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
}
handleResize() {
this.setState({
innerWidth: window.innerWidth,
})
}
\end{lstlisting}
И в конце нам нужно реализовать метод $render$, в котором мы должны отобразить изначальный компонент, передавая ему новые данные:
\begin{lstlisting}
render() {
return <Component {...this.props} {...this.state} />
}
\end{lstlisting}
Можно обратить внимание, что мы через спред оператор передаем не только параметры, но также и внутреннее состояние.
По сути мы аналогично примеси храним $innerWidth$ внутри состояния, но передаем его не в $state$ изначального параметра, а в его $props$.
Как мы уже говорили в Главе 3, использование параметров чаще всего предпочтительнее состояния в разрезе повышения переиспользуемости компонент.
Теперь мы можем без проблем обернуть любой компонент, который ожидает параметр $innerWidth$ (или не ожидает, но зачем тогда все это...) в $withInnerWidth$ HoC.
Создадим для примера компонент, который получает параметр $innerWidth$ и просто выводит его значение на экран:
\begin{lstlisting}
const MyComponent = ({ innerWidth }) => {
console.log('window.innerWidth', innerWidth)
...
}
MyComponent.propTypes = {
innerWidth: React.PropTypes.number,
}
\end{lstlisting}
Который мы можем теперь обернуть функцией $withInnerWidth$ следующим образом:
\begin{lstlisting}
const MyComponentWithInnerWidth = withInnerWidth(MyComponent)
\end{lstlisting}
Есть несколько преимуществ использования этого подхода перед примесями: прежде всего мы не затрагиваем внутреннее состояние исходного компонента, а также не требуем (и не ожидаем) от него реализации каких-либо специфичных методов.
Это значит, что и исходный компонент и компонент высшего порядка не связаны, что позволяет переиспользовать их независимо друг от друга в дальнейшем.
Также передача данных через параметры позволяет уменьшить количество логики внутри исходного компонента, что упрощает его использование внутри Style Guide.
В этом случае нам достаточно создать компонент с разными размера экрана, которые мы поддерживаем внутри приложения.
Т.е. мы без проблем можем передать конкретное число через параметры так:
\begin{lstlisting}
<MyComponent innerWidth={320} />
\end{lstlisting}
Или так:
\begin{lstlisting}
<MyComponent innerWidth={960} />
\end{lstlisting}
\section{Recompose}
В предыдущей главе мы познакомились с компонентами высшего порядка и на примерах посмотрели, как они работают.
Есть библиотека \textbf{recompose}, которая предоставляет набор полезных HoC, а также удобный способ их комбинировать.
Библиотечные HoC представляют собой простые вспомогательные компоненты, которые помогают вынести часть логики из компонент, что конечно же делает их проще и более переиспользуемыми (прим. пер. за регулярками не ходи, чтобы найти здесь самое переиспользуемое слово...).
Предположим, что наш компонент получает объект с данными пользователя из API, и у этого объекта есть множество атрибутов.
Получение сложного объекта компонентом в общем случае считается не самой лучшей практикой. Если компонент получает сложный объект, то скорее всего знает о структуре этого объекта (или его части), что ведет к дополнительным поломкам при изменении структуры этого объекта.
Будет гораздо лучше, если необходимые данные будут переданы в виде отдельных параметров с примитивными значениями.
Пусть у нас есть компонент $Profile$, в котором мы хотим отобразить $username$ и $age$:
\begin{lstlisting}
const Profile = ({ user }) => (
<div>
<div>Username: {user.username}</div>
<div>Age: {user.age}</div>
</div>
)
Profile.propTypes = {
user: React.PropTypes.object,
}
\end{lstlisting}
Если мы хотим изменить интерфейс компонента, чтобы получать одиночные параметры вместо полного объекта, мы можем воспользоваться $flattenProp$ HoC из библиотеки recompose.
Посмотрим, как это работает.
Для начала поправим сам компонент, чтобы получать в нем одиночные параметры:
\begin{lstlisting}
const Profile = ({ username, age }) => (
<div>
<div>Username: {username}</div>
<div>Age: {age}</div>
</div>
)
Profile.propTypes = {
username: React.PropTypes.string,
age: React.PropTypes.number,
}
\end{lstlisting}
Теперь обернем его в $flattenProp$ HoC:
\begin{lstlisting}
const ProfileWithFlattenUser = flattenProp('user')(Profile)
\end{lstlisting}
Вы можете заметить, что мы используем этот HoC немного не так как предыдущие. Сами HoC также могут зависеть от некоторых параметров, тогда обычно сначала передают их, а потом уже компонент. В общем случае такие HoC имеют следующую структуру:
\begin{lstlisting}
const HoC = args => Component => EnhancedComponent
\end{lstlisting}
За счет этого мы можем разделить создание конкретного HoC с определенным набором параметром и использование его с копонентами:
\begin{lstlisting}
const withFlattenUser = flattenProp('user')
const ProfileWithFlattenUser = withFlattenUser(Profile)
\end{lstlisting}
Уже неплохо. Но сейчас параметры компонента завязаны на то, что это именно данные о пользователе. Давайте сделаем их более обобщенными.
В этих целях мы можем использовать $renameProp$ HoC из recompose и обновить компонент следующим образом:
\begin{lstlisting}
const Profile = ({ name, age }) => (
<div>
<div>Name: {name}</div>
<div>Age: {age}</div>
</div>
)
Profile.propTypes = {
name: React.PropTypes.string,
age: React.PropTypes.number,
}
\end{lstlisting}
Теперь мы можем приметь оба HoC (один для выделения простых параметров из объекта $user$ и второй для их переименования) к компоненту. Но множество вложенных вызовов функций будет ужасно читаться.
Тут нам на помощь приходит функция $compose$ библиотеки $recompose$.
Она делает очень простую вещь, принимает множество компонент высшего порядка и возвращает функцию (по сути тоже HoC), которая может применить их к какому-либо компоненту:
\begin{lstlisting}
const enhance = compose(
flattenProp('user'),
renameProp('username', 'name'),
withInnerWidth
)
\end{lstlisting}
Как можете увидеть, функция $compose$ значительно улучшает читаемость кода.
Мы можем объединить множество HoC, чтобы сохранить изначальный компонент настолько простым, насколько это возможно.
Но и тут важно не переусердствовать, так как каждое добавление слоя абстракции потенциально может принести проблем, а конкретно в данном случае, множество вложенных HoC могут сказаться на производительности.
Нужно держать в голове, что добавляя каждый новый HoC, вы добавляете еще один метод $render$, еще одну пачку методов жизненного цикла и выделяете на это память.
Если у вас появляются глубокие вложенные компоненты высшего порядка, то стоит задуматься, возможно у вас что-то поломалось в структуре самого приложения.
\subsection{Context}
Также компоненты высшего порядка очень удобны в работе с контекстом.
Контекст (Context) - инструмент библиотеки React, который используется во множестве библиотек, хотя был задокументирован значительно позже своего появления.
Документация до сих пор рекомендует при возможности не использовать контекст, так как он еще находится в стадии эксперимента и его API может в будущем измениться.
Однако этот инструмент очень полезен в случаях, когда нам нужно передать данные ниже по дереву элементов, но при этом не передавать их через каждый уровень в $props$.
Компоненты высшего порядка и контекст образуют очень мощную связку, так как позволяют передавать данные ниже по дереву, но при этом избежать сильной связи между компонентами и API контекста.
Схема проста - HoC получает данные из контекста, преобразует в $props$ и передает компоненту.
В этом случае компонент ничего не знает о существовании контекста и может быть переиспользован в любом месте приложения.
Помимо этого, в случае изменения API контекста, нам не придется исправлять все компоненты, нужно будет лишь поправить необходимые HoC.
В библиотеки recompose есть специальный метод, который делает процесс извлечения данных из контекста понятным и одинаковым для всех компонент.
Предположим, что у вас есть компонент $Price$, который вы используете для отображения валюты и величины.
Контекст часто используется для того, чтобы передавать общие настройки приложения всем компонентам, валюта может быть одной из таких настроек.
Давайте начнем с компонента, который сам работает с контекстом, и шаг за шагом переделаем его в более универсальный:
\begin{lstlisting}
const Price = ({ value }, { currency }) => (
<div>{currency}{value}</div>
)
Price.propTypes = {
value: React.PropTypes.number,
}
Price.contextTypes = {
currency: React.PropTypes.string,
}
\end{lstlisting}
У нас есть функциональный компонент, который принимает значение как параметр, а валюту вторым аргументом из контекста.
Также для обоих параметром мы определили типы (prop types и context types).
Как видим, его переиспользуемость сильно ограничивается потребностью в родительском элементе с $currency$ в контексте.
Например, мы не сможем без проблем использовать его в Style guide, так как не сможем передать валюту через параметры.
Прежде всего поменяем компонент так, чтобы он получал оба значения через параметры:
\begin{lstlisting}
const Price = ({ currency, value }) => (
<div>{currency}{value}</div>
)
Price.propTypes = {
currency: React.PropTypes.string,
value: React.PropTypes.number,
}
\end{lstlisting}
Конечно, нельзя просто так взять и заменить старый компонент новым, так как нет родительского элемента, который бы передал в параметрах валюту.
Но мы можем создать специальный HoC, чтобы перенести в параметры компонента данные из контекста.
Мы будем использовать функцию $getContext$ из recompose, но ничего не мешает вам написать собственную реализацию с нуля.
Создадим отдельно сам HoC с помощью $getContext$, таким образом его можно будет переиспользовать множество раз:
\begin{lstlisting}
const withCurrency = getContext({
currency: React.PropTypes.string
})
\end{lstlisting}
И мы можем применить его к нашему компоненту:
\begin{lstlisting}
const PriceWithCurrency = withCurrency(Price)
\end{lstlisting}
Теперь мы можем заменить старый компонент $Prive$ новым, и компонент будет работать без явной привязки к контексту.
Для нас это большая победа, так как нам совсем не пришлось изменять родительские компоненты, но теперь мы меньше завязаны на Context API, которое может измениться, и наш компонент стал гибче в использовании.
\section{Функция как Потомок}
Есть еще один паттерн в React, о котором точно стоит знать, он называется \textbf{Функция как Потомок (Function as Child)}
Чаще всего вместе с ним вспоминают библиотеку react-motion, о которой мы подробнее поговорим в Главе 6.
Основная идея здесь заключается в том, что мы передаем компоненту не дочерние элементы, а функция, которая сможет их создать. В этом случае через аргументы этой функции мы сможем передать дополнительные данные.
Посмотрим, как это выглядит:
\begin{lstlisting}
const FunctionAsChild = ({ children }) => children()
FunctionAsChild.propTypes = {
children: React.PropTypes.func.isRequired,
}
\end{lstlisting}
Как вы видите, компонент $FunctionAsChild$ смотрит на параметр $children$ как на функцию. И вместо того, чтобы использовать его внутри JSX, вызывает его.
Этот компонент может быть использован следующим образом:
\begin{lstlisting}
<FunctionAsChild>
{() => <div>Hello, World!</div>}
</FunctionAsChild>
\end{lstlisting}
В общем-то и весь паттерн. Мы передаем в компонент $FunctionAsChild$ функцию, которая создает текст "Hello, World!" внутри тега $div$. Эта функция будет вызвана внутри метода $render$ компонента $FunctionAsChild$.
Смысл этот подход начинает обретать тогда, когда этой функции через аргументы будут передаваться какие-либо данные.
Создадим компонент $Name$, который ожидает в $children$ функцию и передает ей строку 'World':
\begin{lstlisting}
const Name = ({ children }) => children('World')
Name.propTypes = {
children: React.PropTypes.func.isRequired,
}
\end{lstlisting}
Воспользоваться этим компонентом можно следующим образом:
\begin{lstlisting}
<Name>
{name => <div>Hello, {name}!</div>}
</Name>
\end{lstlisting}
На экране будет тот же 'Hello, World!', но на этот раз имя передается не из компонента, где функция создается, а из компонента, в котором она вызывается.
Мы разобрались, как работает этот прием, давайте посмотрим, какую пользу он приносит.
Первый плюс в том, что мы можем обернуть компоненты, передавая им переменные во время исполнения программы, в отличие от фиксированных параметром, как мы делали в HoC.
Хороший пример - компонент $Fetch$, который загружает данные из сети и передает их функции $children$:
\begin{lstlisting}
<Fetch url="...">
{data => <List data={data} />}
</Fetch>
\end{lstlisting}
Во вторых, этот подход позволяет избежать использования предустановленных имен параметров в $children$. Так как в компонент приходит функция, то их может определить разработчик, который использует этот компонент.
И также, что не менее важно, такой компонент очень удобен для переиспользования, так как он не делает никаких предположений относительно того, как будут выглядеть дочерние компоненты.
Таким образом, компонент, использующий паттерн Функция как Потомок, может быть использован в разных частях приложения с разными дочерними компонентами.
\section{Заключение}
В этой главе мы научились комбинировать наши переиспользуемые компоненты и выстраивать между ними эффективную коммуникацию.
Определение минимального и понятного интерфейса компонента через $props$ - отличный способ сделать компоненты менее связными друг с другом.
Потом мы посмотрели на самые распространенные паттерны комбинирования в React.
Первым был паттерн Контейнер и Представление, который помогает отделить логику работы компонента от его отображения. Также этот способ помогает создавать узкоспециализированные компоненты, которые следуют принципу единственности ответственности.
Мы посмотрели, как React предлагает решить проблему использования общего кода между компонентами с помощью примесей. К сожаления, этот подход помимо решения проблемы, приносит множество новых, а также негативно сказывается на поддерживаемости приложения.
Один из способов достижения той же цели -- использование компонент высшего порядка (HoC), которые являясь функцией, принимают аргументом компонент и возвращают его с расширенным функционалом.
Библиотека recompose предлагает множество удобных в использовании HoC, а также удобный способ их комбинирования, что позволяет вынести еще больше логики из наших компонент.
Также мы научились использовать контекст без сильной привязки к нему компонент за счет использования HoC.
И в конце мы разобрались, как связывать компоненты динамически с помощью паттерна Функция как Потомок.
Теперь пришло время, чтобы поговорить о загрузке данных из сети и об однонаправленном потоке данных.