0

Следует ли использовать `useSelector`/`useDispatch` вместо `mapStateToProps`?

15

Проблема с использованием useSelector и mapStateToProps в React

Когда я создаю приложение на React и использую хук useSelector, мне нужно соблюдать правила вызова хуков (вызывать его только на верхнем уровне функционального компонента). Если же я использую mapStateToProps, я получаю состояние в пропсах и могу использовать его где угодно без каких-либо проблем. Та же ситуация и с useDispatch.

Каковы преимущества использования хуков, помимо экономии строчек кода по сравнению с mapStateToProps?

5 ответ(ов)

0

Вам, кажется, не совсем понятно, что такое "топ-уровень". Это просто означает, что в функциональном компоненте useSelector() не может быть использован внутри циклов, условий или вложенных функций. Это не имеет никакого отношения к корневому компоненту или структуре компонентов.

Вот пример неправильного использования:

// плохой вариант
const MyComponent = () => {
  if (condition) {
    // так делать нельзя
    const data = useSelector(mySelector);
    console.log(data);
  }

  return null;
}

А вот правильный вариант:

// хороший вариант
const MyComponent = () => {
  const data = useSelector(mySelector);

  if (condition) {
    console.log(data); // используем data в условии
  }

  return null;
}

Кроме того, mapStateToProps находится на уровне выше, чем вызов хука.

Правила хуков делают использование этого конкретного хука довольно сложным. Тем не менее, вам редко нужно получать изменяемое значение внутри коллбека. Я не вспоминаю, когда в последний раз мне это понадобилось. Обычно, если вашему коллбеку нужно актуальное состояние, лучше просто отправить действие, а обработчик этого действия (например, redux-thunk, redux-saga, redux-observable и т.д.) сам получит актуальное состояние.

Это специфично для хуков в целом (не только для useSelector), и есть множество способов обойти это, если вам это действительно нужно. Например:

const MyComponent = () => {
  const data = useSelector(mySelector);
  const latestData = useRef();
  latestData.current = data;

  return (
    <button
      onClick={() => {
        setTimeout(() => {
          console.log(latestData.current); // всегда ссылается на актуальные данные
        }, 5000);
      }}
    />
  );
}

Что касается преимуществ использования хуков по сравнению с mapStateToProps, вот несколько пунктов:

  1. Вы экономите время, не записывая функцию connect каждый раз, когда вам нужно получить доступ к хранилищу, и удаляя её, когда необходимость пропадает. Нет необходимости в бесконечных обёртках в React DevTools.
  2. Вы четко различаете и избегаете конфликтов между пропсами, приходящими из connect, пропсами, приходящими от родительских компонентов и пропсами, внедряемыми обёртками из сторонних библиотек.
  3. Иногда вы или ваши коллеги выбираете нечеткие имена для пропсов в mapStateToProps, и вам придется прокрутить до mapStateToProps, чтобы выяснить, какой селектор используется для этой конкретной пропсы. С хуками такого нет, так как селекторы и переменные, возвращающие данные, находятся на одной строке.
  4. Используя хуки, вы получаете общие преимущества хуков, самое главное из которых — возможность объединять и повторно использовать связанную логику состояния в нескольких компонентах.
  5. С mapStateToProps обычно нужно иметь дело и с mapDispatchToProps, что ещё более громоздко и легче потеряться в этом, особенно при чтении чужого кода (объектная форма? функциональная форма? bindActionCreators?). Проп, приходящий от mapDispatchToProps, может иметь такое же имя, как и его создатель действия, но с другой сигнатурой, так как он был переопределен в mapDispatchToProps. Если вы используете один создатель действия в нескольких компонентах и затем переименовываете его, эти компоненты продолжат использовать старое имя, приходящее из пропсов. Объектная форма легко ломается, если у вас есть цикл зависимостей, и также нужно иметь дело с затенением имен переменных.

Вот пример, который демонстрирует затенение переменной:

import { getUsers } from 'actions/user';

class MyComponent extends Component {
  render() {
    // затененная переменная getUsers, теперь вам либо нужно переименовать её
    // либо вызывать так: this.props.getUsers
    // либо менять импорт на звездочку, и ни один из вариантов не является хорошим
    const { getUsers } = this.props;
    // ...
  }
}

const mapDispatchToProps = {
  getUsers,
};

export default connect(null, mapDispatchToProps)(MyComponent);
0

Возвращаемое состояние Redux из хуки useSelector можно передавать куда угодно, так же как и в случае с mapStateToProps. Например, его можно передать другой функции. Единственное ограничение заключается в том, что при объявлении хуки необходимо следовать определённым правилам:

  1. Хука должна быть объявлена только внутри функционального компонента.
  2. При объявлении она не может находиться внутри условного блока. Пример кода ниже:
function test(displayText) {
   return (<div>{displayText}</div>);
}

