-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter6.tex
980 lines (692 loc) · 59.9 KB
/
chapter6.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
\chapter{Пишем Код для Браузера}
Есть множество операций, которые мы можем выполнять только в связке с браузером. Например, мы можем попросить пользователя ввести данные через форму, поэтому стоит рассмотреть, какие есть техники для обработки таких
Мы можем использовать \textbf{Неконтролируемые компоненты}, которые сами управляют своим внутренним состоянием, или \textbf{Контролируемые}, в которых мы берем управление на себя.
В этой главе мы также посмотрим, как React работает с событиями (events) и реализует некоторые продвинутые техники для предоставления консистентного интерфейса между разными браузерами.
После событий мы разберем атрибут $ref$, который позволяет получить ссылку на элементы, лежащие ниже в дереве элементов. Это мощный инструмент, который следует использовать с осторожностью. Этот прием ломает некоторые договоренности, которые упрощают разработку на React.
После этого мы разберемся, как работать с анимацией, используя расширения React и сторонние библиотеки, такие как \textbf{react-motion}. И в конце посмотрим, насколько легко в React использовать SVG, и как мы можем динамические создавать иконки для нашего приложения.
В этой главе мы разберем следующие вопросы:
\begin{itemize}
\item Использование различных подходов для создания форм
\item Отслеживание событий DOM и создание собственных обработчиков
\item Способ выполнения императивных операций над DOM элементами через параметр ref
\item Создание простых анимаций, которые будут работать в различных браузерах
\item Способ создания SVG в React
\end{itemize}
\section{Формы}
После того, как мы начали делать приложение, нам очень быстро может понадобиться взаимодействовать с пользователем. Если мы хотим попросить пользователя какие-либо данные, то формы одно из самых распространенных решений этой проблемы.
Из-за особенностей работы React и его декларативной парадигмы работа с формами может показаться на первый взгляд совсем нетривиальной, но когда мы разберемся с этим вопросом, все станет проще.
\subsection{Неконтролируемые компоненты}
Давайте начнем с простого примера кода, состоящего из поля ввода и кнопки отправки:
\begin{lstlisting}
const Uncontrolled = () => (
<form>
<input type="text" />
<button>Submit</button>
</form>
)
\end{lstlisting}
Если мы запустим этот код, то получим форму с полем ввода, в которое мы можем что-то ввести, и кнопкой. Это пример Неконтролируемого Компонента, так как мы не передаем ему данные и оставляем на него управление его состоянием.
Чаще всего мы хотим что-то сделать в момент нажатия на кнопку. Например, мы можем послать данные по сети.
Мы можем сделать это, просто добавив обработчик $onChange$ (об обработчиках событий мы поговорим подробнее дальше в этой главе).
Давайте посмотрим, как это сделать.
Для начала создадим класс для компонента, так как нам нужны дополнительные функции:
\begin{lstlisting}
class Uncontrolled extends React.Component
\end{lstlisting}
В конструкторе класса привяжем обработчик события к экземпляру компонента:
\begin{lstlisting}
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
\end{lstlisting}
Затем определим собственно сам обработчик:
\begin{lstlisting}
handleChange({ target }) {
console.log(target.value)
}
\end{lstlisting}
Обработчик события получает объект события (event object). В поле $target$ этого объекта находится элемент, который это событие создал. В данном случае нас интересует значение (value) этого элемента. Мы начнем с малого шага и пока что просто будем печатать данные в консоль, но в дальнейшем мы будем сохранять их в state.
И компоненту не хватает только функции $render$:
\begin{lstlisting}
render() {
return (
<form>
<input type="text" onChange={this.handleChange} />
<button>Submit</button>
</form>
)
}
\end{lstlisting}
Если мы запустим этот код в браузере и начнем набирать в поле ввода слова \textbf{React}, то увидим в консоли что-то вида:
\begin{lstlisting}
R
Re
Rea
Reac
React
\end{lstlisting}
Обработчик $handleChange$ вызывается после каждого изменения в поле ввода. Наш следующий шаг -- сохранить это значение внутри компонента, чтобы использовать при нажатии кнопки пользователем.
Мы изменим реализацию обработчика событий, чтобы вместо печати в консоль он сохранял значение в $state$ компонента:
\begin{lstlisting}
handleChange({ target }) {
this.setState({
value: target.value,
})
}
\end{lstlisting}
Отслеживание нажатия на кнопку для отправки формы очень похоже на отслеживание изменения поля ввода. В обоих случаях браузером создается событие, которое можно перехватить.
Поэтому нам нужно добавить второй обработчик события:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
value: '',
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
\end{lstlisting}
Также добавим значение по умолчанию для сохраняемого значения на случай, если кнопка будет нажата до начала ввода значения.
Добавим обработчик $handleSubmit$, который сейчас будет печатать данные в консоль. В реальном проекте здесь может быть отправка данных в сеть:
\begin{lstlisting}
handleSubmit(e) {
e.preventDefault()
console.log(this.state.value)
}
\end{lstlisting}
Этот обработчик крайне прост: он печатает текущее значение в консоль. Также он вызывает метод $e.preventDefault$, чтобы предотвратить стандартное поведение браузера на отправку формы.
Отлично, этот код прекрасно работает, но что если у нас больше полей? Предположим, что у нас есть десяток полей для ввода.
Начнем с того, что попытаемся создать обработчики вручную, а потом посмотрим, как мы можем это дело оптимизировать.
Давайте создадим компонент, где будут два поля ввода для имени и фамилии. Мы можем использовать класс $Uncontrolled$, поправив в нем нужные блоки. Начнем с конструктора:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
firstName: '',
lastName: '',
}
this.handleChangeFirstName = this.handleChangeFirstName.bind(this)
this.handleChangeLastName = this.handleChangeLastName.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
\end{lstlisting}
Мы создаем два поля для данных и обработчик для каждого из них. Уже можно заметить не лучшую расширяемость этого решения, но перед тем, как мы создадим что-то более гибкое, посмотрим на реализацию этих обработчиков:
\begin{lstlisting}
handleChangeFirstName({ target }) {
this.setState({
firstName: target.value,
})
}
handleChangeLastName({ target }) {
this.setState({
lastName: target.value,
})
}
\end{lstlisting}
Также немного поправим обработчик отправки формы, чтобы он выводил в консоль новые данные:
\begin{lstlisting}
handleSubmit(e) {
e.preventDefault()
console.log(`${this.state.firstName} ${this.state.lastName}`)
}
\end{lstlisting}
И в конце мы описываем элементы формы в методе $render$:
\begin{lstlisting}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" onChange={this.handleChangeFirstName} />
<input type="text" onChange={this.handleChangeLastName} />
<button>Submit</button>
</form>
)
}
\end{lstlisting}
Теперь у нас есть отправная точка. Мы можем запустить приложение, ввести в первое поле ввода строку \textbf{Dan}, во вторую \textbf{Abramov}, после чего увидеть имя и фамилию в консоли при нажатии на кнопку отправки формы.
Компонент работает, но текущий подход требует значительных доработок. Если мы продолжим добавлять поля сейчас, то нам понадобится написать множество шаблонного кода, чего мы конечно хотим избежать.
Посмотрим, как мы можем это исправить.
Простоя идея, использовать один обработчик событий, чтобы мы могли использовать его для множества полей ввода.
Давайте вернемся к конструктору и заменим обработчики ввода одним:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
firstName: '',
lastName: '',
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
\end{lstlisting}
Мы все еще хотим задавать дефолтные значения для полей ввода, далее мы рассмотрим как передавать эти значения форме.
Теперь мы можем переделать функцию $onChange$, чтобы она работала с разными полями ввода:
\begin{lstlisting}
handleChange({ target }) {
this.setState({
[target.name]: target.value,
})
}
\end{lstlisting}
Как уже говорилось, в параметре $target$ находится представление поля ввода, поэтому мы можем использовать из него не только значение, но и имя.
Теперь нам нужно передать полям ввода имена, чтобы данные сохранялись корректно. Мо можем сделать это в методе $render$:
\begin{lstlisting}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="firstName"
onChange={this.handleChange}
/>
<input
type="text"
name="lastName"
onChange={this.handleChange}
/>
<button>Submit</button>
</form>
)
}
\end{lstlisting}
Теперь мы можем добавить множество полей ввода, используя один обработчик событий для всех полей.
\subsection{Контролируемые компоненты}
Следующий шаг -- заполнить поля ввода значениями, которые мы можем получить от сервера или из параметров компонента.
Чтобы полностью понять идею, мы снова начнем с простого компонента и будем постепенно его улучшать.
Первый пример показывает предварительно заполненное значение внутри поля ввода:
\begin{lstlisting}
const Controlled = () => (
<form>
<input type="text" value="Hello React" />
<button>Submit</button>
</form>
)
\end{lstlisting}
Если мы запустим этот пример в браузере, то увидим, что в поле ввода находится переданное нами значение. Но при попытке ввода значение меняться не будет; что бы мы ни делали, поле ввода будет сохранять константное значение.
В общем и целом все логично. Мы передали в поле ввода константное значение -- мы получили в нем константное значение, но скорее всего мы бы хотели другого поведения.
Если в этот момент мы откроем консоль браузера, то React подскажет нам, что мы делаем что-то не так:
\begin{lstlisting}
You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field
\end{lstlisting}
(Полю формы передан параметр 'value', но не передан параметр 'onChange'. Это приводит к созданию поля без возможности ввода.)
И он чертовски прав.
Сейчас, для того чтобы добавить значение по умолчанию и иметь возможность редактирования поля ввода, мы можем добавить параметр $defaultValue$:
\begin{lstlisting}
const Controlled = () => (
<form>
<input type="text" defaultValue="Hello React" />
<button>Submit</button>
</form>
)
\end{lstlisting}
Таким образом поле ввода получит значение по умолчанию и его можно будет редактировать. Но сейчас мы можем задать значение компонента только в момент создания, а потом он будет управлять собой сам. В целом, это уже решает проблему с начальным значением, но все еще не дает полностью контролировать состояние поля ввода.
Для того, чтобы перенести управление состоянием поля ввода, создадим компонент собственным состоянием, для чего заменим функциональный компонент на класс:
\begin{lstlisting}
class Controlled extends React.Component
\end{lstlisting}
Как всегда, мы начнем с создания конструктора, где определим начальное состояние компонента (в данном случае это будут начальные значение полей ввода) и обработчики событий.
В данном случае для полей ввода мы будем использовать один обработчик событий, который был написан для примера Неконтролируемого компонента ранее:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
firstName: 'Dan',
lastName: 'Abramov',
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
\end{lstlisting}
Обработчики событий такие же, как в предыдущем примере:
\begin{lstlisting}
handleChange({ target }) {
this.setState({
[target.name]: target.value,
})
}
handleSubmit(e) {
e.preventDefault()
console.log(`${this.state.firstName} ${this.state.lastName}`)
}
\end{lstlisting}
Важное изменение происходим в методе $render$, где мы будем использовать параметр $value$ полей ввода для передачи актуального значение:
\begin{lstlisting}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="firstName"
value={this.state.firstName}
onChange={this.handleChange}
/>
<input
type="text"
name="lastName"
value={this.state.lastName}
onChange={this.handleChange}
/>
<button>Submit</button>
</form>
)
}
\end{lstlisting}
Когда компонент отрисовывается в первый раз, React использует начальное значение состояние компонента как значение полей ввода.
Когда пользователь вводит что-либо, вызывается обработчик $handleChange$, который обновляет состояние компонента. После изменения состояние компонент перерисовывается и отображает поля ввода с уже новыми значениями.
Теперь у нас есть способ полного контроля над данными полей ввода, такой паттерн называется \textbf{Контролируемым Компонентом (Controlled Component)}.
\subsection{JSON схема}
Теперь мы знаем, как работают формы в React, и можно задуматься о том, чтобы автоматизировать создание форм, уменьшить количество шаблонного кода и сделать код чище.
Одно из возможных вариантов -- использование библиотеки react-jsonschema-form, которая поддерживается mozilla-services. Для установки библиотеки нужно выполнить команду:
\begin{lstlisting}
npm install --save react-jsonschema-form
\end{lstlisting}
После установки библиотеки мы можем добавить импорт в файл с нашим компонентом:
\begin{lstlisting}
import Form from 'react-jsonschema-form'
\end{lstlisting}
Также мы должны определить схему формы следующим образом:
\begin{lstlisting}
const schema = {
type: 'object',
properties: {
firstName: { type: 'string', default: 'Dan' },
lastName: { type: 'string', default: 'Abramov' },
},
}
\end{lstlisting}
В этой книге мы не будем углубляться в формат JSON Schema, но самое главное здесь то, что мы можем создать конфигурацию формы вместо непосредственного создания HTML элементов.
Как вы можете увидеть в примере, мы установили тип схемы $object$. У этого типа есть два параметра, $firstName$ и $lastName$, каждый из которых типа $string$ и со своим значением по умолчанию.
Если после этого мы передадим объект схемы компоненту $Form$, который мы импортировали из библиотеки, то форма будет создана автоматически.
Давайте, как всегда, начнем с простого использования данный библиотеки и будем постепенно улучшать код:
\begin{lstlisting}
const JSONSchemaForm = () => (
<Form schema={schema} />
)
\end{lstlisting}
Теперь, если мы запустим код, то увидим форму с полями, которые мы определили в схеме, и кнопкой отправки.
Теперь мы хотим как-то отслеживать момент отправки формы для выполнения каких-либо действий с данными полей ввода.
Прежде всего нам нужно создать обработчик событий, поэтому переделаем функциональный компонент в класс:
\begin{lstlisting}
class JSONSchemaForm extends React.Component
\end{lstlisting}
В конструкторе мы привяжем обработчик к экземпляру этого класса:
\begin{lstlisting}
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
\end{lstlisting}
В этом примере мы просто печатаем данные в консоль, но в реальном коде вы скорее всего захотите выполнить с ними какие-то действия, например отправить на сервер.
Обработчик событий $handleSubmit$ получает объект с полем $formData$, в котором находятся все имена и значения полей ввода:
\begin{lstlisting}
handleSubmit({ formData }) {
console.log(formData)
}
\end{lstlisting}
И в итоге метод $render$ будет выглядеть следующим образом:
\begin{lstlisting}
render() {
return (
<Form schema={schema} onSubmit={this.handleSubmit} />
)
}
\end{lstlisting}
Здесь объект $schema$ -- это объект схемы, который мы создали ранее. Этот объект может быть объявлен статически, как в данном примере, собран из параметров компонента или получен от сервера.
Остается только передать обработчик $onSubmit$ компонента $Form$, и работающая форма готова.
Помимо этого можно передать обработчики событий $onChange$ для отслеживания всех изменений данных в форму и $onError$ для перехвата попыток отправить неправильные данные.
\section{События}
Работа \textbf{событий (events)} несколько отличается во всех браузерах. React пытается дать разработчику общий интерфейс для работы с событиями в любых браузерах, скрывая различия в работе событий за единой абстракцией. Это великолепная возможность React, которая позволяет перестать писать отдельные обработчики событий для каждого браузера.
Для реализации такого интерфейса React вводит новый концепт \textbf{Синтетический Событий (Synthetic Event)}. Синтетическое событие -- это объект, созданный на основе оригинального события, но имеющий всегда одну и ту же структуру независимо от структуры события, из которого он был создан.
Для прикрепления к элементу обработчика события мы можем использовать простое соглашение, которое напоминает способ привязки обработчиков событий к DOM элементам. Мы просто используем приставку $on$ и название события в ГорбатомРегистре (например, $onKeyDown$).
Распространенный способ объявления обработчиков событий заключается в использовании названия события в ГорбатомРегистре и добавлении префикса $handle$ (например, $handleKeyDown$).
Мы увидели этот паттерн в полной мере в предыдущем примере, когда создавали обработчик для события $onChange$.
Давайте разберемся, как можно организовать обработку множества событий внутри компонента.
Мы собираемся создать простою кнопку и начнем с создания соответствующего класса:
\begin{lstlisting}
class Button extends React.Component
\end{lstlisting}
В конструкторе сделаем привязку обработчика событий к экземпляру класса:
\begin{lstlisting}
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
\end{lstlisting}
И определим сам обработчик событий:
\begin{lstlisting}
handleClick(syntheticEvent) {
console.log(syntheticEvent instanceof MouseEvent)
console.log(syntheticEvent.nativeEvent instanceof MouseEvent)
}
\end{lstlisting}
Как можно увидеть, мы делаем простую вещь: проверяем тип объекта события, который мы получаем от React, а также, переданного вместе с ним, объекта нативного события. В первом случае мы ожидаем $false$, а во втором $true$.
Скорее всего объект нативного события вам никогда не понадобится, но стоит помнить, что в случае необходимости вы можете им воспользоваться. И в конце нам нужно определить метод $render$, где мы создадим кнопку с атрибутом $onClick$, в который передадим только что созданный обработчик событий:
\begin{lstlisting}
render() {
return (
<button onClick={this.handleClick}>Click me!</button>
)
}
\end{lstlisting}
Теперь представим, что мы хотим добавить второй обработчик событий, который срабатывает при двойных кликах. Для того, чтобы сделать это, можно просто добавить отдельный обработчик и передать кнопке через атрибут $onDoubleClick$:
\begin{lstlisting}
<button
onClick={this.handleClick}
onDoubleClick={this.handleDoubleClick}>
Click me!
</button>
\end{lstlisting}
Помните, нужно всегда стараться писать меньше шаблонного и дублирующегося кода. Поэтому достаточно распространенный подход, писать \textbf{один обработчик событий} на компонент, который может вызывать необходимые методы в зависимости от типа события.
Этот подход хорошо описал Майкл Чен (Michael Chan) в своей коллекции паттернов:
\begin{lstlisting}
http://reactpatterns.com/#event-switch
\end{lstlisting}
Прежде всего поправим конструктор, потому что теперь мы будем использовать один общий обработчик событий:
\begin{lstlisting}
constructor(props) {
super(props)
this.handleEvent = this.handleEvent.bind(this)
}
\end{lstlisting}
Далее реализуем сам обработчик событий:
\begin{lstlisting}
handleEvent(event) {
switch (event.type) {
case 'click':
console.log('clicked')
break
case 'dblclick':
console.log('double clicked')
break
default:
console.log('unhandled', event.type)
}
}
\end{lstlisting}
Общий обработчик событий получает объект события и в зависимости от его типа совершает нужное действие, чаще всего вызывает соответствующий метод.
И осталось только передать новый обработчик событий кнопке в атрибутах $onClick$ и $onDoubleClick$:
\begin{lstlisting}
render() {
return (
<button
onClick={this.handleEvent}
onDoubleClick={this.handleEvent}
>
Click me!
</button>
)
}
\end{lstlisting}
С этого момента, если нам нужно будет начать обрабатывать еще одно событие, то вместо добавления еще одного метода будет достаточно добавить еще один case в switch.
Есть еще два важных нюанса, касающиеся событий в React: Синтетические События переиспользуются и есть \textbf{глобальный обработчик событий}.
Из первого вытекает то, что мы не можем хранить синтетические события для дальнейшего переиспользования, так как React перезапишет все его значения сразу после окончания обработки этого события. Это очень выгодно для производительности приложения, так как помогает избежать создания множества объектов, но добавляет нам головной боли если мы хотим сами сохранить этот объект для дальнейшего использования. Для решения этой проблемы React предоставляет метод синтетических событий $persist$, который позволяет сделать последние персистентными, чтобы иметь возможность хранить их.
Наличие глобального обработчика событий вытекает из способа, которым React привязывает обработчики событий к DOM элементам.
Когда мы используем $on*$ атрибуты, мы описываем, какое поведение мы ожидаем, но React не привязывает эти обработчики событий непосредственно к DOM элементам.
Вместо этого React прикрепляет к корневому элементу один глобальный обработчик событий, который прослушивает все события за счет \textbf{всплытия событий}. Когда интересующее нас событие создается браузером, React вызывает соответствующий обработчик в нужном компоненте. Этот подход называется \textbf{делегацией событий} и используется для оптимизации используемой памяти и производительности.
\section{Refs}
Одна из причин популярности React -- его декларативность. Это значит, что вы просто описываете, что должно быть на экране, а React берет на себя всю работу с браузером. Эта возможность делает React очень простым для понимания с одной стороны и мощным инструментом с другой.
Однако, в некоторых случаях вам может понадобиться получить доступ к внутренним DOM элементам для выполнения императивных операций. В общем случае стоит избегать использование такой возможности, так как чаще всего для достижения того же результата найдется другой способ, который лучше соответствует стилю React.
Предположим, что мы хотим создать форму, где будет поле ввода и кнопка, и при нажатии на кнопку фокус должен переходить на поле ввода.
Первое, что приходит в голову, использовать метод $focus$ элемента поля ввода непосредственно в DOM дереве.
Давайте создадим компонент $Focus$, в конструкторе которого привяжем метод $handleClick$ к экземпляру этого класса:
\begin{lstlisting}
class Focus extends React.Component
\end{lstlisting}
Мы будем ожидать нажатия по кнопке, чтобы перенести фокус на поле ввода:
\begin{lstlisting}
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
\end{lstlisting}
Затем реализуем сам метод $handleClick$:
\begin{lstlisting}
handleClick() {
this.element.focus()
}
\end{lstlisting}
Как вы можете увидеть, мы обращаемся к атрибуту $element$ и вызываем на нем метод $focus$.
Чтобы понять, откуда приходит этот атрибут, нужно посмотреть на метод render:
\begin{lstlisting}
render() {
return (
<form>
<input
type="text"
ref={element => (this.element = element)}
/>
<button onClick={this.handleClick}>Focus</button>
</form>
)
}
\end{lstlisting}
Собственно здесь и ответ. Мы создаем форму с полем ввода внутри и передаем функцию через параметр $ref$.
Функция, которую мы передаем, вызывается сразу после монтирования компонента, а параметр $element$ ссылается на соответствующий элемент в DOM дереве. Важно помнить, что когда компонент будет размонтирован, эта же функция будет вызвана с параметром $null$. Это сделано для освобождения памяти и отсутствия утечек памяти.
Все что мы делаем в этой функции, сохраняем ссылку на элемент для дальнейшего использования (как в $handleClick$ в нашем примере). Если запустить пример в браузере, то при нажатии на кнопку, как и ожидается, фокус будет переходить на поле ввода.
! Как мы уже упоминали ранее, в общем случае стоит избегать использования ссылок на DOM элементы, так как это делает код более императивным, что уменьшает его читаемость и поддерживаемость.
Один из случаев, когда мы вынуждены обращаться к этому методу без других альтернатив, это интеграция с другими императивными библиотеками, такими как jQuery.
Также важно знать, что если мы используем атрибут ref на другом React компоненте, то получаем внутри функции обратного вызова не DOM элемент, а ссылку на экземпляр этого компонента. Эта возможность расширяет наши возможности, так как дает доступ к состоянию дочерних элементов, но также влечет опасности, поэтому следует по возможности избегать ее использования.
Посмотрим на пример использования этой возможности, для чего создадим два компонента:
\begin{itemize}
\item Первый компонент -- простое контролируемое поле ввода. В этом компоненте мы создадим метод $reset$, который будет сбрасывать значение поля ввода к пустой строке.
\item Второй компонент -- форма с предыдущим полем ввода и кнопкой, по нажатию на которую вызывается метод $reset$ и поле ввода очищается.
\end{itemize}
Начнем с создания поля ввода:
\begin{lstlisting}
class Input extends React.Component
\end{lstlisting}
В конструкторе мы определим начальное значение поля ввода (пустая строка) и привяжем методы $onChange$, который нужен для контроля поля ввода, и $reset$ к экземпляру этого класса:
\begin{lstlisting}
constructor(props) {
super(props)
this.state = {
value: '',
}
this.reset = this.reset.bind(this)
this.handleChange = this.handleChange.bind(this)
}
\end{lstlisting}
Функция $reset$ просто меняет текущее значение поля ввода на пустую строку:
\begin{lstlisting}
reset() {
this.setState({
value: '',
})
}
\end{lstlisting}
Метод $handleChange$ нужен для синхронизации состояния компонента и значение поля ввода:
\begin{lstlisting}
handleChange({ target }) {
this.setState({
value: target.value,
})
}
\end{lstlisting}
И в методе $render$ мы определяем элемент $input$ с контролируемым компонентом значением:
\begin{lstlisting}
render() {
return (
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
)
}
\end{lstlisting}
Теперь мы можем создать компонент $Reset$, который использует предыдущий компонент и вызывает метод $reset$ по нажатию на кнопку:
\begin{lstlisting}
class Reset extends React.Component
\end{lstlisting}
В конструкторе мы как всегда привяжем обработчик событий к экземпляру этого класса:
\begin{lstlisting}
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
\end{lstlisting}
Самое интересное находится внутри метода $handleClick$, где мы можем вызывать метод $reset$ компонента с полем ввода:
\begin{lstlisting}
handleClick() {
this.element.reset()
}
\end{lstlisting}
И в конце мы определяем метод $render$:
\begin{lstlisting}
render() {
return (
<form>
<Input ref={element => (this.element = element)} />
<button onClick={this.handleClick}>Reset</button>
</form>
)
}
\end{lstlisting}
Как вы можете увидеть использование атрибута $ref$ выглядит одинаково и для элементов DOM дерева и для других компонентов.
Таким образом мы можем получать доступ к методам дочерних компонентов. С одной стороны это очень полезная возможность, с другой стоит использовать ее крайне осторожно, так как это значительно усложняет рефакторинг. Например, если мы захотим переименовать метод $reset$, то нам придется найти все родительские компоненты, которые используют этот компонент, и поправить их тоже.
React прекрасен своим функциональным подходом и декларативным API, но также позволяет напрямую обращаться к нижележащим элементам DOM дерева и компонентам для реализации сложных сценариев взаимодействия с пользователем.
\section{Анимации}
Когда мы задумываемся о UI и браузере, мы также не должны забывать об анимации.
Анимированные UI гораздо дружелюбнее статических, а также это хороший способ показать пользователю, что что-то произошло или требует его участия.
Эта глава не ставит своей целью, научить создавать красивые анимации и UI; здесь мы поговорим о базовых инструментах создания анимаций и распространенные решения для анимирования React компонент.
Для библиотек UI в целом и для React в частности очень важно предоставлять удобный способ для создания и управления анимацией. В React есть расширение, которое позволяет нам создавать анимации в декларативном стиле и называется \textit{react-addons-css-transition-group}.
Давайте посмотрим, как мы можем создать простой эффект плавного появления элемента(fade-in) для текстового поля, сначала с использованием этого расширения, а затем с помощью сторонней библиотеки \textit{react-motion}, которая делает процесс создания анимаций еще проще.
Для того, чтобы начать работать с этим расширением, нам нужно его установить:
\begin{lstlisting}
npm install --save react-addons-css-transition-group
\end{lstlisting}
После этого мы можем импортировать компонент $CSSTransitionGroup$:
\begin{lstlisting}
import CSSTransitionGroup from 'react-addons-css-transition-group'
\end{lstlisting}
После этого мы оборачиваем компонент, к которому хотим применить анимации, следующим блоком:
\begin{lstlisting}
const Transition = () => (
<CSSTransitionGroup
transitionName="fade"
transitionAppear
transitionAppearTimeout={500}
>
<h1>Hello React</h1>
</CSSTransitionGroup>
)
\end{lstlisting}
Здесь используется несколько параметров, которые определенно требуют объяснения.
Сначала мы определяем $transitionName$. $ReactCSSTransitionGroup$ добавляет класс, указанный в этом параметре, к дочернему элементу, что позволяет использовать CSS переходы(CSS transitions) для создания анимаций.
Но одного класса недостаточно для создания правильных анимаций, поэтому $CSSTransitionGroup$ добавляет множество классов в соответствии с состоянием анимации.
В данном случае, передавая атрибут $transitionAppear$, мы говорим компоненту, что мы хотим анимировать дочерние компоненты, когда они появятся на экране.
Таким образом, компоненту будет присвоен класс $fade-appear$ (где fade взят из параметра $transitionName$) сразу после отображения на экране.
Сразу после этого компоненту будет присвоен класс $fade-appear-active$, что позволяет вызвать анимацию из начального состояния в следующее из CSS.
Также мы передаем атрибут $transitionAppearTimeout$, чтобы указать продолжительность анимации. React не будет удалять компонент из DOM дерева до завершения анимации.
Далее нам нужно определить CSS для работы нашей анимации.
В начальном состоянии нам нужно сделать элемент прозрачным:
\begin{lstlisting}
.fade-appear {
opacity: 0.01;
}
\end{lstlisting}
Затем мы определяем анимацию перехода с помощью второго класса. Анимация сработает сразу после того, как элемент получит этот класс:
\begin{lstlisting}
.fade-appear.fade-appear-active {
opacity: 1;
transition: opacity .5s ease-in;
}
\end{lstlisting}
Таким образом мы создали эффект появления элемента, который длится $500ms$.
Выглядит довольно просто, и такой подход позволяет создавать сложные анимации, состоящие из множества состояний.
Например, классы $*-enter$ и $*-enter-active$ добавляются, когда новый элемент добавляется в $CSSTransitionGroup$.
При удалении элементов также добавляются соответствующие классы.
\subsection{React motion}
По мере роста сложности анимаций, когда одни анимации начинают зависеть от других, или нам нужно поведение элементов схожее с физическими объектами, мы понимаем, что группы переходов становится недостаточно. В этот момент мы можем задуматься об использовании сторонних библиотек.
Одна из самых используемых библиотек для создания анимаций -- $react-motion$, которую поддерживает Ченг Лу(Cheng Lou). Эта библиотека предоставляет чистый и простой API для создания анимаций.
Для ее использования нужно ее установить:
\begin{lstlisting}
npm install --save react-motion
\end{lstlisting}
После установки библиотеки, мы можем импортировать из нее компонент \textbf{Motion} и функцию \textbf{spring}. Компонент потребуется для оборачивания других компонентов, которые мы хотим анимировать, а функция spring может интерполировать значение из начального в заданное конечное:
\begin{lstlisting}
import { Motion, spring } from 'react-motion'
\end{lstlisting}
Посмотрим на пример кода:
\begin{lstlisting}
const Transition = () => (
<Motion
defaultStyle={{ opacity: 0.01 }}
style={{ opacity: spring(1) }}
>
{interpolatingStyle => (
<h1 style={interpolatingStyle}>Hello React</h1>
)}
</Motion>
)
\end{lstlisting}
Здесь есть несколько интересных вещей.
Вы можете заметить, что здесь используется паттерн Функция как Потомок (в Главе 4 он разобран подробно). Это очень удобен для передачи компоненту параметров, которые определяются во время исполнения программы.
Также мы можем увидеть, что у компонента $Motion$ есть два атрибута, первый из которых $defaultStyle$, определяющий начальное состояние.
В этом примере мы устанавливаем прозрачность в $0.01$, чтобы компонент был скрыт в начальный момент времени.
В атрибуте $style$ мы определяем конечное состояние, но вместо того, чтобы установить непосредственное значение, мы используем функцию spring, чтобы значение плавно менялось от начального значения к конечному.
На каждой итерации функции spring создается новое значение, соответствующее данному моменту времени, которое передается через аргумент $interpolatingStyle$ дочерней функции.
Эта библиотека умеет делать еще множество полезных вещей, но на первом шаге этого должно быть достаточно для знакомства с ее основами.
Сравнение подходов $CSSTransitionGroup$ и $react-motion$ может быть интересным занятием с целью выбрать наиболее подходящий для вашего проекта.
\section{Векторная графика, SVG}
И в завершение поговорим еще об одном не менее важном инструменте, который доступен нам в браузере и позволяет создавать масштабируемые иконки и графики, а именно об \textbf{SVG (Scalable Vector Graphics, Масштабируемая векторная графика)}
SVG -- это инструмент декларативного описания векторов, что очень хорошо коррелирует с подходами React.
Возможно, вы привыкли создавать иконки посредством использования специальных шрифтов, но у этого способа есть известные проблемы. Их не так удобно позиционировать с помощью CSS и они могут выглядеть не лучшим образом в различных браузерах. Поэтому мы рекомендуем отдать предпочтение SVG для создания иконок.
Для React нет принципиальной разницы между отрисовкой тега $div$ и элемента SVG, что очень хорошо работает в наших целях.
Также плюсом SVG является возможность его редактирования во время исполнения посредством JavaScript и CSS, что заставляет его выглядеть еще лучше внутри функционального подхода React.
Таким образом, если мы смотрим на компоненты как на функции от передаваемых компонентам параметрам, то мы можем легко представить, как создать компонент для отображения SVG элементов и как управлять им посредством передачи различных параметров.
Таким образом, стандартным способом работы с SVG элементами в React является оборачивание их в компоненты.
Давайте посмотрим на пример, в котором мы отображаем синий круг с помощью SVG элемента, обернутого в React компонент:
\begin{lstlisting}
const Circle = ({ x, y, radius, fill }) => (
<svg>
<circle cx={x} cy={y} r={radius} fill={fill} />
</svg>
)
\end{lstlisting}
Как вы можете увидеть, мы можем использовать функциональный компонент, который принимает все необходимые для отображения круга параметры и создает SVG элемент.
Таким образом, компонент $Circle$ -- лишь шаблон для отображения круга, который мы можем использовать множество раз внутри нашего приложения.
Также определим типы его параметров:
\begin{lstlisting}
Circle.propTypes = {
x: React.PropTypes.number,
y: React.PropTypes.number,
radius: React.PropTypes.number,
fill: React.PropTypes.string,
}
\end{lstlisting}
Не стоит пренебрегать указанием типов параметров компонента, так как при дальнейшем его использовании можно будет без чтения кода понять, какие и какого типа параметры нужно передать компоненту.
Этот компонент мы можем использовать следующим образом:
\begin{lstlisting}
<Circle x={20} y={20} radius={20} fill="blue" />
\end{lstlisting}
Мы можем использовать все возможности React и установить часть значений по умолчанию, тогда, даже если мы не передадим параметры, мы получим какой-то результат.
Например, мы можем установить цвет круга по умолчанию:
\begin{lstlisting}
Circle.defaultProps = {
fill: 'red',
}
\end{lstlisting}
Это очень удобно, когда мы создаем UI компоненты, которые будут потом использоваться другими членами команды, так как в этом случае им не придется пересоздавать SVG элемент с нуля, чтобы использовать с другими параметрами.
Хотя иногда может быть наоборот удобнее зафиксировать некоторые параметры, чтобы сделать компонент более узкоспециализированным.
Например, мы можем создать узкоспециализированный компонент $RedCircle$ для создания красных кругов:
\begin{lstlisting}
const RedCircle = ({ x, y, radius }) => (
<Circle x={x} y={y} radius={radius} fill="red" />
)
\end{lstlisting}
В данном компоненты мы просто отображаем предыдущий компонент $Circle$ с зафиксированным цветом, передавая остальные параметры без изменений.
Соответственно у этого компонента будет следующий интерфейс:
\begin{lstlisting}
RedCircle.propTypes = {
x: React.PropTypes.number,
y: React.PropTypes.number,
radius: React.PropTypes.number,
}
\end{lstlisting}
Как мы видим, за счет декларативной структуры самого SVG мы получаем очень органичный способ их использования внутри React компонент.
\section{Заключение}
В этой главе мы разобрали несколько вопросов, которые появляются, когда мы начинаем иметь дело с браузерами. Мы разобрались с созданием форм, обработкой событий, созданием анимаций и SVG графики.
React предоставляет для нас все возможности для создания web приложений в декларативном стиле.
Однако, он также позволяет работать напрямую с элементами DOM дерева в императивном стиле, что очень удобно, если нам нужно интегрировать приложение с существующими библиотеками, созданными в императивном стиле.
В следующе главе мы детально посмотрим на CSS и внутренние(inline) стили, а также разберемся, как писать CSS в JavaScript.