Привет. 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, его не было в нашем квизе. Давайте попробуем в комментариях подробно описать работу кода из последнего вопроса :)