Уроки, які я засвоїв, використовуючи React-Redux

10 хв. читання

Коли я вперше почав використовувати React з Redux, я, зазвичай, застосовував великий приєднаний компонент контейнера, що встановлював необхідний стан, який наслідувався усіма дочірніми компонентами. З часом застосунок ускладнювався, а його компоненти збільшувались, що призводило до різноманітних проблем у коді.

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

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

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

Приклад такого випадку:

mockup

let UserProfileContainer = ({ userName }) => (
   <Grid>
      <NavBar userName={userName} />
       {/* ...інші компоненти інтерфейсу профілю... */}
    </Grid>
);
const mapStateToProps = state => ({
   userName: `${state.user.firstName} ${state.user.lastName}`,
    //інші значущі властивості...
});
UserProfileContainer = connect(mapStateToProps)(UserProfileContainer);
const NavBar = ({ userName }) => (
   <nav>
      {/* ...елементи навігації */}
        <UserDropdown userName={userName} />
    </nav>
);
const UserDropdown = ({ userName }) => (
   <NavDropdown title={userName}>
      <MenuItem>Settings</MenuItem>
        <MenuItem>Logout</MenuItem>
    </NavDropdown>
);

Як можна побачити, NavBar та UserDropdown знають про властивість userName, хоча лише компонент UserDropdown дійсно використовує ці дані.

Таким чином, наш код стає складним у налагодженні та виникає ризик появи небажаних багів. Скажімо, я хочу змінити назву властивості userName, тому мені необхідно впровадити зміни у всіх пов'язаних компонентах: UserProfileContainer, NavBar та UserDropdown. Тобто три місця, де необхідно застосувати зміни, замість одного компонента, що потребує цих даних.

А якщо існують інші місця у застосунку, де необхідно відобразити ім'я користувача? Для цього необхідно буде приєднати компонент та парсити ім'я зі state, що призведе до великої кількості повторюваного коду. Я зможу повторно використати цю логіку з невеликим помічником, але все ж необхідно буде виконати connect() іншого компоненту.

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

Наведений приклад є дуже спрощеним. Зазвичай, існує безліч властивостей, що передаються трьом-чотирьом дочірнім компонентам, тому що ми використовуємо окремі компоненти контейнеру, що пов'язані зі state.

Чи існує кращий спосіб?

На щастя, я не єдиний, хто зіткнувся з такими проблемами в React-Redux. Я використовую Redux-form для впровадження логіки та UI форм. Якось у них був реліз з масштабними змінами. Дещо з документації:

У v5 лише зовнішній компонент форми був приєднаний до Redux state, а властивості для кожного поля передавалися через компонент форм. Проблема полягає у тому, що увесь компонент форми має перезавантажуватись з кожним натисканням, яке змінює значення форми. Така поведінка прийнятна для невеликих форм авторизації, але призводить до надзвичайно низької продуктивності у більших формах з десятками або сотнями полів.
** У v6 кожне окреме поле приєднано до Redux store.** Зовнішні компоненти форми також приєднані, але таким чином, що не відбувається їх перезавантаження з кожною зміною значення.

Ми зрозуміли, що більшість наших проблем можна вирішити шляхом розбиття великих контейнерів на невеликі виділені компоненти. Якщо повернутися до прикладу, ми можемо створити приєднаний компонент userName:

// наш компонент очікує children у вигляді функції
const UserName = ({ children, userName }) => (
   children({ userName })    
);
const mapStateToProps = state => ({
   userName: `${state.user.firstName} ${state.user.lastName}`
});
export default connect(mapStateToProps)(UserName);
// тому що ми досі хочемо, щоб UserName повертав рядок у більшості випадків
// коли ми створюємо для цього компонент
export const UserNameText = () => (
   <UserName>
      {({ userName }) => (
         <React.Fragment>
            {userName}    
            </React.Fragment>
        )}
    </UserName>
);
Зауважте: NavDropdown::title prop у нашому прикладі може приймати лише рядок, що не є компонентом React. Таким чином, наша реалізація стає набагато гнучкішою з добре відомою концепцією renderProp.
Також зауважте: Якщо ви використовуєте версію React, нижче за 16, вам необхідно огорнути текст елементом <span/>😦. Якщо ви погано знайомі з React.Fragment, ознайомтеся з документацією.

Тож тепер можемо застосувати будь-де у застосунку:

const UserDropdown = () => (
   <UserName>
      {({ userName }) => (
         <NavDropdown title={userName}>
            <MenuItem>Settings</MenuItem>
               <MenuItem>Logout</MenuItem>
            </NavDropdown>
        )}
    </UserName>
);
// або в іншій функції рендеринга десь у моєму коді
...
<H1>Hello <UserNameText />!</H1>
...

Це досить прямолінійний та швидкий спосіб зекономити код. Як можна побачити, ми впровадили компонент UserNameText, що поводиться як наш компонент UserName.

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

Отримати ім'я користувача зі state тепер набагато простіше. Необхідно лише імпортувати наш компонент UserName та відобразити його де ми хочемо.

Тепер лише компонент UserName відображається, коли відповідний стан Redux зазнає змін.

Оптимізація connect()

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

Розбиття на маленькі приєднані компоненти означає, що на сторінці їх буде відображатися більше, а це означає, що функції mapStateToProps викликатимуться частіше. Слухачі React-Redux відстежують кожну зміну на кожну надіслану дію, незалежно від поверненого значення функцією mapStateToProps. Таким чином, ви не повинні реалізовувати якийсь складний парсинг у цій функції.

Функція mapStateToProps викликається з кожною збереженою зміною, що є поведінкою за замовчуванням у бібліотеці React-Redux. На щастя, ми можемо контролювати таку поведінку, шляхом перевизначення areStatesEqual з метою ігнорування зміни станів, окрім поточного.

...
const mapStateToProps = state => ({
   userName: `${state.user.firstName} ${state.user.lastName}`
});
const areStatesEqual = (next, prev) => (
   prev.user === next.user
);
export default connect(mapStateToProps, null, null , {
   areStatesEqual
})(UserName);

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

Іншим важливим пунктом у розумінні впровадження React-Redux приєднаних компонентів є те, що бібліотека має свою власну оптимізацію при оновленні та візуалізації компонента. Життєвий цикл shouldComponentUpdate реалізовується таким чином, що він поверхнево порівнює результат функції mapStateToProps. Принцип роботи багато у чому нагадує React's PureComponent, з різницею у тому, що PureComponents порівнює усі властивості компонента.

Маючи це на увазі, ми намагатимемось уникнути створення нових об'єктів у функції mapStateToProps.

// ПОГАНО. Завжди створюються непотрібні нові об'єкти, що призводить до  
//втрати react-redux оптимізації 
const mapStateToProps = state => ({
   userData: {
      fullName: `${state.user.firstName} ${state.user.lastName}`,
        jobTitle: state.user.jobTitle,
    },
    ...
});

// Натомість ми повинні намагатися залишити властивості поверхневими
const mapStateToProps = state => ({
   userFullName:`${state.user.firstName} ${state.user.lastName}`,
    userJobTitle: state.user.jobTitle,
    ...
});

Як ви можете побачити, дуже легко змусити наш компонент ненавмисно відображатися при кожній зміні стану, хоча в більшості випадків стан є незмінним. Іноді складно уникнути створення нових об'єктів чи масивів у вашій функції. Особливо, коли необхідно відобразити масив об'єктів, відфільтрованих зі state:

// Array.filter створює новий масив при кожному виклику mapStateToProps
const mapStateToProps = state => {
   const activeTodos = state.todos.filter(todo => todo.isActive);
    return {
       activeTodos
    }
};
// Кращим рішенням буде пройтися усіма to-dos та провести фільтрацію у 
// функції рендерингу

const mapStateToProps = state => ({
   todos: state.todos
});
...
render() {
   const activeTodos = this.props.todos.filter(
      todo => todo.isActive
    );

    return (
      <div>
         {activeTodos.map(todo => <TodoItem {...todo} />)}
        </div>
    )
}

Таким чином, на кожну зміну стану, незалежно від того, пов'язаний він з to-dos чи ні, ми отримуємо компонент, що буде відображатися лише коли новий масив to-dos створено у state. Раніше ж рендеринг викликався з кожною зміною state.

Ми навели лише простий сценарій. Іноді у вас не має іншого вибору, як створити новий масив у функції mapStateToProps. У таких випадках ви можете просто реалізувати shouldComponentUpdate самостійно або використовувати memoized селектори з reselect.

Інший спосіб: передати масиву ідентифікаторів, а потім зробити кожен <TodoItem/> приєднаним компонентом, що приймає ідентифікатор to-do як властивість. Маленький компонент TodoItem знатиме, що потрібно обрати to-do зі стану та, якщо mapStateToProps впроваджено коректно, виклик рендерингу станеться лише коли відповідний to-do змінено. Ви можете звернутись до наступного Tree View прикладу з Redux репозиторію.

Висновок

Як було виявлено, досить легко зіткнутися з проблемою продуктивності, використовуючи React з Redux у середніх та великих застосунках.

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

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

Codeguida 8.4K
Приєднався: 3 місяці тому

Hosting Ukraine

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація