-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter3.tex
1108 lines (785 loc) · 65.9 KB
/
chapter3.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
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{Создаем переиспользуемые компоненты}
Для того чтобы создавать действительно переиспользуемые компоненты мы должны разобраться, какие в целом способы создания компонент предоставляет React и какой в каком случае стоит выбирать. Также относительно недавно в React появился новая способ определения компонент с помощью \textbf{функций без состояния (stateless function)}
Один из способов изучения -- рассмотреть множество примеров. Следуя по этому пути, мы начнем с примера компонента, который имеет узкую специализацию, и превратим его в переиспользуемый.
В этой главе мы рассмотрим:
\begin{itemize}
\item Различные способы создания React компонент и когда каждый из них использовать
\item Что такое stateless компоненты и чем они отличаются от stateful компонент
\item Как работает state компонент и когда следует избежать его использование
\item Почему важно определять prop types для каждого компоненты и как с помощью них создавать динамически документацию с помощью \textbf{React Docgen}
\item Примеры создания универсальных компонент из узкоспециализированных
\item Как мы можем задокументировать нашу коллекцию универсальных компонент с помощью \textbf{React Storybook}
\end{itemize}
\section{Создание классов}
Давайте начнем детально разбираться с возможностями определения компонент, которые предоставляет React.
\subsection{Фабрика createClass}
Если открыть документацию React, то первый способ создания компонента, который мы найдем, будет $React.createClass$.
Попробуем создать с его помощью простой пример:
\begin{lstlisting}
const Button = React.createClass({
render() {
return <button />
},
})
\end{lstlisting}
Мы создали простую кнопку, которую можем использовать внутри других компонент нашего приложения.
Мы даже можем заменить JSX внутри этого компонента на обычный JavaScript:
\begin{lstlisting}
const Button = React.createClass({
render() {
return React.createElement('button')
},
})
\end{lstlisting}
Теперь нам не придется использовать babel для запуска этого кода, так как мы можем открыть его напрямую в браузере.
\subsection{Наследование React.Component}
Следующий подход -- использовать классы ES2015. Ключевое слово $class$ уже неплохо поддерживается браузерами, но все равно в этом случае уже лучше транслировать код с babel.
Создадим ту же кнопку, но уже с использованием JavaScript классов:
\begin{lstlisting}
class Button extends React.Component {
render() {
return <button />
}
}
\end{lstlisting}
Этот способ создание компонент появился с версии React 0.13, и разработчики Facebook настаивают на том, что ему стоит отдать предпочтение. Активный участник React сообщества Ден Абрамов (Dan Abramov) в защиту преимущества ES2015 классов перед createClass высказал:
\textit{"ES6 classes: better the devil that's standardized (Классы ES6: лучше тот дьявол, который стандартизирован)"}
Таким разработчики библиотеки React ратуют за использование стандартных классов ES2015 вместо createClass фабрики.
\subsection{Главные отличия}
За исключением синтаксических различий есть значительные различия, которые стоит держать в голове. Давайте взглянем на них, чтобы вы могли осознанно выбирать между ними во время разработки проекта.
\subsubsection{Props}
Первое отличие относится к тому, как определить параметры и их значения по умолчанию для компонента.
Как работает передача параметров компоненту мы рассмотрим чуть позже, сейчас сконцентрируемся на том, как они определяются.
В $createClass$ мы определяем параметры, которые можно передать компоненту, в поле $propTypes$ объекта, передаваемого этой функции, а значения по умолчанию передаем с помощью функции $getDefaultProps$:
\begin{lstlisting}
const Button = React.createClass({
propTypes: {
text: React.PropTypes.string,
},
getDefaultProps() {
return {
text: 'Click me!',
}
},
render() {
return <button>{this.props.text}</button>
},
})
\end{lstlisting}
Чтобы получить тот же результат с помощью JavaScript классов, нам нужно будет немного поменять структуру:
\begin{lstlisting}
class Button extends React.Component {
render() {
return <button>{this.props.text}</button>
}
}
Button.propTypes = {
text: React.PropTypes.string,
}
Button.defaultProps = {
text: 'Click me!',
}
\end{lstlisting}
Мы вынуждены определить $propTypes$ и $defaultProps$ вне класса, так как \textbf{Class Properties} еще не являются частью стандарта языка.
Когда нам нужно определить параметры по умолчанию, нам нужно было возвращать их из специальной функции, с JavaScript классами нам достаточно определить их в параметры класса.
Основной плюс в том, что с использованием классов, мы избавились от специфичных для React функций, таких как $getDefaultProps$.
\subsubsection{State}
Еще одно различие заключается в том, как мы можем определить начальное состояние компонента.
Аналогично с определением параметров в $createClass$ мы используем функция, а в ES2015 классе атрибут экземпляра класса.
Посмотрим на пример:
\begin{lstlisting}
const Button = React.createClass({
getInitialState() {
return {
text: 'Click me!',
}
},
render() {
return <button>{this.state.text}</button>
},
})
\end{lstlisting}
Метод $getInitialState$ должен вернуть объект с начальным состоянием для каждого элемента.
В случае с классом мы должны определить начальное состояние в поле state экземпляра класса, и происходит это в момент вызова конструктора:
\begin{lstlisting}
class Button extends React.Component {
constructor(props) {
super(props)
this.state = {
text: 'Click me!',
}
}
render() {
return <button>{this.state.text}</button>
}
}
\end{lstlisting}
Оба способа эквиваленты, единственное, в последнем случае JavaScript классы позволяют нам избавиться от специфичного метода React API.
В ES2015 мы должны также вызвать конструктор родительского класса, чтобы он был проинициализирован и мы могли работать с $this$.
\subsubsection{Autobinding}
У $createClass$ есть очень удобная фича, которая скрывает механизм работы JavaScript и может вводить в заблуждение начинающих разработчиков. Эта фича позволяет привязывать к компоненту обработчики событий, ожидая, что в момент вызова этого обработчика в $this$ попадет сам компонент.
Подробно об обработчиках событий мы поговорим позднее в \textit{Главе 6}. Сейчас нас интересует только то, как эти обработчики привязываются к компонентам.
Посмотрим на пример:
\begin{lstlisting}
const Button = React.createClass({
handleClick() {
console.log(this)
},
render() {
return <button onClick={this.handleClick} />
},
})
\end{lstlisting}
При использовании $createClass$ мы можем спокойно использовать $this$, что позволяет нам использовать другие методы компонента, такие как $this.setState()$.
Посмотрим, насколько отличается поведение $this$ при наследовании $React.Component$ и как нам добиться такого же поведения.
Мы можем создать компонент следующим образом:
\begin{lstlisting}
class Button extends React.Component {
handleClick() {
console.log(this)
}
render() {
return <button onClick={this.handleClick} />
}
}
\end{lstlisting}
Если мы вызовем этот код, то результатом нажатия на кнопку будет $null$. Это связано с тем, что наша функция передается обработчику событий и мы теряем связь с текущим компонентом.
Это не значит, что мы не можем использовать обработчики событий от слова совсем, но нам придется связывать с компонентом вручную.
Разберемся какие есть возможности связывания функций и компонент.
Возможно вы уже знаете, что стрелочные функции ES2015 автоматически связываются с текущим $this$ контекста, где эта функция создается.
Посмотрим на пример стрелочной функции:
\begin{lstlisting}
() => this.setState()
\end{lstlisting}
Если мы транслируем этот код с помощью Babel, то получим:
\begin{lstlisting}
var _this = this;
(function () {
return _this.setState();
});
\end{lstlisting}
Как вы уже можете догадаться, одно из решений проблемы \textbf{автоматического связывания} -- использование стрелочных функций. Посмотрим, как это работает:
\begin{lstlisting}
class Button extends React.Component {
handleClick() {
console.log(this)
}
render() {
return <button onClick={() => this.handleClick()} />
}
}
\end{lstlisting}
Этот код будет работать корректно. Но у данного варианта есть проблемы с производительностью, чтобы понять почему, нужно разобраться, как этот код работает.
Создание стрелочной функции внутри $render$ метода влечет непредвиденный посторонний эффект. Эта функция пересоздается на каждом вызове $render$, что может происходить довольно часто.
Помимо того, что мы каждый раз пересоздаем не самый легкий объект функции, мы также передаем этот объект дочерним элементам, заставляя их пересоздаваться.
Лучшим решением будет привязывать эти функции к компоненту в момент вызова конструктора. В этом случае функция и пересоздаваться не будет и будет иметь нужный нам контекст:
\begin{lstlisting}
class Button extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this)
}
render() {
return <button onClick={this.handleClick} />
}
}
\end{lstlisting}
Проблема решена (Прим.пер. помимо этого сейчас можно определить стрелочную функция как параметр класса, тогда она не будет каждый раз пересоздаваться, но и контекст будет иметь правильный):
\begin{lstlisting}
class Button extends React.Component {
handleClick = () => {
console.log(this)
}
render() {
return <button onClick={this.handleClick} />
}
}
\end{lstlisting}
\subsection{Stateless functional components}
Есть еще один способ создать компонент, который несколько отличается от первых двух.
Этот способ появился в \textbf{React 0.14} и принес возможности сделать код еще чище.
Чтобы определить компонент, нам достаточно определить функцию, которая будет возвращать React элемент:
\begin{lstlisting}
() => <button />
\end{lstlisting}
Благодаря стрелочной функции этот код минималистичен и понятен. Само собой внутри этой функции можно использовать JSX, иначе бы это, наверно, не имело смысла.
\subsubsection{Props and context}
Компонент, который не может получить параметры от родительского элемента, в большинстве случаев будет бесполезен. В новом способе определения компонент параметры передаются в первом параметре самой функции:
\begin{lstlisting}
props => <button>{props.text}</button>
\end{lstlisting}
Помимо этого мы можем использовать возможности деструктуризации ES2015:
\begin{lstlisting}
({ text }) => <button>{text}</button>
\end{lstlisting}
Также мы можем определить, какие параметры компонента может принимать, по аналогии с классами через $propTypes$ атрибут самой функции:
\begin{lstlisting}
const Button = ({ text }) => <button>{text}</button>
Button.propTypes = {
text: React.PropTypes.string,
}
\end{lstlisting}
Также функциональные компоненты получают вторым аргументом $контекст (context)$:
\begin{lstlisting}
(props, context) => (
<button>{context.currency}{props.value}</button>
)
\end{lstlisting}
\subsubsection{Ключевое слово this}
Главное отличие функциональных компонентов от других заключается в том, что для них $this$ не ссылается на сам компонент.
Следствием этого является невозможность управлять жизненным циклом компонента и использовать такие методы как $setState$.
\subsubsection{State}
Как можно понять из названия (stateless), такие компоненты не обладают внутренним состоянием.
Все что делает такая компоненты, это принимает параметры и контекст в аргументах функции и возвращает React элемент.
Это напоминает нам о функциональном программировании, в разрезе которого мы можем смотреть на функциональные компоненты как на функции React элементов от параметров и контекста.
\subsubsection{Lifecycle}
Функциональные компоненты не предоставляют никаких возможностей по отслеживанию методов жизненного цикла, таких как $componentDidMount$. Все, что не касается непосредственно генерации JSX должно обрабатываться в родительских элементах.
\subsubsection{Refs and event handlers}
Несмотря на то, что мы не можем получить ссылку на сам элемент, мы все же можем получить ссылки на элементы, которые создаются внутри функциональных компонентов. Сделать это можно следующим образом:
\begin{lstlisting}
() => {
let input
const onClick = () => input.focus()
return (
<div>
<input ref={el => (input = el)} />
<button onClick={onClick}>Focus</button>
</div>
)
}
\end{lstlisting}
\subsubsection{Отсутствие ссылки на компонент}
Еще одно отличие функциональных компонентов заключается в том, что если мы создадим экземпляр такого компонента с помощью $ReactTestUtils$, мы не получим никакой ссылки на созданный элемент (подробнее о тестировании и отладке мы поговорим в Главе 10).
Например:
\begin{lstlisting}
const Button = React.createClass({
render() {
return <button />
},
})
const component = ReactTestUtils.renderIntoDocument(<Button />)
\end{lstlisting}
В этом случае переменная component содержит ссылку на созданный элемент.
\begin{lstlisting}
const Button = () => <button />
const component = ReactTestUtils.renderIntoDocument(<Button />)
\end{lstlisting}
А в этом случае переменная component будет иметь значение $null$. Для того, чтобы обойти это ограничение, можно обернуть компонент в другой элемент, например $div$:
\begin{lstlisting}
const component = ReactTestUtils.renderIntoDocument(<div><Button/></div>)
\end{lstlisting}
\subsubsection{Производительность}
Основное, что стоит держать в голове относительно производительности функциональных компонентов то, что они легковеснее классов и лучше поддаются оптимизации внутри самой библиотеки, о чем говорят разработчики Facebook.
Помимо этого есть нюанс, что в жизненном цикле компонента отсутствует метод $shouldComponentUpdate$, что не дает возможность сказать Reacrt'у, что компонент не нужно повторно вызывать метод render. Это на самая большая проблема, но стоит держать ее в голове.
\section{The state}
Мы разобрались с тем, как создавать компоненты в React.
Теперь мы можем пойти дальше и посмотреть детально на управление состоянием компонент.
Также мы должны понять, когда стоит использовать функциональные компоненты вместо классов, и как это влияет на архитектуру наших компонент.
\subsection{Сторонние библиотеки}
Прежде всего мы должны понять, почему мы вообще должны рассматривать управление состоянием внутри наших компонент.
На данный момент большинство учебных материалов и шаблонов приложений на React уже содержат сторонние библиотеки для управления состоянием, такие как \textbf{Redux} и \textbf{MobX}.
К сожалению, это может натолкнуть людей на мысль, что невозможно написать приложение с наличием хоть сколько-то сложного состояние только средствами React, что конечно же не так.
Это приводит к тому, что многие начинающие разработчики изучают React и Redux вместе и плохо представляют как управлять состоянием приложения средствами React.
В этой части мы рассмотрим детально, как управлять состоянием в компонентах React, и в каких случаях мы можем обойтись без использования сторонних библиотек.
\subsection{Как это работает}
Несмотря на различия в разных способах создания компонент, все компоненты, созданные функцией $createClass$ или наследование $React.Component$ могут обладать внутренним состоянием, которое может быть изменено с помощью функции $setState$.
После каждого изменения состояние компоненты React вызывает метод $render$, чтобы перестроить элементы в соответствии с новым состояние.
Поэтому часто говорят, что React компоненты похожи на конечный автомат (state machine).
После вызова метода $setState$ с новым состоянием (или его частью), объект переданный функции сливается с текущим состоянием компонента. Например, если у нас есть следующее состояние:
\begin{lstlisting}
this.state = {
text: 'Click me!',
}
\end{lstlisting}
И мы вызываем $setState$ со следующим объектом:
\begin{lstlisting}
this.setState({
cliked: true,
})
\end{lstlisting}
На выходе мы получим новое состояние компонента:
\begin{lstlisting}
{
cliked: true,
text: 'Click me!',
}
\end{lstlisting}
После каждого изменения состояние React сам вызывает метод $render$, поэтому от нас не требуется никаких дополнительных движений для обновления элемента.
Однако, если нам нужно совершить какие-либо действия сразу после обновления состояния, то есть возможность передать функцию обратного вызова вторым аргументом функции $setState$:
\begin{lstlisting}
this.setState(
{
clicked: true,
},
() => {
console.log('the state is now', this.state)
}
)
\end{lstlisting}
\subsection{Асинхронность}
Функцию $setState$ стоит всегда рассматривать как асинхронную.
Как сказано в официальной документации:
\begin{quotation}
There is no guarantee of synchronous operation of calls to setState... (Нет никаких гарантий в синхронной обработке вызова setState)
\end{quotation}
На деле это выливается в то, что если мы, например, распечатаем состояние компонента сразу после вызова $setState$, то увидим состояние, которое было перед вызовом $setState$:
\begin{lstlisting}
handleClick() {
this.setState({
clicked: true,
})
console.log('the state is now', this.state)
}
render() {
return <button onClick={this.handleClick}>Click me!</button>
}
\end{lstlisting}
Если у компонента не было состояния до вызова $setState$, то в консоль будет напечатано: the state is now $null$.
Причина этого -- оптимизации, которые проводит React при перерисовке компонент. Асинхронность вызова $setState$ позволяет ему откладывать вызов при нехватке ресурсов и объединять при возможности множество вызовов в один.
Но если мы совсем слегка поменяем наш код:
\begin{lstlisting}
handleClick() {
setTimeout(() => {
this.setState({
clicked: true,
})
console.log('the state is now', this.state)
})
}
\end{lstlisting}
Результат будет уже совершенно другим:
\begin{quotation}
\textbf{the state is now Object $\{clicked: true\}$}
\end{quotation}
Это то, что мы хотели бы ожидать в первом случае.
Но в данном случае React не видит никаких возможностей для оптимизации, поэтому состояние обновляется мгновенно.
В данном случае $setTimeout$ использовался для примера различного поведения обновления состояния. Писать обработчики событий в таком стиле и надеяться на синхронность $setState$ крайне не рекомендуется.
\subsection{Reacrt lumberjack}
Если мы можем рассматривать компонент как конечный автомат, то мы можем пойти дальше и реализовать не только изменение состояния компонент, но также и откат этого состояния к предыдущим значениям, что может быть очень полезно при отладке.
Такой функционал уже реализован в библиотеке \textit{react- lumberjack} Райаном Флоренсом (Ryan Florence), создателем очень известной библиотеки \textit{react-router}.
Использовать библиотеку очень просто, главное не стоит забывать выключать ее на проде.
Ее можно установить, как и множество других библиотек, через $npm$ или добавить прямую ссылку на $unpkg.com$:
\begin{lstlisting}
<script src="https://unpkg.com/[email protected]"></script>
\end{lstlisting}
После установки библиотеки наше приложение продолжает работать как раньше.
Но если мы хотим отменить изменения в состоянии приложения, то достаточно набрать в консоли:
\begin{lstlisting}
Lumberjack.back()
\end{lstlisting}
Если после этого мы хотим снова вернуться к состоянию до отката, то нужно набрать в консоли:
\begin{lstlisting}
Lumberjack.forward()
\end{lstlisting}
Таким нехитрым образом мы можем гулять по состояниям назад и вперед.
Но стоит помнить, что это достаточно экспериментальная библиотека и она может или исчезнуть вовсе после какого-нибудь изменения React API или наоборот стать частью React Developer Tools.
\subsection{Использование state}
Теперь, когда мы узнали, как хранится и изменяется состояние компонент, мы можем подумать о там, какие данные стоит или не стоит хранить в компонентах.
Нам нужно выработать некоторые правила, которые бы помогли нам в большинстве случаев понимать, должен ли компонент быть с внутренним состоянием или нет.
Прежде всего стоит запомнить, что внутри компонент стоит хранить минимально необходимое количество данных.
Например, если у нас есть текстовое поле, значение которого должно меняться когда пользователь нажимает на кнопку, то нам не стоит хранить весь текст поля во внутреннем состоянии компонента, достаточно хранить лишь boolean значение факта нажатия кнопки.
Таким образом, если мы можем посчитать какое-то значение на основе данных состояния, то это значение хранить внутри компоненты не стоит.
Во-вторых, во внутреннем состоянии компонента стоит хранить только те данные, изменение которых должно приводить к перерисовке компонента.
Пример таких данных -- флаг $isClicked$, который меняется при нажатии пользователя, или любое поле ввода данных.
В общем случае, во внутреннем состоянии следует хранить данные, которых будет необходимо и достаточно для восстановления работоспособности сайта. Сюда же могу войти текущая страница, на которой находится пользователь, выбранная вкладка, введенные фильтры.
Также принять решение относительно каких-то данных поможет знание того, какому количеству внешних или дочерних элементов требуются эти данные.
Если данные требуются большому количеству компонент, то скорее всего стоит задуматься о специальном хранилище данных на уровне всего приложения, например Redux.
Сейчас посмотрим на случаи, когда мы должны избежать использования внутреннего состояния компонент для хранения данных.
\subsubsection{Derivables}
Если какое-либо значение, которое требуется для отображения компонента, может быть посчитано на основе $props$, то это не нужно сохранять в $state$.
Например, если мы получаем валюту и цену из $props$ и всегда показываем их вместе, то могли бы предположить, что логично поместить их в $state$:
\begin{lstlisting}
class Price extends React.Component {
constructor(props) {
super(props)
this.state = {
price: `${props.currency}${props.value}`
}
}
render() {
return <div>{this.state.price}</div>
}
}
\end{lstlisting}
Но это будет работать только если мы будем создавать экземпляры этого компонента со статичными параметрами, например:
\begin{lstlisting}
<Price currency="\textdollar" value="100" />
\end{lstlisting}
Проблема в том, что если валюта или цена изменится в течение жизни компонента, то это никак не отразится на отображении, так как конструктор вызывается только один раз в момент создания элемента.
Вместо этого нам следует использовать $props$ для вычисления значение.
Помимо этого, как говорилось ранее, эти расчеты можно вынести в отдельную вспомогательную функцию:
\begin{lstlisting}
getPrice() {
return `${this.props.currency}${this.props.value}`
}
\end{lstlisting}
\subsubsection{The render method}
Нужно помнить, что любое изменение состояния компонента влечет перерисовку элемента, поэтому в $state$ должны быть только те данные, которые используются для отрисовки элемента.
Например, если нам нужно хранить подписку на API сервера или ссылку на работающий таймер, но ни первое ни второе не влияет на отображение компонента, то стоит хранить их в отдельном модуле.
Следующий пример плохо кода показывает, как не стоит хранить данные в $state$. Данные сохраняются в $state$, но при этом не используются в $render$ функции, что влечет к излишним обновлениям компонента:
\begin{lstlisting}
componentDidMount() {
this.setState({
request: API.get(...)
})
}
componentWillUnmount() {
this.state.request.abort()
}
\end{lstlisting}
Такого использования $state$ следует избегать и лучше хранить данные, которые не затрагивают отображение, в отдельном модуле (прим.пер. SOLID и дядя Боб вам в помощь).
Еще одним распространенным решение является хранение таких данных внутри экземпляра компонента, но не в $state$, а в обычном поле. Это хорошо работает, так как компонента все еще остается обычным JavaScript классом:
\begin{lstlisting}
componentDidMount() {
this.request = API.get(...)
}
componentWillUnmount() {
this.request.abort()
}
\end{lstlisting}
В этом случае данные сохранены в экземпляре класса и никак не затрагивают метод $render$ и перерисовку элемента.
Следующий пример кода от Дена Абармова кратко поясняет, что не нужно хранить в $state$:
\begin{figure}[h]
\includegraphics[width=.8\textwidth]{images/dan-cheat-sheet}
\end{figure}
\section{Prop types}
Мы ставим перед собой цель, создавать переиспользуемые компоненты, поэтому нам нужно описывать интерфейс этих компонент удобным для пользователей этих компонент образом.
React из коробки позволяет нам описывать, какие параметры ожидает компонент и простые правила валидации к ним. Правила позволяют указывать, какого типа данных должны быть параметры, а также являются ли параметры обязательными. Помимо этого React позволяет определять пользовательские функции проверки параметров.
Посмотрим на простой пример:
\begin{lstlisting}
const Button = ({ text }) => <button>{text}</button>
Button.propTypes = {
text: React.PropTypes.string,
}
\end{lstlisting}
В этом примере мы создаем простой функциональный компонент, который должен получить один параметр $text$ типа $string$.
Теперь каждый, кто попытается воспользоваться данным компонентом, сможет легко понять, что ему нужно передавать даже без детального чтения кода.
Но что если компонент вообще не сможет работать, если ему не передать определенный параметр? В этом случае этот параметр можно указать как обязательный:
\begin{lstlisting}
Button.propTypes = {
text: React.PropTypes.string.isRequired,
}
\end{lstlisting}
В этом случае кто-либо, кто создаст такой компонент, не передав обязательный параметр, получат следующую ошибку:
\begin{quotation}
\textbf{Failed prop type: Required prop `text` was not specified in `Button`.}
\end{quotation}
Важно понимать, что предупреждение об ошибке мы получим только в режиме разработки. В релизной сборке валидация с $propTypes$ выключается в целях улучшения производительности.
React предоставляет возможность проверки множества типов параметров: от чисел до массивов и компонент.
Если мы хотим, чтобы компонент работал с данными разных типов, передаваемых через один параметр, то можем использовать функцию \textbf{oneOf}, которая позволяет указать список разных типов для одного параметра.
Также стоит стараться передавать через параметры только примитивные типы, так как они лучше поддаются отладке и тестированию.
Передача примитивов также позволяет быстрее находить разбухающие интерфейсы у компонент. Если компонент начинает требовать все больше и больше параметров, то возможно он содержит больше логики чем должен и нарушает принцип единственности ответственности.
Если мы замечаем, что компонент получает множество параметров, которые слабо связаны логикой приложения, то можно попробовать разделить этот компонент на два.
Однако нам все равно достаточно часто приходится передавать компонентам объекты. В этом случае для валидации следует использовать функцию $shape$.
Функция $shape$ позволяет определить параметр типа объект, а также определить для него типы всех полей (которые в свою очередь тоже могут быть объектами).
Например, если мы создадим компонент $Profile$, который ожидает объект с именем и фамилией пользователя, то мы можем создать следующие $propTypes$:
\begin{lstlisting}
const Profile = ({ user }) =>(
<div>{user.name} {user.surname}</div>
)
Profile.propTypes = {
user: React.PropTypes.shape({
name: React.PropTypes.string.isRequired,
surname: React.PropTypes.string,
}).isRequired,
}
\end{lstlisting}
Если же ни одного из стандартных методов валидации React нам не подходят, то мы можем определить собственную функцию проверки:
\begin{lstlisting}
user: React.PropTypes.shape({
age: (props, propName) => {
if (!(props[propName] > 0 && props[propName] < 100)) {
return new Error(`${propName} must be between 1 and 99`)
}
return null
},
})
\end{lstlisting}
Например, в примере выше мы проверяем, что возраст находится в определенном числовом промежутке. Если же возраст выйдет за этот промежуток, то мы увидим соответствующую ошибку в консоли.
\subsection{React Docgen}
Хорошо описанные $propTypes$ уже значительное облегчение жизни тем, кто будет пользоваться нашими компонентами. Но мы можем пойти дальше и еще больше упростить их использование.
Когда количество компонентов значительно возрастает, то появляется проблема поиска необходимого компонента среди многих, особенно для новых членов проекта.
Но если мы поддерживали $propTypes$ в хорошем состоянии, то мы можем автоматически создавать из них документацию.
Для этого мы можем использовать библиотеку $react-docgen$, которую можно установить следующей командой:
\begin{lstlisting}
npm install --global react-docgen
\end{lstlisting}
React Docgen проходит по файлу с компонентом и достает, необходимую для него информацию, из $propTypes$ и комментариев.
Например, если у нас есть компонент:
\begin{lstlisting}
const Button = ({ text }) => <button>{text}</button>
Button.propTypes = {
text: React.PropTypes.string,
}
\end{lstlisting}
И мы запустим:
\begin{lstlisting}
react-docgen button.js
\end{lstlisting}
Мы получим следующий результат;
\begin{lstlisting}
{
"description": "",
"methods": [],
"props": {
"text": {
"type": {
"name": "string"
},
"required": false,
"description": ""
}
}
}
\end{lstlisting}
Этот JSON представляет собой описание интерфейса компонента. Как вы видите, в него попали поля и их типы, описанные в $propTypes$.
Также мы можем добавить комментарий к нашему компоненту:
\begin{lstlisting}
/**
* A generic button with text.
*/
const Button = ({ text }) => <button>{text}</button>
Button.propTypes = {
/**
* The text of the button.
*/
text: React.PropTypes.string,
}
\end{lstlisting}
Если мы снова запустим Docgen, то получим:
\begin{lstlisting}
{
"description": "A generic button with text.",
"methods": [],
"props": {
"text": {
"type": {
"name": "string"
},
"required": false,
"description": "The text of the button."
}
}
}
\end{lstlisting}
Теперь, с этим описанием интерфейса в формате JSON мы можем создать документацию и использовать ее внутри команды.
Результат работы Docgen имеет простой формат, поэтому не составляет никакого труда создать страницу с документацией на его основе.
Один из ярких примеров использования React Docgen -- документация библиотеки $Material UI$, где вся документация создана на основе исходного кода библиотеки.
\section{Переиспользуемые компоненты}
Мы уже хорошо разобрались с тем как создавать компоненты, как использовать внутреннее состояние компонент и как сделать их переиспользуемыми с помощью $propTypes$.
Давайте теперь, вооружившись всеми полученными знаниями, попробуем сделать из непереиспользуемых компонент переиспользуемые.
Предположим, что у нас есть компонент, который загружает список постов через API сервера и отображает их на экране.
Это упрощенный пример, но он хорошо подходит, чтобы показать все этапы создания переиспользуемого компонента.
Создадим класс посредством его наследования от React.Component:
\begin{lstlisting}
class PostList extends React.Component
\end{lstlisting}
Затем создадим конструктор и добавим загрузку данных в $componentDidMount$:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
posts: [],
}
}
componentDidMount() {
Posts.fetch().then(posts => {
this.setState({ posts })
})
}
\end{lstlisting}
Во $state$ компонента только одно поле $posts$, в котором мы будем хранить посты. Его значением по умолчанию будет пустой массив.
В $componentDidMount$ вызывается API сервера, для получения списка постов. По окончанию запроса посты сохраняются в $state$ компонента с помощью метода $setState$.
Это распространенный паттерн загрузки данных, другие варианты детальнее мы рассмотрим в Главе 5.
$Posts$ -- это вспомогательный класс, который содержит логику общения с сервером. Сейчас для нас важно только то, что этот класс имеет метод $fetch$. Данный метод возвращает $Promise$, который при успешном выполнении вернет список постов.
Теперь мы можем отобразить список постов:
\begin{lstlisting}
render() {
return (
<ul>
{this.state.posts.map(post => (
<li key={post.id}>
<h1>{post.title}</h1>
{post.excerpt && <p>{post.excerpt}</p>}
</li> ))}
</ul>
)
}
\end{lstlisting}
Внутри метода $render$ мы обходим все посты и для каждого создаем элемент $<li>$.
Мы полагаем, что у поста всегда есть поле $title$ и безусловно показываем его внутри $<h1>$. Поле же $post.excerpt$ мы считаем необязательным и отображаем только при наличии.
Теперь представим другой компонент. Пусть он отображает список пользователей, которые получает из $props$, а не из собственного состояния:
\begin{lstlisting}
const UserList = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>
<h1>{user.username}</h1>
{user.bio && <p>{user.bio}</p>}
</li>
)
)}
</ul>
)
\end{lstlisting}
Данный компонент отображает список пользователей очень похожим на отображение постов способом.
Отличие в том, что теперь вместо $title$ отображается $username$ и опциональное поле теперь $bio$ пользователя вместо $excerpt$ поста.
Дублирующийся код считается плохим звоночком, так что давайте разбираться как React позволяет следовать правилу \textbf{Не повторяйся (Don't Repeat Yourself, DRY)}. Прежде всего мы можем создать отдельный компонент $List$, в который вынесем логику отображения списков, отделив ее от самих данных. Главным требованием является возможность передать ключи полей по которым мы сможем получить данные, чтобы иметь возможность брать нужные данные из разных типов объектов.
Чтобы сделать это, мы определим два параметра: $titleKey$ для передачи ключа, по которому мы получим значение для обязательного поля, и $textKey$ для передачи ключа опционального поля.
Параметры нашего нового компонента будут выглядеть следующим образом:
\begin{lstlisting}
List.propTypes = {
collection: React.PropTypes.array,
textKey: React.PropTypes.string,
titleKey: React.PropTypes.string,
}
\end{lstlisting}
Так как компонент $List$ не будет обладать своим собственным состоянием, мы можем сделать его функциональным:
\begin{lstlisting}
const List = ({ collection, textKey, titleKey }) => (
<ul>
{collection.map(item =>
<Item
key={item.id}
text={item[textKey]}
title={item[titleKey]}
/>
)}
</ul>
)
\end{lstlisting}
Компонент $List$ получает через $props$ коллекцию объектов, проходит по ним и преобразует в элементы $Item$, который мы скоро реализуем. Также в дочерний элемент мы передаем $text$ и $title$, который получаем с помощью полученных ключей из элементов коллекции.
Компонент $Item$ будет максимально простым и чистым:
\begin{lstlisting}
const Item = ({ text, title }) => (
<li>
<h1>{title}</h1>
{text && <p>{text}</p>}
</li>
)
Item.propTypes = {
text: React.PropTypes.string,
title: React.PropTypes.string,
}
\end{lstlisting}
Таким образом мы создали два компонента с достаточно простыми интерфейсами, чтобы с их помощью отображать пользователей, посты или что-либо еще. При этом такие небольшие компоненты очень удобны в поддержке и тестировании.
Отлично, теперь мы можем переписать наши исходные компоненты с использованием новых вспомогательных компонент.
Метод $render$ компонента $PostsList$ будет выглядеть следующим образом:
\begin{lstlisting}
render() {
return (
<List
collection={this.state.posts}
textKey="excerpt"
titleKey="title"
/>
)
}
\end{lstlisting}
А функциональный компонент $UserList$ следующим:
\begin{lstlisting}
const UserList = ({ users }) => (
<List
collection={users}
textKey="bio"
titleKey="username"
/>
)
\end{lstlisting}
Таким образом мы из узкоспециализированных компонент получили базовые компоненты, которые могут быть переиспользованы в будущем.
Мы также можем использовать $react-docgen$ для генерации документации для полученных нами компонент.
Также теперь в случае необходимости расширения данного отображения, которое пока состоит из двух текстовых полей, нам будет достаточно поменять его в одном компоненте $Item$, а не в множестве узкоспециализированных компонент.
Например, если нам понадобится в случае слишком длинной строки урезать ее и показывать многоточие, нам будет достаточно добавить эту логику внутрь одного компонента.
\section{Living style guides}
Использование переиспользуемых компонент с простыми интерфейсами -- хороший способ сократить количество дублирующегося кода в проекте, но это не единственная причина, чтобы сосредоточиться на переиспользуемости.
Если вы создаете простые и понятные компоненты с чистыми интерфейсами, которые хорошо отделены абстракциями от конкретных данных, то эти компоненты можно объединить в библиотеку компонент и использовать за пределами команды. Такая библиотека будет представлять собой набор готовых к использованию блоков, которыми можно будет поделиться с другими командами, дизайнерами или выложить ее в open source.
Очень часто новым членам команды может быть сложно понять, какие компоненты уже есть, а какие нужно реализовать. Решением этой проблемы может быть создание Style guide'а, которое бы позволило распространять не только сами компоненты, но и примеры их использования.
По сути style guide -- это собранное визуальное представление всех единичных компонентов, которые уже реализованы в проекте. Это очень удобный способ сохранять единый стиль всех компонент среди множества разработчиков разного уровня.
К сожалению, создание style guide'а не всегда является простой задачей, так как из-за меняющихся требований может появиться множество дублирующихся компонент с небольшими отличиями, решающие какие-то локальные проблемы. Тем не менее React позволяет без значительных усилий создавать такой род документации, что может окупить немало времени в будущем.
Но не только React может вам помочь создать библиотеку визуальных компонентов из кода самих компонент. Есть инструменты, которые помогают решить эту проблему, например, $react-storybook$.
React Storybook изолирует компоненты, предоставляя вам возможность создавать компоненты без запуска всего приложения, что помогает в тестировании и разработке.
Как видно из названия библиотеки React Storybook позволяет создавать истории для отображения разных состояний компонента. Например, если вы пишите TO-DO приложение, то вы можете создать две истории для отображения выбранного и невыбранного состояний элемента.
Давайте попробуем применить эту библиотеку к примеру с компонентом $List$. Прежде всего нам нужно установить Storybook:
%npm install --save @kadira/react-storybook-addon
Теперь мы можем начать создавать истории.
В нашем примере компонент $Item$ требует обязательный параметр $title$ и опциональный $text$, в этом случае мы можем создать как минимум две истории.
Обычно истории хранят в директории $stories$ внутри проекта, но в целом никто не запрещает использовать любую удобную для вас директорию.