React и Redux вместе. Часть 3 из 7

02.09.2022

Теги: FrontendHookJavaScriptReact.jsWeb-разработкаСостояниеСписокТеорияФункция

Будем создавать приложение списка задач — почти такое же, как в первой части — но уже с использованием Redux. Есть несколько вариантов использования Redux в React-приложении. Пакет redux позволяет работать с классическим Redux. Пакет @reduxjs/toolkit позволяет работать с Redux по-новому. Пакет react-redux обеспечивает взаимодействие компонентов с хранилищем. В третьей части будем использовать классический Redux и два хука из пакета react-redux.

1. Классический Redux

Есть два подхода связать компоненты React и хранилище Redux — использовать функцию connect (старый, не рекомендуется) или использовать хуки (новый, рекомендуется). В любом случае, надо обернуть компонент верхнего уровня в <Provider>, чтобы все его потомки могли получить доступ к хранилищу.

import { Provider } from 'react-redux';
import { store } from './redux/store.js';

function App() {
    return (
        <Provider store={store}>
            <h1>Список задач</h1>
            <TodoList />
        </Provider>
    );
}

1.2. Пакет react-redux, используем хуки

Как это работает

Hooks API является заменой HOC connect. Хук useSelector предоставляет компоненту доступ к хранилищу, а хук useDispatch возвращает функцию store.dispatch для возможности отправки экшенов.

Хук useSelector

Хук useSelector приблизительно эквивалентен mapStateToProps. В качестве параметра useSelector принимает функцию-селектор, которая извлекет из хранилища какие-то данные. Функция-селектор получает на вход state и будет вызываться при рендере компонента или при изменении состояния хранилища.

export function TodoList(props) {
    const items = useSelector((state) => state.todo);

    return (
        <div className="todo-list">
            {items.length > 0 ? (
                items.map(item => <TodoItem key={item.id} id={item.id} />)
            ) : (
                <p>Список задач пустой</p>
            )}
        </div>
    );
}
Функция-селектор должна быть чистой, поскольку потенциально она может выполняться несколько раз и в произвольные моменты времени.

Хук useSelector может вернуть любое значение, а не только объект. При отправке экшена useSelector выполнит сравнение предыдущего и текущего значений, полученных от функции-селектора. Если они отличаются — это приведет к принудительному рендеру компонента. Если они совпадают — повторного рендера не будет. По умолчанию useSelector использует строгую проверку === равенства.

Результат выполнения mapStateToProps сравнивается с предыдущим результатом с использованием функции shallowEqual. То есть содержимое объекта будет сравниваться поле за полем по ссылке или по значению, и если будет найдено различие, то компонент будет перерисован.

Хук useSelector выполняет сравнение предыдущего и текущего значений с помощью строгого равенства ===. Если функция-селектор возвращает объект, сравнение может вернуть false, хотя по значениям полей объекты идентичные. Это можно изменить с помощью второго аргумента useSelector.

import { shallowEqual, useSelector } from 'react-redux';

const selectedData = useSelector(selectorReturningObject, shallowEqual);

Функцию-селектор лучше вынести за пределы компонента, чтобы при каждом новом рендере не создавать ее заново. В документации сказано, что хук может вернуть кэшированный результат без повторного запуска селектора, если это та же ссылка на функцию, что и при предыдущем рендере компонента.

const todos = (state) => state.todo;

export function TodoList(props) {
    const items = useSelector(todos);

    return (
        <div className="todo-list">
            {items.length > 0 ? (
                items.map(item => <TodoItem key={item.id} id={item.id} />)
            ) : (
                <p>Список задач пустой</p>
            )}
        </div>
    );
}
Хук useDispatch

Хук useDispatch приблизительно эквивалентен mapDispatchToProps — и просто возвращает функцию store.dispatch.

export function TodoItem(props) {
    // извлекаем их хранилища один элемент списка задач по id
    const todo = useSelector((state) => state.todo.find((item) => item.id === props.id));
    const { id, title, completed } = todo;

    // создаем две функции для отправки экшенов в хранилище
    const dispatch = useDispatch();
    const toggle = () => dispatch(todoToggle(id));
    const remove = () => dispatch(todoRemove(id));

    return (
        <div className="todo-item">
            <span>
                <input type="checkbox" checked={completed} onChange={toggle} />
                <span>{title}</span>
            </span>
            <span className="remove" onClick={remove}>&times;</span>
        </div>
    );
}

Простое приложение

Нам потребуется развернуть React приложение с помощью create-react-app + установить два пакета — redux и react-redux.

$ cd react-redux/react-react-hooks
$ npx create-react-app .
$ npm install redux react-redux --save-prod
$ npm install uuid --save-prod # доп.пакет
Создаем хранилище

Для начала нам нужно хранилище, для этого создаем директорию src/redux, внутри нее — все необходимое для работы Redux. Содержимое директории src/redux будет как и в предыдущей версии приложения, так что просто копируем из react-redux-connect.

Создаем компоненты

Создаем директорию src/component, внутри нее — компоненты TodoList.js, TodoItem.js, TodoForm.js, StatusBar.js и Login.js.

Файл компонента src/App.js:

import './App.css';

import { TodoList } from './component/TodoList.js';
import { TodoForm } from './component/TodoForm.js';
import { StatusBar } from './component/StatusBar.js';
import { Login } from './component/Login.js';
import { Provider } from 'react-redux';
import { store } from './redux/store.js';

function App() {
    return (
        <Provider store={store}>
            <div className="App">
                <h1>Список задач</h1>
                <TodoForm />
                <StatusBar />
                <TodoList />
                <Login />
            </div>
        </Provider>
    );
}

export default App;

Файл компонента src/component/TodoList.js:

import { useSelector } from 'react-redux';
import { TodoItem } from './TodoItem.js';

export function TodoList(props) {
    const items = useSelector((state) => state.todo);

    return (
        <div className="todo-list">
            {items.length > 0 ? (
                items.map(item => <TodoItem key={item.id} id={item.id} />)
            ) : (
                <p>Список задач пустой</p>
            )}
        </div>
    );
}

Файл компонента src/component/TodoItem.js:

import { useDispatch, useSelector } from 'react-redux';
import { actions } from '../redux/actions.js';

export function TodoItem(props) {
    // извлекаем их хранилища один элемент списка задач по id
    const todo = useSelector((state) => state.todo.find((item) => item.id === props.id));
    const { id, title, completed } = todo;

    // создаем две функции для отправки экшенов в хранилище
    const dispatch = useDispatch();
    const toggle = () => dispatch(actions.todo.toggle(id));
    const remove = () => dispatch(actions.todo.remove(id));

    return (
        <div className="todo-item">
            <span>
                <input type="checkbox" checked={completed} onChange={toggle} />
                &nbsp;
                <span>{title}</span>
            </span>
            <span className="remove" onClick={remove}>
                &times;
            </span>
        </div>
    );
}

Файл компонента src/component/TodoForm.js:

import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { actions } from '../redux/actions.js';
import { v4 as uuid } from 'uuid';

export function TodoForm(props) {
    const [text, setText] = useState('');
    const dispatch = useDispatch();

    const handleChange = (event) => {
        setText(event.target.value);
    };

    const handleClick = () => {
        if (text.trim().length !== 0) {
            const data = {
                id: uuid(),
                title: text,
                completed: false,
            };
            dispatch(actions.todo.create(data));
        }
        setText('');
    };

    return (
        <div className="todo-form">
            <input type="text" value={text} onChange={handleChange} placeholder="Новая задача" />
            <button onClick={handleClick}>Добавить</button>
        </div>
    );
}

Файл компонента src/component/StatusBar.js:

import { useSelector } from 'react-redux';

export function StatusBar(props) {
    const items = useSelector((state) => state.todo);

    const total = items.length;
    const completed = items.filter((item) => item.completed).length;
    const uncompleted = items.filter((item) => !item.completed).length;

    return (
        <div className="status-bar">
            Всего задач {total}, не завершенных {uncompleted}, завершенных {completed}.
        </div>
    );
}

Файл компонента src/component/Login.js:

import { useSelector, useDispatch } from 'react-redux';
import { actions } from '../redux/actions.js';

export function Login(props) {
    // извлекаем их хранилища данные по авторизации (да,нет)
    const auth = useSelector((state) => state.user.auth);

    // создаем две функции для отправки экшенов в хранилище
    const dispatch = useDispatch();
    const login = () => dispatch(actions.user.login());
    const logout = () => dispatch(actions.user.logout());

    return (
        <div className="user-login">
            {auth ? (
                <>
                    <span>Пользователь авторизован</span>
                    &nbsp;
                    <button onClick={logout}>Выйти</button>
                </>
            ) : (
                <>
                    <span>Пользователь не авторизован</span>
                    &nbsp;
                    <button onClick={login}>Войти</button>
                </>
            )}
        </div>
    );
}

Исходные коды здесь, директория react-redux-hooks.

$ cd react-redux/react-redux-hooks
$ npm install

Оптимизация

Первый этап

Сейчас у нас анонимные функции-селекторы разбросаны по всем компонентам. К тому же эти функции часто дублируются. Давайте соберем их все в одном месте.

Файл src/redux/selectors.js:

import {
    all,
    allLength,
    completed,
    completedLength,
    uncompleted,
    uncompletedLength,
    findById,
} from './todoSelectors.js';
import { auth } from './userSelectors.js';

export const selectors = {
    todo: {
        all: all,
        allLength: allLength,
        completed: completed,
        completedLength: completedLength,
        uncompleted: uncompleted,
        uncompletedLength: uncompletedLength,
        findById: findById,
    },
    user: {
        auth: auth,
    },
};

Файл src/redux/todoSelectors.js:

export const all = (state) => state.todo; // массив всех задач
export const allLength = (state) => all(state).length; // количество всех задач
export const completed = (state) => all(state).filter((item) => item.completed); // завершенные задачи
export const completedLength = (state) => completed(state).length; // количество завершенных
export const uncompleted = (state) => all(state).filter((item) => !item.completed); // не завершенные задачи
export const uncompletedLength = (state) => uncompleted(state).length; // количество не завершенных
export const findById = (state, id) => all(state).find((item) => item.id === id);

Файл src/redux/userSelectors.js:

export const auth = (state) => state.user.auth;

Теперь дублирования нет и изменять логику извлечения данных можно в одном месте. Компоненты не содержат логики извлечения данных, потому как им это и не нужно. И есть приятный побочный эффект — useSelector будет всегда получать одну и ту же ссылку на функцию.

import { useSelector } from 'react-redux';
import { selectors } from '../redux/selectors.js';

export function StatusBar(props) {
    const total = useSelector(selectors.todo.allLength);
    const completed = useSelector(selectors.todo.completedLength);
    const uncompleted = useSelector(selectors.todo.uncompletedLength);

    return (
        <div className="status-bar">
            Всего задач {total}, не завершенных {uncompleted}, завершенных {completed}.
        </div>
    );
}
import { useSelector, useDispatch } from 'react-redux';
import { selectors } from '../redux/selectors.js'

export function Auth(props) {
    // извлекаем их хранилища данные по авторизации
    const auth = useSelector(selectors.user.auth);

    return (
        <div className="user-auth">
            {auth ? (
                <span>Пользователь авторизован</span>
            ) : (
                <span>Пользователь не авторизован</span>
            )}
        </div>
    );
}

Исходные коды здесь, директория react-redux-hooks-one.

$ cd react-redux/react-redux-hooks-one
$ npm install
Второй этап

Еще один момент, на который хотелось бы обратить внимание. Мы уже знаем, что для сравнения текущего и предыдыщего значения хук useSelector использует строгое равенство. Посмотрим, как мы могли бы оптимизировать наше приложение, используя функцию shallowEqual. Для этого добавим в компонент TodoList.js возможность фильтрации задач.

import { useSelector } from 'react-redux';
import { selectors } from '../redux/selectors.js';
import { TodoItem } from './TodoItem.js';

export function TodoList(props) {
    console.log('Component TodoList render');
    let selector = selectors.todo.all;
    if (props.completed === true) selector = selectors.todo.completed;
    if (props.completed === false) selector = selectors.todo.uncompleted;
    const items = useSelector(selector);

    return (
        <div className="todo-list">
            {items.length > 0 ? (
                items.map(item => <TodoItem key={item.id} id={item.id} />)
            ) : (
                <p>Список задач пустой</p>
            )}
        </div>
    );
}
import './App.css';

import { TodoList } from './component/TodoList.js';
import { TodoForm } from './component/TodoForm.js';
import { StatusBar } from './component/StatusBar.js';
import { Login } from './component/Login.js';
import { Provider } from 'react-redux';
import { store } from './redux/store.js';


function App() {
    return (
        <Provider store={store}>
            <div className="App">
                <h1>Список задач</h1>
                <TodoForm />
                <StatusBar />
                <TodoList completed={true} />
                <TodoList completed={false} />
                <Login />
            </div>
        </Provider>
    );
}

export default App;

