Основи Redux

31 хв. читання
30 листопада 2021

Так як веб-додатки стають все більш складними, виникає необхідність оновлення та відображення їх основних даних. Є багато підходів управління цими даними. Проте розробники досі залишаються з непрозорим кодом, який практично неможливо змінити. Ще гірше, розробники можуть знайти помилку в різних, непов'язаних, частинах програми. Використовуйте Redux! Це передбачуваний контейнер станів для JavaScript додатків, який пропонує вирішення вищеназваних проблем.

Redux використовує 3 основні концепції:

  1. Існує єдине джерело істини для всього вашого стану програми.
  2. Цей стан тільки для читання (read-only).
  3. Всі зміни в стан додатку вносяться за допомогою чистих функцій.

Основна концепція

1. Єдине джерело істини

При використанні Redux, основні дані для всього додатку представлені єдиним JavaScript об'єктом з посиланням на стан або дерево станів. Цей об'єкт може бути простим або складним, залежно від вимог вашої програми. Наприклад, станом для простого todo-додатку може бути один масив об'єктів списку завдань.

const state = [
    {
        id: 1,
        task: 'Do laundry',
        completed: true
    },
    {
        id: 2,
        task: 'Paint fence',
        completed: false
    }
];

Станом для сайту соціальних медіа може бути словник, який містить інформацію про статті, повідомлення, дані профілю та інші соціальні дані.

const defaultState = {
    posts: [
        // об'єкти записів для стрічки користувача
    ],
    notifications: [
        // непрочитані сповіщення для користувача
    ],
    messages: [
        // нові повідомлення
    ],
    friends: [
        // інші користувачі онлайн
    ],
    profile: null
}

Незалежно від розміру програми, всі дані стану зберігаються в одному об'єкті. Трохи пізніше ми розглянемо методи управління великими за розміром станами додатків.

2. Стан тільки для читання (read-only)

Рівень представлення ніколи не буде безпосередньо керувати станом вашого додатку. Наприклад, уявіть, що обробник станів у формі додавання дії todo, безпосередньо не буде додавати нову задачу на ваш масив todos. Крім того, обробник буде пропускати action (дію), яка говорить: "Додаток, я хотів би додати «купити молоко» в масив Todos".

Action(Дія) – простий об'єкт JavaScript, що виражає намір змінити стан об'єкту.

Він містить мінімальну інформацію, необхідну для опису того, що повинно змінитися в результаті взаємодії з користувачем. Єдиним обов'язковим атрибутом action є тип, а всі інші дані, включені в action будуть специфічні для вашої програми, а тип дії буде включатися. Коли користувач додає завдання «Купити молоко», то дія «публікування» може виглядати наступним чином:

{
    type: 'ADD_TODO',
    task: 'Buy milk',
    id: 3
}

Основи Redux

3. Зміни за допомогою функцій

Отже, що ж відбувається з "діями", як тільки вони відбуваються в UI? Існує єдина функція, яка працює з цими "діями". Перемикач, стан якого залежить від дії поля type. Кожен тип дії, який може виділятися в вашому додатку потребує обчислення нового стану додатку на основі поточного стану. Функція, яка це виконує повинна бути чистою функцією. Якщо ви не знайомі з чистими функціями, я рекомендую вам подивитися відео Дена Абрамова, творця Redux, який пояснює, що це таке.

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

При передачі А і В завжди результатом в чистій функції буде C. Якщо функція є нечистою, передача А і В може дати C, або ж вони можуть дати інше значення D. Вихід залежить від вхідних даних і більше від нічого іншого. Чисті функції не мають ніяких побічних ефектів, тому їм не потрібен мережевий доступ або запити до баз даних. Крім того, чисті функції не змінюють свої вхідні аргументи. Замість цього вони використовують вхідні дані для обчислення деякого значення, а потім повертають розраховане значення.

Продовжуючи наш todo приклад, case для типу дії 'ADD_TODO' не буде заносити нове значення в масив todos. Це не чиста функція, тому що вона змінює існуючий масив. Замість цього 'ADD_TODO' зробить копію масиву todos, додасть todo до кінця цього нового масиву, а потім повертає новий масив в якості наступного стану програми.