export function App(props) {
    const displayReady = useSelector(state => {
        return state.readyFlag;
    });

    const displayText = useSelector(state => {
        return state.displayText;
    });

    if(displayReady) {
        return (
            <div>
                Outer
                {test(displayText)}
            </div>
        );
    } else {
        return null;
    }
}

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

export default function CustomPaginationActionsTable(props) {
    // Читаем состояние с помощью useSelector.
    const searchCriteria = useSelector(state => {
        return state && state.selectedFacets;
    });

    // Используем прочитанное состояние в обратном вызове, вызываемом из хуки useEffect.
    useEffect(() => {
        const postParams = constructParticipantListQueryParams(searchCriteria);
        const options = {
            headers: {
                'Content-Type': 'application/json'
            },
            validateStatus: () => true
        };
        var request = axios.post(PORTAL_SEARCH_LIST_ALL_PARTICIPANTS_URI, postParams, options)
            .then(function(response) { 
                if (response.status === HTTP_STATUS_CODE_SUCCESS) {
                    console.log('Доступ к выводам хуки useSelector в обратном вызове axios. Печатаем: ' + JSON.stringify(searchCriteria));
                }
            })          
            .catch(function(error) {
                // Обработка ошибок
            });
    }, []);
}

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

0

Смотрите Редактирование 2 в конце для окончательного ответа

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

Если у кого-то есть лучший ответ, чем этот, я изменю принятый ответ.

Редактирование: Некоторые ответы были добавлены, но они лишь подчеркивают, почему не следует использовать useSelector вообще, до тех пор пока не изменятся правила хуков, и вы не сможете использовать его в коллбеках. С учетом сказанного, если вы не хотите использовать его в коллбеке, это может быть хорошим решением для вас.

EDIT 2: Был добавлен ответ с примерами всего того, что я хотел, и показано, как useSelector и useDispatch легче использовать.

0

В ответ на ваш вопрос, вы можете использовать значение, возвращаемое из useSelector, так же, как и значение из useState, при создании колбеков. Приведем следующий пример:

const ExampleComponent = () => {
    // Используем хук для получения данных из состояния redux.
    const stateData = useSelector(state => state.data);

    // Получаем функцию dispatch для работы с redux.
    // Это позволяет отправлять действия в store.
    const dispatch = useDispatch();

    // Создаем колбек без мемоизации, который использует stateData.
    // Эта функция пересоздается при каждом рендере, и изменение
    // state.data в redux приведет к повторному рендеру.
    const callbackWithoutMemo = (event) => {
        // Используем значения из состояния.
        if (stateData.condition) {
            doSomething();
        } else {
            doSomethingElse();
        }

        // Отправляем действие в store
        // можем передать данные, если это нужно.
        dispatch(someActionCreator());
    };

    // Создаем мемоизированный колбек, используя stateData.
    // Эта функция пересоздается только когда изменяется значение в
    // массиве зависимостей (сравнение ссылок).
    const callbackWithMemo = useCallback((event) => {
        // Используем значения из состояния.
        if (stateData.condition) {
            doSomething();
        } else {
            doSomethingElse();
        }

        // Отправляем действие в store
        // можем передать данные, если это нужно.
        dispatch(someActionCreator());
    }, [stateData, doSomething, doSomethingElse]);

    // Используем колбеки.
    return (
        <>
            <div onClick={callbackWithoutMemo}>
                Нажми на меня
            </div>
            <div onClick={callbackWithMemo}>
                Нажми на меня
            </div>
        </>
    )
};

Как упомянул Макс в своем ответе, правило хуков подразумевает, что хуки должны использоваться на корневом уровне вашего компонента, что означает, что вы не можете использовать их в динамических или условных конструкциях. Это важно, поскольку порядок вызовов базовых хуков (внутренние хуки React: useState и др.) используется фреймворком для заполнения хранимых данных при каждом рендере.

Однако значения из хуков можно использовать в любом месте вашего компонента. Хотя я сомневаюсь, что это полностью ответит на ваш вопрос, колбеки часто возникают в обсуждениях, и примеры не всегда публикуются.

0

Это не ответ на вопрос, но этот хуку может быть очень полезным, если вы хотите отделить логику mapDispatchToProps при сохранении простоты и удобства разработки, которые предлагают хуки:

https://gist.github.com/ErAz7/1bffea05743440d6d7559afc9ed12ddc

Я не упоминаю аналогичный подход для mapStateToProps, потому что useSelector сам по себе уже лучше отделяет логику работы со стором по сравнению с mapStateToProps, поэтому я не вижу в нем никаких преимуществ. Конечно, я не имею в виду использование useSelector напрямую, а предлагаю создать обертку вокруг него в ваших файлах стора (например, в файле редьюсера) и импортировать оттуда, вот так:

// например, userReducer.js
export const useUserProfile = () => useSelector(state => state.user.profile)
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь