Привет. 24–25 сентября в Москве прошла конференция фронтенд-разработчиков HolyJs https://holyjs-moscow.ru/. Мы на конференцию пришли со своим стендом, на котором проводили quiz. Был основной квиз — 4 отборочных тура и 1 финальный, на котором были разыграны Apple Watch и конструкторы лего. И отдельно мы провели квиз на знание react.
Под катом — разбор задач квиза по react. Правильные варианты будут спрятаны под спойлером, поэтому вы можете не только почитать разбор, но и проверить себя :)

Поехали!
Для удобства мы сгруппировали вопросы по секциям:
Секция 1. Базовое понимание работы this.setState и updating lifecycle компонента:
Вопрос 1.
Выберите наиболее полный список способов обновить react-компонент: 1) SetProps, SetState, ForceUpdate 2) ForceUpdate, SetState 3) ForceUpdate, SetState, Parent (re)render 4) ForceUpdate, SetState, directly call UpdateComponent
3) ForceUpdate, SetState, Parent (re)render
Вопрос 2.
Что произойдет, если вызвать this.setState({}) в react 1) Компонент пометится грязным, вызовется updating lifecycle 2) Ничего не произойдет, компонент не обновится 3) React упадет с ошибкой "Object cannot be empty" 4) Все поля в state будут заресечены
1) Компонент пометится грязным, вызовется updating lifecycle
Для ответа на вопрос разберем 2 части:
1) Собственный запрос компонента на updating цикл
2) Запрос снаружи компонента
У самого компонента есть 2 способа обновить самого себя:
1) this.setState и this.forceUpdate. В этом случае компонент будет помечен грязным и на тик Reconcilliation, если он будет в приоритете на рендеринг, запустится updating цикл.
Интересный факт: this.setState({}) и this.forceUpdate отличаются. При вызове this.setState({}) вызывается полный updating цикл, в отличие от this.forceUpdate, когда updating цикл запускается без shouldComponentUpdate метода. Пример работы this.setState({}) можно посмотреть здесь: https://codesandbox.io/s/m5jz2701l9 (если заменить в примере setState на forceUpdate, можно посмотреть, как изменится поведение компонентов).
2) Когда родитель компонента ререндерится, он возвращает часть vDOM, все children, которые должны будут обновиться, — и у них также будет вызван полный updating lifecycle. Полного пересчета поддерева можно избежать, описав shouldComponentUpdate или определив компонент как PureComponent.
Вопрос 3
Чем отличается Component от PureComponent (PC) 1) Component не поддерживает наследование, в отличие от Pure 2) PC реализует SCU, проводит shallowEqual props и state 3) PC используют только для компонентов, которые зависят от store 4) В PC необходимо определять функцию shouldComponentUpdate
2) PC реализует SCU, проводит shallowEqual props и state
Как мы обсудили ранее, при (ре)рендеринге родителя все поддерево будет отправлено на updating lifeCycle. Представьте, что у вас обновился корневой элемент. В этом случае по цепному эффекту у вас должно будет обновиться практически все react-дерево. Чтобы оптимизировать и не отправлять лишнее на updating, в react есть метод shouldComponentUpdate, который позволяет вернуть true, если компонент должен обновиться, и false в ином случае. Для упрощения сравнения в react, можно унаследоваться от PureComponent, чтобы получить сразу готовый shouldComponentUpdate, который сравнит по ссылке (если речь идет об object types) или по значению (если речь про value types) все props и state, которые приходят в компонент.
Вопрос 4.
this.setState(() => {}, () => {}) — зачем нужно передавать вторую функцию в setState? 1) set принимает набор объектов. Они смержатся перед updating 2) Вторая функция будет вызвана после обновление state 3) setState принимает только 1 аргумент
2) Вторая функция будет вызвана после обновление state
В React-lifecycle есть два метода: componentDidMount для mounting цикла и componentDidUpdate для updating, где можно добавить какую-то логику после обновления компонента. Например, сделать http-запрос, внести какие-то стилевые изменения, получить метрики html-элементов и (по условию) сделать setState. Если же вы хотите сделать какое-то действие после изменения определенных полей в state, то в методе componentDidUpdate придется писать либо сравнение:
componentDidUpdate(prevProp, prevState) { if (prevState.foo !== this.state.foo) { // do awesome things here } }
Либо вы можете сделать это по setState:
setState( // set new foo {foo: 'baz'}, () => { // do awesome things here } );
У каждого подхода есть плюсы и минусы (например, если вы изменяете setState в нескольких местах, может оказаться удобнее написать один раз условие).
Вопрос 5.
Сколько раз будет выведено в консоль render: class A extends React.PureComponent { render() { console.log('render'); return <div /> } } function Test() { return <A foo='bar' onClick={() => console.log('foo')} /> } const rootElement = document.getElementById("root"); ReactDOM.render(<Test />, rootElement); setTimeout(() => ReactDOM.render(<Test />, rootElement)); 1) 1 2) 2 3) 3 4) 0
2) 2
Вопрос 6.
Сколько раз будет выведено в консоль render: class A extends React.PureComponent { render() { console.log('render'); return <div /> } } function Test() { return <A foo='bar' /> } const rootElement = document.getElementById("root"); ReactDOM.render(<Test />, rootElement); setTimeout(() => ReactDOM.render(<Test />, rootElement)); 1) 1 2) 2 3) 3 4) 0
1) 1
Вопрос 7.
Сколько раз будет выведено в консоль render: class A extends React.PureComponent { componentDidMount() { console.log('render'); } render() { return <div /> } } const rootElement = document.getElementById("root"); ReactDOM.render(<A />, rootElement); setTimeout(() => ReactDOM.render(<A />, rootElement)); 1) 1 2) 2 3) 3 4) 0
1) 1
Вопросы 5–7 Нужны для одного и того же — проверить понимание работы PureComponent и обновления компонентов при передаче props. Если внутри метода render мы передаем в виде jsx колбек, описывая это прямо в функции render:
render () { return <Button onClick={() => {}} />; }
То каждый render родителя будет обновлять данный хендлер клика. Это происходит, потому что при каждом рендере создается новая функция с уникальной ссылкой, которая при сравнении в PureComponent выдаст, что новые props не равны старым и нужно обновить компонент. В случае же, когда все проверки проходят и shouldComponentUpdate возвращает false, обновления не происходит.
Секция 2. Keys in React
Подробный разбор работы keys мы публиковали здесь: https://habr.com/company/hh/blog/352150/
Вопрос 1.
Для чего может потребоваться key, если работа происходит не с массивом? 1) Удалить предыдущий инстанс и замаунтить новый при смене key 2) Дополнительный способ вызвать updating lifecycle 3) Причин использовать key нет 4) Для форсирования механизма reconciliation
1) Удалить предыдущий инстанс и замаунтить новый при смене key
Без использования key react будет сравнивать список элементов попарно сверху вниз. Если мы используем key, сравнение будет происходить по соответствующим key. Если появился новый key — то такой компонент не будет сравниваться ни с кем и сразу будет создан с нуля.
Этим способом можно пользоваться, даже если у нас есть 1 элемент: мы можем задать <A key="1" />, в следующем рендере укажем <A key="2" /> и в таком случае react удалит <A key="1" /> и создаст с нуля <A key="2" />.
Вопрос 2.
Имеет ли сам компонент доступ к this.prop.key? 1) Да 2) Нет 3) Необходимо определить static getKey
2) Нет
Компонент может узнать key у своих children, которые были переданы ему в качестве prop, но не может узнать о своем key.
Вопрос 3.
Сколько раз будет выведено в консоль render: class A extends React.PureComponent { componentDidMount() { console.log('render'); } render() { return <div /> } } const rootElement = document.getElementById("root"); ReactDOM.render(<A key='1' />, rootElement); setTimeout(() => ReactDOM.render(<A />, rootElement)); 1) 1 2) 2 3) 3 4) 0
2) 2
При изменении key компонент будет пересоздан, поэтому render будет выведен дважды.
Секция 3. Вопросы по jsx
Вопрос 1.
Выберите подходящий ответ. Дочерний компонент может уведомить своего родителя об изменениях с помощью 1) Колбека в виде prop / context 2) Выноса слоя модели и работы через нее 3) Определения setParentProps 4) Через static getParentRef
1) Колбека в виде prop / context
2) Выноса слоя модели и работы через нее
Здесь есть два правильных ответа. Выбор любого из них на квизе засчитает вам баллы. Данный вопрос на знания data-flow react. Данные сверху вниз распространяются в виде props или context, в них может быть callback, который компонент ниже может вызывать, чтобы повлиять на состояние системы.
Другой способ, сочетающий вынос модели, context и prop, — это, например, react-redux биндинг.
Эта библиотека берет вынесенную из react модель (redux). Сетит redux.store в Provider, который на самом деле сетит store в context. Затем разработчик использует HOC connect, который идет в контекст, подписывается на изменения store (store.subscribe) и при изменении store пересчитывает mapStateToProps функцию. Если данные изменились, сетит их в props в оборачиваемый объект.
В то же время connect позволяет указать mapDispatchToProps, где разработчик указывает те actionCreators, которые необходимо передать в компонент. Их, в свою очередь, мы получаем извне (без контекста), биндим actionCreators на store (оборачиваем их в store.dispatch) и передаем в качестве props оборачиваемому компоненту.
Вопрос 2.
В какие props можно передавать jsx? Выберите наиболее подходящий ответ 1) В любые 2) Только в children
1) В любые
Передавать можно в любые. Например:
<Button icon={<Icon kind='warning'/>}>Внимание</Button>
Нарисует кнопку с иконкой. Такой подход очень удобно использовать, чтобы оставлять за компонентом право управлять расположением различных элементов относительно друг друга, а не перебирать один children prop.
Секция 4. Продвинутое понимание setState
Здесь 3 сильно связанных вопроса:
Вопрос 1.
this.state = {a: 'a'}; ... this.setState({a: 'b'}); this.setState({a: this.state.a + 1}) this.state? 1) {a: 'a1'} 2) {a: 'b1'} 3) Недостаточно данных 4) {a: 'a'}
3) Недостаточно данных
Вопрос 2.
this.state={a: 'a'} ... this.setState({a: 'b'}) this.setState(state => ({a: state.a + 1})) this.state? 1) {a: 'a1'} 2) {a: 'b1'} 3) Недостаточно данных 4) {a: 'ab1'}
2) {a: 'b1'}
Вопрос 3.
При вызове подряд 2 setState внутри componentDidUpdate сколько updating lifecycle будет вызвано 1) 1 2) 2 3) 3 4) Недостаточно данных
1) 1
Вся работа setState полностью описана здесь:
1) https://reactjs.org/docs/react-component.html#setstate
2) https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973
Дело в том, что setState не происходит синхронно.
И в случае, если есть несколько вызовов setState подряд, то в зависимости от того, находимся ли мы внутри react-lifecycle метода, функции-обработчика react-события (onChange, onClick) или нет, зависит исполнение setState.
Внутри react обработчиков setState работает батчево (изменения накатываются только после того, как пользовательские функции в call stack закончатся и мы попадем в функции, которые вызывали наши event handler и lifecycle методы). Они накатываются подряд друг за другом, поэтому в случае, если мы находимся внутри react-handler, мы получим:
this.state = {a: 'a'}; // a: 'a' ... this.state.a // a: 'a' this.setState({a: 'b'}); // a: 'b' + компонент не обновляется. Была зарегистрирована только необходимость в этом this.state.a // a: 'a' this.setState({a: this.state.a + 1}) // a: 'a1'
так как изменения произошли батчево.
Но в тоже время, если setState был вызван вне react-handlers:
this.state = {a: 'a'}; // a: 'a' ... this.state.a // a: 'a' this.setState({a: 'b'}); // a: 'b' + компонент ушел на ререндер this.state.a // a: 'b' this.setState({a: this.state.a + 1}) // a: 'b1' + компонент ушел на ререндер
Так как в этом случае изменения будут накатываться отдельно.
Секция 5. Redux
Вопрос 1.
Можно ли задавать кастомные action, например () => {} ? 1) Нет. Все action должны быть объектом с полем type 2) Да, но такой action должен вернуть объект с полем type 3) Да, нужно определить кастомный middleware для такого action 4) Да, но такая функция должна принимать метод dispatch
3) Да, нужно определить кастомный middleware для такого action
Возьмем в качестве простейшего примера redux-thunk. Весь middleware — это небольшой блок кода:
https://github.com/reduxjs/redux-thunk/blob/master/src/index.js#L2-L9
return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); };
Как работают middleware?
Они получают управление до того, как action придет в store. Поэтому action, который был задиспачен, вначале пройдет по цепочке middleware.
Каждый middleware принимает инстанс store, метод next, который позволяет пробросить action далее, и cам action.
Если middleware обрабатывает кастомные action, как, например, redux-thunk, то он в случае, если action является функцией, не пробрасывает action далее, а "заглушает" его, вместо этого вызывая action с передачей туда метода dispatch и getState.
Что бы случилось, если redux-thunk сделал next для action, который является функцией?
Перед вызовом редьюсеров store проверяет тип action. Он должен удовлетворять следующим условиям:
1) Это должен быть объект
2) У него должно быть поле type
3) Поле type должно быть типа string
Если одно из условий не выполняется, redux выдаст ошибку.
Бонусные вопросы:
Бонусный вопрос 1.
Что будет выведено? class Todos extends React.Component { getSnapshotBeforeUpdate(prevProps, prevState) { return this.props.list.length - prevProps.list.length; } componentDidUpdate(a, b, c) { console.log(c); } ... } ReactDOM.render(<Todos list={['a','b']} />, app); setTimeout(() => ReactDOM.render(<Todos list={['a','b','a','b']} />, app), 0); a) 0 b) 1 c) 2 d) undefined
c) 2
getSnapshotBeforeUpdate — редко используемая функция в react, которая позволяет получить снепшот, который затем будет передан в componentDidUpdate. Этот метод нужен, чтобы заранее подсчитать те или иные данные, на основе которых можно затем сделать, например, fetch-запрос.
Бонусный вопрос 2.
Чему будет равно значение в инпуте через 2,5 секунды? function Input() { const [text, setText] = useState("World!"); useEffect( () => { let id = setTimeout(() => { setText("Hello " + text); }, 1000); return () => { clearTimeout(id); }; }, [text] ); return ( <input value={text} onChange={e => { setText(e.target.value); }} /> ); } a) "World!" b) "Hello World!" c) "Hello Hello World!" d) В коде ошибка
c) "Hello Hello World!"
Это уже вопрос на знание новых фичей в react, его не было в нашем квизе. Давайте попробуем в комментариях подробно описать работу кода из последнего вопроса :)
