Створіть програму React з нуля (частина 7): Налаштування Реагування та найкращих практик

Ця публікація є частиною серії публікацій для початківців, призначених для людей, які використовують
готові інструменти, шаблони або котли для React, але бажають навчитися та зрозуміти, як створити додаток React з самого початку.

Усі повідомлення цієї серії:
Частина 1: Вступ
Частина 2: Ініціалізація та перший файл
Частина 3: Використання синтаксису ES2015
Частина 4: Застосування посібника зі стилів
Частина 5: Налаштування Express Server
Частина 6: Використання модуля Bundler
Частина 7: Налаштування реагування та найкращих практик
Частина 8: Налаштування Redux
Частина 9: Налаштування маршрутизатора React
Частина 10: TDD та налаштування Jest

Налаштування React

У цій публікації ми збираємось налаштувати React і створити дуже простий компонент, тоді ми переглянемо деякі найкращі практики, про які слід пам’ятати, розробляючи компоненти React. Ось тут починається весела частина, тож давайте копаємось прямо!

Встановіть пакети React і React Dom як залежності:

$ npm install - зберегти реакцію reag-dom

Потім відкрийте index.js і створіть дуже простий компонент React, який представляє наш додаток. У нашому файлі шаблону index.pug у нас вже є елемент з додатком id, тому давайте використовувати його для монтажу програми.

/ **
 * index.js
 * /
імпорт React від 'реагувати';
import {render} з 'react-dom';
const MainApp = () => (
  

Привіт, реагуйте!

);
// візуалізувати додаток
render (, document.getElementById ('app'));

Цей простий код створює функціональний компонент MainApp без стану і монтує його на елементі DOM, який має додаток id. Цей код не працює негайно, і ви отримаєте помилку, якщо спробуєте створити пакет або запустити сервер.

Причиною цієї помилки є те, що у нашому файлі index.js є синтаксис JSX, який Babel ще не розуміє. Щоб дозволити Babel інтерпретувати цей синтаксис до звичайного JavaScript, ми будемо використовувати попередньо встановлену програму React для Babel.

Встановіть пакет як залежність:

$ npm install - зберегти реабілітацію бабеля

Потім додайте пресет до списку пресетів у файлі .babelrc:

{
  "пресетів": [
    "es2015",
    "етап-0",
    "реагувати"
  ],
  "plugins": ["перетворення-вбудоване-середовище-змінні"]
}

Також повинна бути помилка підшивки, яка не дозволить вам створити пакет. Лінк скаржиться на те, що index.js - це файл JavaScript, який містить синтаксис JSX, але використовує розширення js замість jsx.

Опис цього правила ви можете прочитати тут. У розділі "Коли не використовувати це" йдеться про те, що не слід використовувати це правило, якщо ви не хочете обмежувати розширення файлів, що містять синтаксис JSX.

Ви можете продовжувати використовувати це правило, але я вважаю за краще використовувати розширення js для всіх файлів, тому я відключу це правило:

{
  "extends": "airbnb",
  "env": {
    "es6": вірно,
    "браузер": true,
    "вузол": вірно
  },
  "правила": {
    "react / jsx-filename-extension": 0
  }
}

Увімкнення HMR

Увімкнути заміну гарячого модуля так само просто, як додати блок коду:

/ **
 * index.js
 * /
імпорт React від 'реагувати';
import {render} з 'react-dom';
якщо (module.hot) {
  module.hot.accept ();
}
const MainApp = () => (
  

Привіт, реагуйте!

);
// візуалізувати додаток
render (, document.getElementById ('app'));

Поради та найкращі практики

Перш ніж ми продовжимо роботу з підручником, ми ознайомимось з узагальненим списком порад та найкращих практик, які я засвоїв із досвіду роботи з React, а також з читання та пошуку в Інтернеті. Майте це на увазі, коли ви створюєте свої компоненти React.

Імпорт залежності від локального імпорту

Новою лінією відокремте залежність імпорту від місцевого імпорту. Імпортний залежність повинен вийти на перше місце.

import React, {Component} з 'реагувати';
імпортувати кориснийModule з 'корисного модуля';
імпортувати myLocalModule з './my-local-module';

Функціональні компоненти без громадянства

Якщо компонент є лише рендерінговим компонентом або йому не потрібно використовувати об'єкт State, використовуйте звичайну функцію JavaScript замість класу. Це називається функціональним компонентом без громадянства.

Тож замість цього робити:

import React, {Component} з 'реагувати';
клас MyComponent розширює компонент {
  render () {
    повернути (
      
Привіт!
    );   } }
експорт за замовчуванням MyComponent;

Зробити це:

імпорт React від 'реагувати';
const MyComponent = () => 
Привіт!
;
експорт за замовчуванням MyComponent;

Подивіться, скільки знято безладу? Ви також можете зробити це простіше, експортуючи саму функцію:

імпорт React від 'реагувати';
експортувати за замовчуванням () => 
Привіт!
;

Однак я не вважаю за краще це робити, тому що це робить налагодження важче. Якщо ви перевірте Інструменти реагування Dev, ви побачите, що назва компонента "Невідомо", оскільки функція анонімна.

Анонімний функціональний компонент

Кращим підходом було б використання звичайної іменованої функції замість анонімної функції:

імпорт React від 'реагувати';
експорт за замовчуванням функція MyComponent () {
  return 
Привіт!
; }
Названий функціональний компонент

Почніть з презентаційних компонентів

Презентаційні компоненти простіші у визначенні, простіші для розуміння та їх можна повторно використовувати знову і знову, оскільки вони не залежать від решти програми.

Якщо ви розділите свою програму на набір презентаційних компонентів, ви можете розмістити їх на одній сторінці та налаштувати їх дизайн та варіанти, щоб досягти єдиного вигляду та вигляду у всій програмі.

Створіть свій компонент як презентаційний компонент і додайте стан лише тоді, коли вам потрібно, що приведе нас до наступної підказки.

Мінімізувати використання держави

Використовуйте стан економно у своїх компонентах і переконайтеся, що вони використовують стан для інтерфейсу, а не даних, іншими словами, якщо ви не використовуєте його в render (), то він не повинен знаходитись у цьому стані. Пам’ятайте, що ви повинні використовувати setState лише у тому випадку, якщо хочете переглянути свій компонент.

Скажімо, у нас є компонент, який складається з однієї кнопки. Цю кнопку можна натиснути лише один раз, і коли її натиснути, на консоль буде записано повідомлення:

import React, {Component} з 'реагувати';

клас MyComponent розширює компонент {
  state = {
    clickOnce: false,
  };
  handleClick = () => {
    якщо (! this.state.clickedOnce) {
      console.log ("Клацнув");
    }
    this.setState ({
      clickOnce: вірно,
    });
  }
  компонентDidUpdate () {
    console.log ('Оновлено!');
  }
  render () {
    повернути (
      
        <кнопка onClick = {this.handleClick}> Клацніть на мене       
    );   } }
експорт за замовчуванням MyComponent;

Це приклад поганої реалізації, використовуючи стан, щоб встановити прапор, натиснутийOnce, який визначає, чи можна натиснути кнопку знову. Кожного разу при натисканні кнопки компонент буде повторно відображатися, навіть якщо цього не потрібно.

Додаток відновлює натискання кнопки

Це було б краще:

import React, {Component} з 'реагувати';

клас MyComponent розширює компонент {
  clickedOnce = помилково;
  
  handleClick = () => {
    if (! this.clickedOnce) {
      console.log ("Клацнув");
    }
    this.clickedOnce = true;
  }
  компонентDidUpdate () {
    console.log ('Оновлено!');
  }
  render () {
    повернути (
      
        <кнопка onClick = {this.handleClick}> Клацніть на мене       
    );   } }
експорт за замовчуванням MyComponent;

Ця реалізація використовує властивість класу замість ключа стану, оскільки прапор clickedOnce не являє собою стан користувальницького інтерфейсу і, отже, не повинен жити всередині компонентного стану. З цією новою реалізацією натискання на кнопку не раз запускає оновлення.

Завжди визначайте propTypes та defaultProps

Усі компоненти повинні мати propTypes та defaultProps, визначені якомога вище в межах компонента. Вони служать документацією для компонентів і повинні бути негайно видимими для інших розробників, які читають файл.

Оскільки React v15.5, React.PropTypes перемістився в інший пакет, тож давайте встановимо його як залежність:

$ npm install - збереження типів опори

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

Функції розміщені в JavaScript, це означає, що ви можете використовувати функцію до її оголошення:

імпорт React від 'реагувати';
імпортувати PropTypes з 'типів prop';
MyComponent.propTypes = {
  назва: PropTypes.string,
};
MyComponent.defaultProps = {
  назва: "Простий лічильник",
};
експорт за замовчуванням MyComponent (реквізит) {
  повернути 

{props.title}

; }

ESLint поскаржиться на використання функції до її визначення, але заради кращої документації на компоненти, ми відключимо це правило зв’язування функцій, змінивши файл .eslintrc:

{
  ...
  "правила": {
    "react / jsx-filename-extension": 0,
    "без використання перед визначенням": [
      "помилка",
      {
        "функції": хибні
      }
    ]
  }
}

Для компонентів на основі класу:

На відміну від функцій, класи в JavaScript не піднімаються, тому ми не можемо просто зробити MyComponent.propTypes = ... перед визначенням самого класу, але ми можемо визначити propTypes і defaultProps як статичні властивості класу:

import React, {Component} з 'реагувати';
імпортувати PropTypes з 'типів prop';
клас MyComponent розширює компонент {
  статичні propTypes = {
    назва: PropTypes.string,
  };
  static defaultProps = {
    назва: "Простий лічильник",
  };
  render () {
    повернути 

{this.props.title}

;   } }
експорт за замовчуванням MyComponent;

Ініціалізація держави

Стан можна ініціалізувати в конструкторі компонентів:

клас MyComponent розширює компонент {
  конструктор (реквізит) {
    супер (реквізит);
    this.state = {
      кількість: 0,
    };
  }
}

Кращий спосіб - ініціалізувати стан як властивість класу:

клас MyComponent розширює компонент {
  конструктор (реквізит) {
    супер (реквізит);
  }
  state = {
    кількість: 0,
  };
}

Це виглядає набагато краще, чистіше, читабельніше, а також сприяє документації на компоненти. Об'єкт стану слід ініціалізувати після propTypes та defaultProps:

import React, {Component} з 'реагувати';
імпортувати PropTypes з 'типів prop';
клас MyComponent розширює компонент {
  // propTypes виходить першим
  статичні propTypes = {
    назва: PropTypes.string,
  };
  // defaultProps поступає другим
  static defaultProps = {
    назва: "Простий лічильник",
  };
  // конструктор приходить сюди
  конструктор () {
    ...
  }
  // тоді настає держава
  state = {
    кількість: 0,
  };
}

Передайте функцію setState

Документація з реагуванням відмовляє покладатися на значення this.state та this.props для обчислення наступного стану, оскільки React оновлює їх асинхронно. Це означає, що стан може не змінитися відразу після виклику setState ().

клас MyComponent розширює компонент {
  state = {
    кількість: 10,
  }
  onClick = () => {
    console.log (this.state.count); // 10
    
    // кількість не зміниться негайно
    this.setState ({count: this.state.count + this.props.step});
    
    console.log (this.state.count); // ще 10
  }
}

Хоча це працює для простих сценаріїв і стан все ще буде оновлюватися правильно, це може призвести до несподіваної поведінки у більш складних сценаріях.

Розглянемо цей сценарій, у вас є компонент, який відображає одну кнопку. Після натискання цієї кнопки викликається метод handleClick:

клас MyComponent розширює компонент {
  static defaultProps = {
    крок: 5,
  }
  статичні propTypes = {
    крок: PropTypes.number,
  }
  
  state = {
    кількість: 10,
  }
  
  handleClick = () => {
    this.doSomething ();
    this.doSomethingElse ();
  }
  doSomething = () => {
    this.setState ({count: this.state.count + this.props.step});
  }
  doSomethingElse = () => {
    this.setState ({count: this.state.count - 1});
  }
  render () {
    повернути (
      
        

Поточний підрахунок: {this.state.count}

        <кнопка onClick = {this.handleClick}> Клацніть на мене       
    );   } }

Кнопка викликає handleClick () при натисканні, яка в свою чергу викликає doSomething (), потім doSomethingElse (). Обидві функції будуть змінювати значення підрахунку всередині стану.

За логікою, 10 + 5 - це 15, тоді віднімаємо 1, а результат повинен бути 14, правда? Ну, в цьому випадку це не так - значення підрахунку після першого клацання дорівнює 9, а не 14. Це трапляється тому, що значення this.state.count все ще 10, коли doSomethingElse () викликається, а не 15.

Щоб виправити це, ви можете використовувати другу форму setState (), яка приймає функцію, а не об'єкт. Ця функція отримає попередній стан як перший аргумент, а реквізити під час оновлення застосовуються як другий аргумент:

this.setState ((prevState, реквізит) => ({
  кол .: prevState.count + props.step
}))

Ми можемо використовувати цю форму для виправлення нашого прикладу:

клас MyComponent розширює компонент {
  ...
  handleClick = () => {
    this.doSomething ();
    this.doSomethingElse ();
  }
  doSomething = () => {
    this.setState ((prevState, реквізит) => ({
      кол .: prevState.count + props.step
    }));
  }
  doSomethingElse = () => {
    this.setState (prevState => ({
      кол .: prevState.count - 1
    }));
  }
  ...
}

При цій реалізації кількість належним чином оновлюється від 10 до 14 до 18 тощо. Прості математики знову мають сенс!

Використовуйте функції стрілок як властивості класу

Це ключове слово завжди бентежить розробників JavaScript, і його поведінка не менш заплутана в компонентах React. Чи знаєте ви, як це ключове слово змінюється в компоненті React? Розглянемо наступний приклад:

import React, {Component} з 'реагувати';
клас MyComponent розширює компонент {
  state = {
    кількість: 0,
  };
  onClick () {
    console.log (this.state);
  }
  render () {
    повернути (
      
        

Кількість: {this.state.count}

        <кнопка onClick = {this.onClick}> Клацніть на мене       
    );   } }
експорт за замовчуванням MyComponent;

Натискання на кнопку призведе до помилки:

Uncaught TypeError: Неможливо прочитати властивість "state" невизначеного

Це тому, що onClick, як метод класу, за замовчуванням не пов'язаний. Є кілька способів виправити це. (каламбур призначений, зрозумієте?)

Один із способів - прив’язати функцію до правильного контексту під час передачі її всередині функції render ():

<кнопка onClick = {this.onClick.bind (this)}> Клацніть на мене 

Або ви можете уникнути зміни контексту, використовуючи функцію стрілки в render ():

<кнопка onClick = {e => this.onClick (e)}> Клацніть на мене 

Однак ці два способи мають невелику вартість продуктивності, оскільки функція буде перерозподілена на кожному візуалізації. Щоб уникнути цієї незначної вартості продуктивності, ви можете зв’язати функцію всередині конструктора:

import React, {Component} з 'реагувати';
клас MyComponent розширює компонент {
  конструктор (реквізит) {
    супер (реквізит);
    this.onClick = this.onClick.bind (це);
  }
  ...
  render () {
    ...
    <кнопка onClick = {this.onClick}> Клацніть на мене 
    ...
  }
}
експорт за замовчуванням MyComponent;

Цей прийом кращий, але ви можете легко захопитися і закінчити щось таке:

конструктор (реквізит) {
  // це погано, справді погано
  this.onClick = this.onClick.bind (це);
  this.onChange = this.onChange.bind (це);
  this.onSubmit = this.onSubmit.bind (це);
  this.increaseCount = this.increaseCount.bind (це);
  this.decreaseCount = this.decreaseCount.bind (це);
  this.resetCount = this.resetCount.bind (це);
  ...
}

Оскільки ми використовуємо Babel і підтримуємо властивості класу, кращим способом було б використовувати функції стрілок при визначенні методів класу:

клас MyComponent розширює компонент {
  ...
  onClick = () => {
    // 'це' збережено
    console.log (this.state);
  }
  render () {
    повернути (
      
        

{this.state.count}

        <кнопка onClick = {this.onClick}> Клацніть на мене       
    );   } }

Знищити об’єкт реквізиту

Коли компонент має багато реквізиту, знищити реквізитний об'єкт, розмістивши кожну властивість у своєму власному рядку.

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

експорт за замовчуванням MyComponent ({
  ім'я,
  прізвище,
  адреса електронної пошти,
  опис,
  onChange,
  onSubmit,
}) {
  повернути (
    
      

{firstName}

      ...     
  ); }

Аргументи за замовчуванням не є приводом для скидання defaultProps. Як було сказано раніше, ви завжди повинні визначати propTypes та defaultProps.

Для компонентів на основі класу:

клас MyComponent розширює компонент {
  ...
  render () {
    const {
      ім'я,
      прізвище,
      адреса електронної пошти,
      опис,
      onChange,
      onSubmit,
    } = this.props;
    повернути (
      
        

{firstName}

        ...       
    );   } }

Це чистіше, полегшує повторне замовлення властивостей та полегшує додавання / видалення властивостей до / зі списку, створюючи читабельний розріз для Git. Розглянемо наступне:

Різниця легше читається, коли кожна властивість знаходиться у новому рядку

У правій частині ви можете легко вказати, яку властивість було додано, однак на лівій частині ви знаєте лише, що щось змінилося в цій лінії, і вам потрібно дуже уважно дивитися, щоб визначити, яка частина рядка була змінився.

Умовна візуалізація

Коли вам потрібно надати один з двох компонентів або блоків коду JSX на основі умови, використовуйте потрійний вираз:

isLoggedIn
  ? 
Ласкаво просимо, {ім'я користувача}!
  : <кнопка onClick = {this.login}> Вхід

Якщо код складається з декількох рядків, використовуйте круглі дужки:

isLoggedIn? (
  
    Ласкаво просимо, {usename}!   
): (   <кнопка onClick = {this.login}>     Вхід    )

Якщо вам потрібно вивести один компонент або блок коду JSX на основі умови, замість цього використовуйте оцінку короткого замикання:

isComplete && 
Ви закінчили!

Для більш ніж одного рядка використовуйте круглі дужки:

isComplete && (
  
    Ви закінчили!   
)

Ключовий атрибут

Це звичайна модель використання Array.prototype.map та подібних методів масиву всередині функції render (), і про атрибут ключа можна легко забути. Примирення досить важке, не ускладнюйте. Завжди пам'ятайте, щоб розмістити ключ там, де він належить. (каламбур призначений знову, зрозумієте?)

render () {
  повернути (
    
    {       {         items.map (item => (           
  •             {назва виробу}           
  •         ))        }     
  ); }

Коли ви використовуєте map (), filter () або подібні методи масиву, другим параметром зворотного виклику є індекс елемента. Як правило, погано використовувати цей індекс як ключовий. React використовує атрибут ключа, щоб визначити, які елементи були змінені, додані чи видалені, а значення ключа повинно бути стабільним і однозначно ідентифікувати кожен елемент.

У випадках, коли масив сортується або елемент додається до початку масиву, індекс буде змінено, навіть якщо елемент, що представляє цей індекс, може бути однаковим. Це призводить до непотрібних візуалізацій, а в деяких випадках - до неправильних даних.

Використовуйте UUID або ShortID, щоб створити унікальний ідентифікатор для кожного елемента при його першому створенні та використовувати його як ключове значення.

Нормалізуйте державу

Постарайтеся нормалізувати державний об’єкт і зберегти його максимально рівним. Введення даних у стан означає, що для їх оновлення потрібна більш складна логіка. Уявіть, як потворно було б оновити глибоко вкладене поле - зачекайте, не уявляйте тут:

// об'єкт держави
state = {
  ...
  дописи: [
    ...,
    {
      мета: {
        id: 12,
        автор: '...',
        загальнодоступний: помилковий,
        ...
      },
      ...
    },
    ...
  ],
  ...
};
// оновити 'public' до 'true'
this.setState ({
  ... це. держава,
  дописи: [
    ... this.state.posts.slice (0, індекс),
    {
      ... this.state.posts [індекс],
      мета: {
        ... this.state.posts [індекс] .meta,
        громадськість: правда,
      }
    },
    ... this.state.posts.slice (індекс + 1),
  ]
});

Цей код не тільки некрасивий, але він також може змусити непов'язані компоненти повторно відображатись, навіть якщо дані, які вони відображають, насправді не змінилися. Це тому, що ми оновили всіх предків у державі дерева новими посиланнями на об’єкти.

Оновлення того ж поля було б простіше, якщо ми переструктуруємо дерево стану:

state = {
  дописи: [
    10,
    11,
    12,
  ],
  мета: {
    10: {
      id: 10,
      автор: "автор-а",
      загальнодоступний: помилковий,
    },
    11: {
      id: 11,
      автор: "автор-б",
      загальнодоступний: помилковий,
    },
    12: {
      id: 12,
      автор: 'author-c',
      загальнодоступний: помилковий,
    },
  },
}
this.setState ({
  мета: {
    ... this.state.meta,
    [id]: {
      ... this.state.meta [id],
      громадськість: правда,
    }
  }
})

Використовуйте назви класів

Якщо ви коли-небудь стикалися з ситуацією, коли вам потрібно використовувати умовне ім'я класу, ви, можливо, написали щось таке:

  ...

Це стає дуже некрасивим, якщо у вас є кілька умовних імен класів. Замість використання терміналу використовуйте пакет імен класів:

імпортувати назви класів із 'назви класів';
const class = імена класів ('tab', {'is-active': isActive});
  ...

Один компонент на файл

Це не складне правило, але я вважаю за краще зберігати кожен компонент в одному файлі та експортувати цей компонент за замовчуванням. У мене немає конкретних причин для цього, але я просто вважаю це більш чистим та організованим - висока п'ять, якщо ви теж це зробите!

Висновок

Існує не один правильний спосіб розробити компонент React. Ми переглянули декілька найкращих практик та моделей, які допоможуть вам розробити багаторазові та ремонтопридатні компоненти, і мені дуже цікаво дізнатися, які схеми та практики ви віддаєте перевагу та чому.

У наступній частині цієї серії ми збираємося створити просту програму To-Do та застосувати деякі з цих кращих практик та моделей.

Чи була ця стаття корисною? Будь ласка, натисніть кнопку «Клоп» нижче або слідкуйте за мною для отримання додаткової інформації

Дякуємо за прочитане! Якщо у вас є відгуки, залиште коментар нижче.

Перейдіть до частини 7-б: Створення простого додатка ToDo (незабаром)