Следует ли использовать `useSelector`/`useDispatch` вместо `mapStateToProps`?
Проблема с использованием useSelector и mapStateToProps в React
Когда я создаю приложение на React и использую хук useSelector
, мне нужно соблюдать правила вызова хуков (вызывать его только на верхнем уровне функционального компонента). Если же я использую mapStateToProps
, я получаю состояние в пропсах и могу использовать его где угодно без каких-либо проблем. Та же ситуация и с useDispatch
.
Каковы преимущества использования хуков, помимо экономии строчек кода по сравнению с mapStateToProps
?
5 ответ(ов)
Вам, кажется, не совсем понятно, что такое "топ-уровень". Это просто означает, что в функциональном компоненте 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
, вот несколько пунктов:
- Вы экономите время, не записывая функцию
connect
каждый раз, когда вам нужно получить доступ к хранилищу, и удаляя её, когда необходимость пропадает. Нет необходимости в бесконечных обёртках в React DevTools. - Вы четко различаете и избегаете конфликтов между пропсами, приходящими из
connect
, пропсами, приходящими от родительских компонентов и пропсами, внедряемыми обёртками из сторонних библиотек. - Иногда вы или ваши коллеги выбираете нечеткие имена для пропсов в
mapStateToProps
, и вам придется прокрутить доmapStateToProps
, чтобы выяснить, какой селектор используется для этой конкретной пропсы. С хуками такого нет, так как селекторы и переменные, возвращающие данные, находятся на одной строке. - Используя хуки, вы получаете общие преимущества хуков, самое главное из которых — возможность объединять и повторно использовать связанную логику состояния в нескольких компонентах.
- С
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);
Возвращаемое состояние Redux из хуки useSelector
можно передавать куда угодно, так же как и в случае с mapStateToProps
. Например, его можно передать другой функции. Единственное ограничение заключается в том, что при объявлении хуки необходимо следовать определённым правилам:
- Хука должна быть объявлена только внутри функционального компонента.
- При объявлении она не может находиться внутри условного блока. Пример кода ниже:
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
может быть эффективно использован и в других контекстах, соблюдая при этом правила его использования.
Смотрите Редактирование 2 в конце для окончательного ответа
Поскольку никто не знает, как на это ответить, кажется, что лучший ответ заключается в том, что вы НЕ ДОЛЖНЫ использовать useSelector
, когда вам нужна информация в других местах, кроме корневого уровня вашего компонента. Поскольку вы не можете знать, как компонент изменится в будущем, просто не используйте useSelector
вообще.
Если у кого-то есть лучший ответ, чем этот, я изменю принятый ответ.
Редактирование: Некоторые ответы были добавлены, но они лишь подчеркивают, почему не следует использовать useSelector
вообще, до тех пор пока не изменятся правила хуков, и вы не сможете использовать его в коллбеках. С учетом сказанного, если вы не хотите использовать его в коллбеке, это может быть хорошим решением для вас.
EDIT 2: Был добавлен ответ с примерами всего того, что я хотел, и показано, как useSelector
и useDispatch
легче использовать.
В ответ на ваш вопрос, вы можете использовать значение, возвращаемое из 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
и др.) используется фреймворком для заполнения хранимых данных при каждом рендере.
Однако значения из хуков можно использовать в любом месте вашего компонента. Хотя я сомневаюсь, что это полностью ответит на ваш вопрос, колбеки часто возникают в обсуждениях, и примеры не всегда публикуются.
Это не ответ на вопрос, но этот хуку может быть очень полезным, если вы хотите отделить логику mapDispatchToProps
при сохранении простоты и удобства разработки, которые предлагают хуки:
https://gist.github.com/ErAz7/1bffea05743440d6d7559afc9ed12ddc
Я не упоминаю аналогичный подход для mapStateToProps
, потому что useSelector
сам по себе уже лучше отделяет логику работы со стором по сравнению с mapStateToProps
, поэтому я не вижу в нем никаких преимуществ. Конечно, я не имею в виду использование useSelector
напрямую, а предлагаю создать обертку вокруг него в ваших файлах стора (например, в файле редьюсера) и импортировать оттуда, вот так:
// например, userReducer.js
export const useUserProfile = () => useSelector(state => state.user.profile)
Метод set в useState не отражает изменения немедленно
Как реализовать дебаунс?
Обнаружена ошибка: Невозможное нарушение: Неверный тип элемента: ожидался строковый тип (для встроенных компонентов) или класс/функция, но получен объект
Добавление тега script в React/JSX
React + Redux — Замедление работы onChange в Input при вводе, когда значение берется из состояния