Разбор конкурса-квиза по React со стенда HeadHunter на HolyJs 2018

в 10:22, , рубрики: javascript, quiz, React, ReactJS, Блог компании HeadHunter, разбор задач

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

Под катом — разбор задач квиза по react. Правильные варианты будут спрятаны под спойлером, поэтому вы можете не только почитать разбор, но и проверить себя :)

image

Поехали!

Для удобства мы сгруппировали вопросы по секциям:

Секция 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

Разбор вопросов 1 и 2

Для ответа на вопрос разберем 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

Вопросы 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

Разбор вопросов 1–3

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

Автор: xnim

Источник

  1. Муз:

    че за конкурсы капец. вот помню на https://gorodrabot.ru?smi=1 стольники раздавали, вот это борьба была)))

* - обязательные к заполнению поля