Иммутабельность и чистые функции

Основы иммутабельности

Иммутабельность — это принцип, согласно которому данные не изменяются после создания. Вместо модификации существующих объектов создаются новые копии с внесёнными изменениями. В контексте Preact это имеет критическое значение, поскольку фреймворк использует виртуальный DOM и диффинг для эффективного обновления интерфейса. Изменяемые объекты могут приводить к непредсказуемым ререндерингам и багам, тогда как иммутабельные данные делают поведение компонентов предсказуемым.

Ключевые моменты иммутабельности:

  • Состояние компонента не модифицируется напрямую.
  • Любые изменения состояния создают новый объект или массив.
  • Сравнение старого и нового состояния может выполняться по ссылке, что ускоряет алгоритмы диффинга.

Пример иммутабельного обновления массива в Preact:

import { h } from 'preact';
import { useState } from 'preact/hooks';

function TodoList() {
    const [todos, setTodos] = useState(['Купить хлеб', 'Сделать зарядку']);

    const addTodo = (todo) => {
        // Создание нового массива вместо модификации существующего
        setTodos([...todos, todo]);
    };

    return (
        <div>
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>{todo}</li>
                ))}
            </ul>
            <button onCl ick={() => addTodo('Новая задача')}>Добавить</button>
        </div>
    );
}

Здесь массив todos не изменяется напрямую, создаётся новая копия с добавленной задачей.


Чистые функции

Чистая функция — это функция, которая для одинаковых аргументов всегда возвращает одинаковый результат и не имеет побочных эффектов. В Preact чистые функции особенно важны для компонентов и утилит, используемых при работе с состоянием.

Свойства чистых функций:

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

Пример чистой функции для обновления состояния:

function toggleItem(items, id) {
    return items.map(item =>
        item.id === id ? { ...item, completed: !item.completed } : item
    );
}

// Использование в компоненте
const [items, setItems] = useState([
    { id: 1, completed: false, title: 'Сделать уроки' }
]);

const handleToggle = (id) => {
    setItems(toggleItem(items, id));
};

Функция toggleItem не изменяет исходный массив, а возвращает новый. Это упрощает работу Preact с виртуальным DOM, так как изменения легко отслеживать через ссылочные сравнения.


Взаимосвязь иммутабельности и чистых функций

Иммутабельность и чистые функции тесно связаны. Чистая функция облегчает создание иммутабельных структур данных, так как результат функции всегда новый объект без изменения исходного. Это повышает производительность Preact:

  • Оптимизация ререндеринга: Preact сравнивает старый и новый виртуальный DOM. Если объекты иммутабельны, можно быстро определить, какие части интерфейса изменились.
  • Упрощение отладки: Отсутствие побочных эффектов и неизменяемость состояния позволяют легко отслеживать источник ошибок.
  • Согласованность данных: Любая функция, работающая с состоянием, предсказуемо создаёт новое состояние, исключая неожиданное поведение.

Использование иммутабельности в хуках

Хуки Preact (useState, useReducer) естественным образом сочетаются с иммутабельными подходами. useReducer особенно подходит для сложных состояний:

import { h } from 'preact';
import { useReducer } from 'preact/hooks';

function reducer(state, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return [...state, { id: Date.now(), text: action.text, done: false }];
        case 'TOGGLE_TODO':
            return state.map(todo =>
                todo.id === action.id ? { ...todo, done: !todo.done } : todo
            );
        default:
            return state;
    }
}

function TodoApp() {
    const [todos, dispatch] = useReducer(reducer, []);

    return (
        <div>
            <button onCl ick={() => dispatch({ type: 'ADD_TODO', text: 'Новая задача' })}>
                Добавить задачу
            </button>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id} onCl ick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}>
                        {todo.text} {todo.done ? '(выполнено)' : ''}
                    </li>
                ))}
            </ul>
        </div>
    );
}

Каждое действие возвращает новый массив задач, не мутируя предыдущий. Это обеспечивает надёжность и предсказуемость состояния приложения.


Рекомендации по практическому применению

  • Всегда использовать spread-операторы или функции map, filter, reduce для создания новых массивов.
  • Для объектов применять Object.assign или spread { ...obj } для создания копий с изменениями.
  • Компоненты должны быть максимально чистыми, минимизируя побочные эффекты.
  • Сложные состояния лучше управлять через useReducer с чистыми редьюсерами, что упрощает тестирование и поддержку кода.
  • При работе с Preact и Virtual DOM предпочтение иммутабельности повышает производительность и упрощает оптимизацию ререндеринга.

Иммутабельность и чистые функции в Preact создают основу для устойчивого, предсказуемого и производительного кода. Они обеспечивают логическую целостность состояния, сокращают количество ошибок и делают приложение удобным для масштабирования и тестирования.