(currentState, action) => {
    switch(action.type){
        case 'ADD_TODO':
            const nextState = [
                ...currentState,
                {
                    id: action.id,
                    task: action.task,
                    completed: false
                }
            ];
            return nextState;
            break;
        default:
            return currentState;
    }
};

Чиста функція, яка знає, як перетворити поточний стан додатку та будь-які дії в оновленому стані додатку називається root reducer – кореневим редуктором. Той факт, що root reductor обчислює наступний стан, а не змінює існуючий стан є дуже важливим в рамках Redux. Використовуючи цю модель, розрахунки стану залишаються швидкими, так як ми можемо просто передати посилання будь-яких незмінених даних в поточному стані до наступного стану. Ми також отримуємо гарантію, що наш стан не зміниться, бо знаємо, що він не може бути змінений за межами ланцюга action -> reductor.

Основи Redux

Найкращі практики

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

Форма стану

Flat-об'єкти(рівні об'єкти)

Підтримка ваших станів «рівними» - відмінний спосіб зменшити складність і зробити розробку, обслуговування та налагодження простішим для себе. В основі лежить така ж ідея, як і в нормалізації таблиць бази даних. Скажімо, ваш список завдань буде редагуватися мільйонами людей, і ви хочете відстежувати, хто створив кожен елемент todo. Можна висвітлити ці дані шляхом додавання об'єкта автора до кожного завдання todo:

const state = [
    {
        id: 1,
        task: 'Do laundry',
        completed: true,
        author: {
            name: 'Billy Bob',
            role: 'Assistant Editor'
        }
    },
    {
        id: 2,
        task: 'Paint fence',
        completed: false,
        author: {
            name: 'Billy Bob',
            role: 'Assistant Editor'
        }
    }
];

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

const state = [
    todos: [
        {
            id: 1,
            task: 'Do laundry',
            completed: true,
            authorId: 1
        },
        {
            id: 2,
            task: 'Paint fence',
            completed: false,
            authorId: 1
        }
    ],
    authorsById: {
        1: {
            id: 1,
            name: 'Billy Bob',
            role: 'Assistant Editor'
        }
    }
];

Ми можемо зробити ще крок далі і створити окремий об'єкт, проіндексований ідентифікатором, для наших todo. Тоді наш список todos може бути представлений простим масивом ідентифікаторів – id.

const state = {
    todos: [1, 2],
    todosById: {
        1: {
            id: 1,
            task: 'Do laundry',
            completed: true,
            authorId: 1
        },
        2: {
            id: 2,
            task: 'Paint fence',
            completed: false,
            authorId: 1
        }
    },
    authorsById: {
        1: {
            id: 1,
            name: 'Billy Bob',
            role: 'Assistant Editor'
        }
    }
};

У цій flat-структурі, є одне місце, яке повинно бути оновлено, коли вносяться зміни в дані, що лежать в основі додатків. Розробники можуть бути впевнені, що їх зміни в одній частині додатка (напр. авторська інформація користувача) не порушить іншу частину програми (напр. порядок списку todo). Також стає легше багатозначно посилатися на ці дані та відображати їх по-різному.

Якщо ви шукаєте спосіб для вирівнюваня відповіді JSON API, аби зберігати стани, ви повинні перевірити бібліотеку Normalizr, яка допомагає налагодити дані в форматі JSON.

Дії

Робіть ваші дії невеликими! Кожна дія має містити тільки мінімальну кількість інформації, необхідної для перетворення стану програми. Наприклад, кожен todo в нашому стані додатку включає в себе логічне значення. Так як ми знаємо, що поле завжди буде хибним для нового todo, нам не потрібно означувати це поле в нашій 'ADD_TODO' дії.

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

Кнопка, яка додає todo до додатка Redux може виглядати наступним чином:

<button onclick="dispatch({ type: 'ADD_TODO', task: 'Walk dog', id: nextTodoId++ })">Add Walk Dog Todo</button>
<script>
    // Redux код тут
    let nextTodoId = 0;
</script>

Примітка: dispatch – це функція Redux-store об'єкту – це те, що ви використовуєте, щоб оголошувати дії протягом вашої програми. Я покажу код, який встановлює ваш Redux-store трохи пізніше.

Логіка додатка буде виглядати приблизно так, якби він був створений за допомогою дій (функція addTodo є творцем дії).

<button onclick="dispatch(addTodo('Walk dog'))">Add Walk Dog Todo</button>

