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

Комментарии (2)


  1. AlexMiroshnikov
    29.11.2018 16:44
    +1

    Давайте попробуем в комментариях подробно описать работу кода из последнего вопроса

    Я так понимаю, происходит следующее. «Цепляемся» через хук к переменной text из state и инициируем ее как «World!», и на первом рендере значение в инпуте будет «World!». Далее, согласно reactjs.org/docs/hooks-reference.html#useeffect,
    The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.

    т.е. после первого рендера из-за useEffect запускается таймаут, который через секунду обновит text в state на «Hello World!». По истечении секунды state обновляется, value инпута становится «Hello World!», срабатывает onChange, text в state становится «Hello World!», происходит очередной рендер и снова запускается таймаут, который через секунду обновит text в state на «Hello Hello World!», и именно это значение и будет в input через 2.5 секунды. Вроде так.


  1. xnim Автор
    01.12.2018 09:01

    Разбор вопросов по JS — здесь habr.com/post/431698