Функции-селекторы completed и uncompleted всегда возвращают новый массив. Когда мы будем кликать по кнопке «Войти/Выйти» — это будет вызывать новый рендер компонента TodoList.js без всякой на то необходимости. Хотя массивы одинаковые — ссылки на них разные.

Component TodoList render
Component TodoList render

Давайте это исправим — используем функцию shallowEqual для сравнения массивов (в javascript массив — это тоже объект).

import { useSelector, shallowEqual } from 'react-redux';
import { selectors } from '../redux/selectors.js';
import { TodoItem } from './TodoItem.js';

export function TodoList(props) {
    console.log('Component TodoList render');
    let selector = selectors.todo.all;
    if (props.completed === true) selector = selectors.todo.completed;
    if (props.completed === false) selector = selectors.todo.uncompleted;
    const items = useSelector(selector, shallowEqual);

    return (
        <div className="todo-list">
            {items.length > 0 ? (
                items.map((item) => <TodoItem key={item.id} id={item.id} />)
            ) : (
                <p>Список задач пустой</p>
            )}
        </div>
    );
}

Исходные коды здесь, директория react-redux-hooks-two.

$ cd react-redux/react-redux-hooks-two
$ npm install
Третий этап

Когда мы кликаем на checkbox — изменяется массив state.todo. Вызываются все функции-селекторы, которые мы передаем в useSelector. Хук useSelector в компоненте TodoList обнаруживает изменение массива и запускает рендер. Рендер компонента TodoList вызывает рендер всех дочерних компонентов TodoItem. Хотя на самом деле, нам нужен рендер только одного TodoItem — того, по которому кликнули.

Но мы можем избежать рендера TodoList, если функция-селектор будет возвращать не массив элементов списка задач, а массив идентификаторов задач. При клике на checkbox — изменяется статус задачи, но не ее идентификатор. Массив идентификаторов задач остается без изменений, пока не будет добавлена новая задача или удалена существующая.

export const all = (state) => state.todo; // массив всех задач
export const allLength = (state) => all(state).length; // количество всех задач
export const completed = (state) =>  all(state).filter((item) => item.completed); //завершенные задачи
export const completedLength = (state) => completed(state).length; // кол-во завершенных задач
export const uncompleted = (state) => all(state).filter((item) => !item.completed); // не завершенные
export const uncompletedLength = (state) =>uncompleted(state).length; // кол-во не завершенных задач
export const findById = (state, id) => all(state).find((item) => item.id === id);
export const ids = (state) => all(state).map((item) => item.id); // NEW массив идентификаторов задач
import {
    all,
    allLength,
    completed,
    completedLength,
    uncompleted,
    uncompletedLength,
    findById,
    ids,
} from './todoSelectors.js';
import { auth } from './userSelectors.js';

export const selectors = {
    todo: {
        all: all,
        allLength: allLength,
        completed: completed,
        completedLength: completedLength,
        uncompleted: uncompleted,
        uncompletedLength: uncompletedLength,
        findById: findById,
        ids: ids,
    },
    user: {
        auth: auth,
    },
};
import { useSelector, shallowEqual } from 'react-redux';
import { selectors } from '../redux/selectors.js';
import { TodoItem } from './TodoItem.js';

export function TodoList(props) {
    console.log('Component TodoList render');
    const ids = useSelector(selectors.todo.ids, shallowEqual);

    return (
        <div className="todo-list">
            {ids.length > 0 ? (
                ids.map((id) => <TodoItem key={id} id={id} />)
            ) : (
                <p>Список задач пустой</p>
            )}
        </div>
    );
}
export function TodoItem(props) {
    console.log('Component TodoItem render, id =', props.id);
    /* .......... */
}

Как было раньше и как стало сейчас:

Component TodoList render
Component TodoItem render, id = 1
Component TodoItem render, id = 2
Component TodoItem render, id = 3
Component TodoItem render, id = 4
Component TodoItem render, id = 5
Component TodoItem render, id = 3

У одной задачи изменился статус — один компонент был отрисован заново.

Исходные коды здесь, директория react-redux-hooks-three.

$ cd react-redux/react-redux-hooks-three
$ npm install
Четвертый этап

В Redux нельзя подписаться на изменение конкретного кусочка данных. Изначально, можно лишь узнать о том, что «где-то что-то изменилось». Поэтому, при любом изменении в store, будут вызваны все функции-селекторы, которые были переданы в активные useSelector. Давайте в этом убедимся.

export const all = (state) => { // массив всех задач
    console.log('Call selector function all');
    return state.todo;
}
export const allLength = (state) => all(state).length; // количество всех задач
export const completed = (state) => all(state).filter((item) => item.completed); // завершенные задачи
export const completedLength = (state) => completed(state).length; // кол-во завершенных задач
export const uncompleted = (state) => all(state).filter((item) => !item.completed); // не завершенные задачи
export const uncompletedLength = (state) => uncompleted(state).length; // кол-во не завершенных задач
export const findById = (state, id) => all(state).find((item) => item.id === id);
export const ids = (state) => all(state).map((item) => item.id);

Кликаем по кнопке «Войти» или «Выйти» компонента Login.js — получаем много сообщений в консоли разработчика.

Call selector function all
Call selector function all
Call selector function all
..........

Функция-селектор all вызывается много раз при одном клике. Один раз — из компонента TodoList.js через функцию-селектор ids. Три раза — из компонента StatusBar.js через completed и uncompleted. Пять раз — из компонента TodoItem.js через findById.

Хорошо, если функция-селектор простая. А если нужны сложные вычисления или фильтрация/сортировка большого массива?

function ExampleComponent() {
    const data = useSelector((state) => {
        const initialData = state.data;
        // фильтрация, сортировка и преобразование при каждом экшене
        const filteredData = expensiveFiltering(initialData);
        const sortedData = expensiveSorting(filteredData);
        const transformedData = expensiveTransforming(sortedData);
        return transformedData;
    });
    /* .......... */
}

Эту проблему решают мемоизированные селекторы, так что устанавливаем пакет reselect:

$ npm install reselect --save-prod
const inputSelectorOne = (state) => state.one;
const inputSelectorTwo = (state, id) => state.two[id];
const resultFunction = (one, two) => one + two;

// Результаты выполнения первых двух функций-аргументов (входные селекторы) подаются на вход третьей
// функции-аргумента. Если результаты выполнения входных селекторов будут такими же, как в прошлый
// раз — то третья функция (результирующая) не вызывается, а результат возвращается из кэша. Входных
// селекторов может быть один,два,три — тогда результирующая ф-ция принимает один,два,три аргумента.
const awesomeSelector = createSelector(inputSelectorOne, inputSelectorTwo, resultFunction);

Как reselect понимает, когда отдавать данные из кеша:

  • Если параметры входных селекторов inputSelectorOne и inputSelectorTwo не изменились (по shallowEqual) — возвратить данные из кэша. В нашем примере, если state и id не изменились, то reselect вернет предыдущий результат resultFunction.
  • Если не изменились результаты выполнения inputSelectorOne и inputSelectorTwo (по shallowEqual) — возвратить данные из кэша. В нашем примере, если state всё-таки изменился, то reselect вызовет inputSelectorOne и inputSelectorTwo. Если они вернут неизмененные данные, то reselect вернет предыдущий результат resultFunction.

Давайте заменим наши селекторы на мемоизированные версии с использованием функции createSelector.

import { createSelector } from 'reselect';

export const all = (state) => state.todo; // массив всех задач
export const allLength = createSelector( // количество всех задач
    all,
    (items) => items.length
);
export const completed = createSelector( // массив завершенных задач
    all,
    (items) => items.filter((item) => item.completed)
);
export const completedLength = createSelector( // количество завершенных задач
    completed,
    (items) => items.length
);
export const uncompleted = createSelector( // массив не завершенных задач
    all,
    (items) => items.filter((item) => !item.completed)
);
export const uncompletedLength = createSelector( // количество не завершенных задач
    uncompleted,
    (items) => items.length
);
export const findById = (state, id) => all(state).find((item) => item.id === id);
export const ids = createSelector(
    all,
    (items) => items.map((item) => item.id)
);

Давайте проверим, что это работает. Добавим тяжелые вычисления и будем кликать по кнопке «Войти/Выйти» и по checkbox-ам отдельных задач.

import { createSelector } from 'reselect';

function sleep(ms) {
    const nowDate = Date.now();
    let curDate;
    do {
        curDate = Date.now();
    } while (curDate - nowDate < ms);
}

export const all = (state) => state.todo; // массив всех задач
export const allLength = createSelector( // количество всех задач
    all,
    (items) => items.length
);
export const completed = createSelector( // массив завершенных задач
    all,
    (items) => {
        console.log('Тяжелые вычисления, функция completed');
        sleep(1000);
        return items.filter((item) => item.completed);
    }
);
export const completedLength = createSelector( // количество завершенных задач
    completed,
    (items) => {
        console.log('Тяжелые вычисления, функция completedLength');
        sleep(1000);
        return items.length;
    }
);
export const uncompleted = createSelector( // массив не завершенных задач
    all,
    (items) => {
        console.log('Тяжелые вычисления, функция uncompleted');
        sleep(1000);
        return items.filter((item) => !item.completed);
    }
);
export const uncompletedLength = createSelector( // количество не завершенных задач
    uncompleted,
    (items) => {
        console.log('Тяжелые вычисления, функция uncompletedLength');
        sleep(1000);
        return items.length;
    }
);
export const findById = (state, id) => all(state).find((item) => item.id === id);
export const ids = createSelector(
    all,
    (items) => items.map((item) => item.id)
);

При клике на кнопку — никаких сообщений в консоли нет, приложение работает быстро. Это потому, что второй аргумент функци createSelector не вызывается, а результат reselect возвращает из кэша. С другой стороны, при клике по checkbox — приложение замирает и в консоли появляются сообщения.

Тяжелые вычисления, функция completed
Задержка одна секунда...
Тяжелые вычисления, функция completedLength
Задержка одна секунда...
Тяжелые вычисления, функция uncompleted
Задержка одна секунда...
Тяжелые вычисления, функция uncompletedLength
Задержка одна секунда...

Исходные коды здесь, директория react-redux-hooks-four.

$ cd react-redux/react-redux-hooks-four
$ npm install
Пятый этап

Мы мемоизоровали почти все функции-селекторы, за исключением findById — и это не случайно. При нажатии кнопку «Войти/Выйти» — изменяется состояние и вызываются все функции-селекторы, переданные в активные useSelector. Если у нас список из пяти задач, то будет пять вызовов функции-селектора в компоненте TodoItem с разными id. Но reselect может хранить в кэше только одно значение — и мы будем постоянно перезаписывать кэш.

Для решения этой проблемы установим пакет re-reselect:

$ npm install re-reselect --save-prod
// одно значение в кэше
const getData = createSelector(
    state => state.data,
    (state, id) => id,
    (data, id) => expensiveComputation(data, id)
);
// много значений в кэше
const getData = createCachedSelector(
    state => state.data,
    (state, id) => id,
    (data, id) => expensiveComputation(data, id)
)(
    (data, id) => id // use id as cacheKey
);

Как это работает? Вызвать функцию (data, id) => id, чтобы получить ключ cacheKey. Получить из кэша функцию-селектор, созданную ранее с помощью createSelector, по ключу cacheKey. Если в кэше еще нет функции-селектора — создать с помощью createSelector и поместить в кэш. Наконец, вызвать полученную из кэша или созданную функцию-селектор с предоставленными аргументами.

Простой вариант реализации функции createCachedSelector:

import { createSelector } from 'reselect';

const cache = {};

export const createCachedSelector = (...funcs) => {
    return (keySelector) => {
        const selector = (...args) => {
            const cacheKey = keySelector(...args);
            let cacheResponse = cache[cacheKey];
            if (cacheResponse === undefined) {
                cacheResponse = createSelector(...funcs);
                cache[cacheKey] = cacheResponse;
            }
            return cacheResponse(...args);
        };
        return selector;
    };
};

Теперь доработаем наше приложение и убедимся, что при клике на кнопку «Войти/Выйти» значения функций-селекторов возвращаются из кэша.

// проверяем, что есть сообщения в консоли при нажатии кнопки «Войти/Выйти»
export const findById = (state, id) => {
    console.log('Поиск элемента списка задач, id =', id);
    sleep(500);
    return all(state).find((item) => item.id === id);
};
// проверяем, что нет сообщений в консоли при нажатии кнопки «Войти/Выйти»
export const findById = createCachedSelector(
    all,
    (state, id) => id,
    (items, id) => {
        console.log('Поиск элемента списка задач, id =', id);
        sleep(500);
        return items.find((item) => item.id === id);
    }
)(
    (state, id) => id
);
// окончательный вариант функции, позволяющий кэшировать множество значений
export const findById = createCachedSelector(
    all,
    (state, id) => id,
    (items, id) => items.find((item) => item.id === id)
)(
    (state, id) => id
);

Исходные коды здесь, директория react-redux-hooks-five.

$ cd react-redux/react-redux-hooks-five
$ npm install

Поиск: Frontend • Hook • JavaScript • React.js • Web-разработка • Состояние • Список • Теория • Функция • useSelector • useDispatch

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.