<script>
    // Redux код тут
    let nextTodoId = 0;
    const addTodo = (task) => {
        return {
            type: 'ADD_TODO',
            id: nextTodoId++,
            task
        };
    };
</script>

Зауважте, кнопка додавання todo більше не потребує наступний ідентифікатор для todo. Ця інформація може бути збережена під творцем дії addTodo, яка дає можливість додавати todo об'єкти. Крім того, дія-творець addTodo надає змогу тривіально додати кнопку «Погодувати кота». Запис, який містить addTodo забезпечує хороший список дій, які доступні для нас.

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

Редуктори

У міру зростання вашого додатку, з'явиться функція кореневого редуктора, який обробляє всі типи дій. Для того, щоб йти в ногу з цим зростанням, функція кореневого редуктора вашого застосування може передавати управління різними частинами свого стану дерева на інші, спеціалізовані редуктори. У нашому прикладі todos, кореневий редуктор може передати об'єкт todo до todo редуктора і автора об'єкта до певного автора редуктора. Така процедура називається композицією редукторів (reducer composition). Це допомагає розділяти розробку, так як він чітко відокремлює логіку додатка на частини, з якими можуть працювати різні розробники.

Для збереження простоти прикладу, я просто хочу включити масив авторів і todos у стан. Але, як вже говорилося раніше, було б краще зберігати ці дані в об'єктах проіндексованих id для великих додатків.

На самому початку цього шляху, ми створили редуктор управління для керування нашими todos:

const todos = (currentState = [], action) => {
    switch(action.type){
        case 'ADD_TODO':
            const nextState = [
                ...currentState,
                {
                    id: action.id,
                    task: action.task,
                    completed: false
                }
            ];
            return nextState;
            break;
        default:
            return currentState;
    }
};

Тепер нам потрібно створити редуктор, який керує нашими об'єктами автора. Це може виглядати наступним чином:

const authors = (currentState = [], action) => {
    switch(action.type) {
        case 'ADD_AUTHOR':
            const nextState = [
                ...currentState,
                {
                    id: action.id,
                    name: action.name,
                    role: action.role
                }
            ];
            return nextState;
            break;
        default:
            return currentState;
    }
};

Для того, щоб звести все це разом, ми створюємо кореневий редуктор, який об'єднує об'єкти, керовані цими редукторами в єдиний об'єкт стану!

const todoApp = (currentState = {}, action) => {
    return {
        todos: todos(currentState.todos, action),
        authors: authors(currentState.authors, action),
    }
};

Функція todoApp – наш кореневий редуктор, який забирає гілки todo і автора стану додатку до спеціалізованих редукторів. Цей приклад показує, як редуктор композиції, використовуює цілі об'єкти, але ви можете зробити те ж саме з масивами і їх змістом. Один редуктор знатиме, як додавати і видаляти елементи з массиву, а інший редуктор знатиме, як оновити окремі елементи в масиві. У цьому шаблоні "батьківський" редуктор масиву буде викликати редуктор кожного елементу, коли необхідно додати або змінити один з його елементів.

Редуктор todoApp являє собою чисту функція, яка перетворює поточний стан і дію в наступний стан. Цей редуктор Redux використовується для створення Redux-store вашого додатку. Ось проста html сторінка з 2-ма кнопками, яка використовує творця дій і редуктор композиції, щоб додати авторів і todos до нашого стану програми. Якщо ви збережете це як html і відкриєте його в браузері, ви можете додати todos і авторів, а також подивитися на стан додатку в консолі.




    <meta charset="utf-8">
    <title>Super Simple Redux Example</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script>


    <button onclick="store.dispatch(addTodo('Walk dog')); console.log(store.getState());">Add Walk Dog Todo</button>
    <button onclick="store.dispatch(addAuthor('Billy Bob', 'Assistant Editor')); console.log(store.getState());">Add Billy Bob Author</button>

    <script>
        // Action Creators
        let nextTodoId = 0;
        const addTodo = (task) => {
            return {
                type: 'ADD_TODO',
                id: nextTodoId++,
                task
            };
        };
        let nextAuthorId = 0;
        const addAuthor = (name, role) => {
            return {
                type: 'ADD_AUTHOR',
                id: nextAuthorId++,
                name,
                role,
            };
        };
    </script>
    <script>
        // Reducers
        const todos = (currentState = [], action) => {
            switch(action.type){
                case 'ADD_TODO':
                    const nextState = [
                        ...currentState,
                        {
                            id: action.id,
                            task: action.task,
                            completed: false
                        }
                    ];
                    return nextState;
                    break;
                default:
                    return currentState;
            }
        };
        const authors = (currentState = [], action) => {
            switch(action.type) {
                case 'ADD_AUTHOR':
                    const nextState = [
                        ...currentState,
                        {
                            id: action.id,
                            name: action.name,
                            role: action.role
                        }
                    ];
                    return nextState;
                    break;
                default:
                    return currentState;
            }
        };
        const todoApp = (currentState = {}, action) => {
            return {
                todos: todos(currentState.todos, action),
                authors: authors(currentState.authors, action),
            }
        };
    </script>
    <script>
        // Redux setup
        const { createStore } = Redux;
        const store = createStore(todoApp);
    </script>


Нагадаємо короткий огляд кращих практик в Redux:

  1. Тримайте об'єкт стану рівним, тобто flat.
  2. Передавайте настільки мало даних, наскільки це можливо в ваших діях.
  3. Використовуйте творців дій для відправки дій замість збірки.
  4. Кореневий редуктор повинен складатися з більш дрібних редукторів, які керують певними частинами стану додатку.

Зберігайте ці поради у голові, коли ви починаєте проектування і створення вашого Redux-додатку.

Тестування

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

Творці дій

Коли ми тестуємо творців дій нашого застосування, ми хочемо переконатися, що створюється правильна дія. Це досить просте завдання, так як наші творці дії повертають прості JavaScript об'єкти. Ось тест для нашого addTodo творця дій:

const taskText = 'Walk dog';
const expectedAction = {
    type: 'ADD_TODO',
    task: taskText,
    id: 0
};
expect(addTodo(taskText)).toEqual(expectedAction);

Редуктори

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

const initialState = {
    todos: [],
    authors: []
};
expect(todoApp(undefined, {})).toEqual(initialState);

А ось простий тест, щоб переконатися, що наш автор Біллі Боб правильно доданий в стан програми:

const initialState = {
    todos: [],
    authors: []
};
const newAuthor = {
    name: 'Billy Bob',
    role: 'Assistant Editor',
    id: 0
};
const addAuthorAction = {
    type: 'ADD_AUTHOR',
    name: newAuthor.name,
    role: newAuthor.role,
    id: newAuthor.id
};
expect(todoApp(initialState, addAuthorAction)).toEqual({
    todos: [],
    authors: [ newAuthor ]
});

Редуктори є чистими функціями

Найголовніше в редукторах це те, що вони є чистими функціями. Таким чином, на додаток до перевірки, що кінцевий стан об'єкта містить очікувані дані, ми повинні також гарантувати, що наші редуктори НЕ змінюють об'єкт стану. Ми можемо перевірити це, викликаючи заморожування наших об'єктів, перш ніж передати їх нашим редукторами. Таким чином, якщо ми спробуємо змінити стан, наші тести будуть давати нам знати це. Deep freeze - хороша бібліотека для виклику Object.freeze() в JavaScript рекурсивно на нашому стані об'єкта.

const initialState = {
    todos: [],
    authors: []
};
const newAuthor = {
    name: 'Billy Bob',
    role: 'Assistant Editor',
    id: 0
};
const addAuthorAction = {
    type: 'ADD_AUTHOR',
    name: newAuthor.name,
    role: newAuthor.role,
    id: newAuthor.id
};
deepFreeze(initialState);
expect(todoApp(initialState, addAuthorAction)).toEqual({
    todos: [],
    authors: [ newAuthor ]
});

Підсумки

Про Redux в двох словах. Це прекрасне рішення для управління станами веб-додатків. Єдине, доступне тільки для читання, з чистими редукторами і простими в тестуванні компонентами, безумовно, підвищить довіру і продуктивність будь-якого розробника додатків на JavaScript. І, так як він не прив'язаний до певного виду механізму (хоча він часто використовується з React), то Ви або Ваша команда може легко підключити Redux в існуючий стек розвитку, якщо стан програми управління був невирішеною точкою на минулих проектах.

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

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

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

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