Перейти к содержанию

Оптимизация. Рефакторинг

В нашем решении есть слабые места:

  • некоторые названия переменных избыточны (чтобы было понятно, добавлено Actions у экшенов, которые мы приклеиваем);
  • повторяющийся однотипный код (5 кнопок с номером года в <Page />);
  • в action улетает текст с кнопки, если текст изменится - код сломается. Проблема: большая связанность. Нужно облегчить.
  • возможно существует более простой путь "достать" из вк фото за конкретный год (не рассматриваю это как проблему);
  • фраза "Привет, ИМЯ" после обновления страницы заменяется кнопкой "войти", то есть не отображает реальной картины (фотографии у нас при этом доступны для загрузки, то есть мы уже авторизованы);
  • после авторизации (или после перезагрузки) было бы неплохо сразу загружать фото для 2018 года, так как юзер видит пустой экран и заголовок 2018;

Можно отнести это к "доработкам". Однако у нас есть место, которое является опасным и о котором я лишь вскользь говорил в учебнике, пора исправится.

Приглашаю вас "убить" главную проблему текущего приложения - лишние перерисовки компонента в следующем подразделе.

Остальные проблемы и будущие доработки живут в одноименном разделе.

Главная проблема приложения

У нас есть 2 компонента <User /> и <Page />. Мы специально сделали для них два редьюсера, чтобы обновлять их независимо! А у нас? А у нас <User /> каждый раз обновляется при обновлении <Page /> и наоборот.

Добавьте console.log в render метод у <User />:

src/components/User.js

1
2
3
4
render() {
  console.log('<User/> render')
  return <div className="ib user">{this.renderTemplate()}</div>
}

user-render-console-log

Перерисовка компонента User происходит постоянно. Это не влияет на производительность нашего мини-приложения, однако, мы не готовы с этим мириться.

Представьте дашборд (панель) с большим количеством виджетов, информация в которых обновляется по событиям от бэкэнда. Если каждый виджет будет перерисовывать полностью весь дашборд, то это будет крайне некрасиво (как для юзера, так и для производительности).

Чтобы такого не было, мы должны каждую отдельную сущность приложения класть в отдельный контейнер.

Будем исправлять, для этого:

<App /> становится тупым компонентом, который рендерит 2 контейнера:

  • <PageContainer />
  • <UserContainer />

Данные контейнеры - просто обертки над нашими компонентами, в которых мы "подключаемся (connect) к Redux".

Так же, я сразу заменю название у экшенов внутри mapDispatchToProps: уберу оттуда частичку Action.

В остальном, мы просто "разносим" то, что было в <App /> по раздельным контейнерам.

src/index.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './store/configureStore';
import App from './components/App'; // изменили путь

import registerServiceWorker from './registerServiceWorker';

import './index.css';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
registerServiceWorker();

src/components/App.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import React, { Component } from 'react';
import UserContainer from '../containers/UserContainer'; // изменили импорт
import PageContainer from '../containers/PageContainer'; // изменили импорт

class App extends Component {
  render() {
    return (
      <div className="app">
        <PageContainer />
        <UserContainer />
      </div>
    );
  }
}

export default App;

src/containers/PageContainer.js

 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
import React from 'react';
import { connect } from 'react-redux';
import { Page } from '../components/Page';
import { getPhotos } from '../actions/PageActions';

class PageContainer extends React.Component {
  render() {
    const { page, getPhotos } = this.props;
    return (
      <Page
        photos={page.photos}
        year={page.year}
        isFetching={page.isFetching}
        error={page.error}
        getPhotos={getPhotos}
      />
    );
  }
}

const mapStateToProps = (store) => {
  return {
    page: store.page,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    getPhotos: (year) => dispatch(getPhotos(year)),
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(PageContainer);

Как вы могли заметить, все что касалось <Page /> хранится в отдельном контейнере: подписка на часть стора, экшен, пропсы...

То же самое, делаем для <UserContainer />

src/containers/UserContainer.js

 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
import React from 'react';
import { connect } from 'react-redux';
import { User } from '../components/User';
import { handleLogin } from '../actions/UserActions';

class UserContainer extends React.Component {
  render() {
    const { user, handleLogin } = this.props;
    return (
      <User
        name={user.name}
        error={user.error}
        isFetching={user.isFetching}
        handleLogin={handleLogin}
      />
    );
  }
}

const mapStateToProps = (store) => {
  return {
    user: store.user,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    handleLogin: () => dispatch(handleLogin()),
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(UserContainer);

Теперь внимание: в компоненте App есть два "независимых компонента". Сейчас при изменении данных в редьюсере для Page - User перерисовываться не будет. App тоже, само собой. App у нас вообще не будет перерисовываться более при таком раскладе.

Снова покликаем по кнопкам (console.log в <User /> остался):

user-not-re-renders-console-log

Еще раз заострю внимание: мы не просто сделали хорошо, мы сделали супер-хорошо! Render - обычно самая дорогая операция. Вызывать "перерисовку" каждого "кусочка" приложения нужно осознанно. Всегда проверяйте (например, так же банально с помощью console.log) сколько раз у вас что рендерится, и нет ли чего лишнего.

Давайте заодно здесь быстренько исправим отрисовку кнопок в <Page />

src/components/Page.js

 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
export class Page extends React.Component {
  onBtnClick = e => {
    ...
  }
  renderButtons = () => {
    const years = [2018, 2017, 2016, 2015, 2014]

    return years.map((item, index) => { // [1]
      return (
        <button key={index} className="btn" onClick={this.onBtnClick}>
          {item}
        </button>
      )
    })
  }
  renderTemplate = () => {

  }

  render() {
    const { year, photos } = this.props
    return (
      <div className="ib page">
        <p>{this.renderButtons()}</p>
        <h3>
          {year} год [{photos.length}]
        </h3>
        {this.renderTemplate()}
      </div>
    )
  }
}

(Добавьте по вкусу щепотку margin для .btn)

Использовать в данной ситуации index для key плохо?. В данном случае - не плохо. Напоминаю, что индекс в качестве ключа плохо использовать, когда у вас элементы меняются местами. Справедливости ради, здесь в качестве индекса можно было бы использовать "год", так как главное в индексе - это уникальность.

Итого: мы научились бережно относится к перерисовкам, а так же закрепили на практике вопрос зачем разбивать редьюсер на маленькие редьюсеры.

Исходный код.

Комментарии