#reactjs #redux #react-hooks #redux-toolkit
Вопрос:
Когда я использую useSelector, переменная всегда сохраняет свое начальное состояние. У меня такое чувство, что он хранится в какой-то параллельной галактике и никогда не обновляется. Но когда я получаю значение с помощью const store = useStore(); store.getState()… оно дает правильное значение (но не содержит подписок). Когда я осматриваю магазин в redux devtools, я вижу, что все значения записаны в магазине правильно. Значения просто не извлекаются из хранилища с помощью useSelector.
Чего я хотел добиться, так это иметь некоторый кэш для профилей пользователей, т. Е. Не извлекать /api/профиль/25 несколько раз на одной и той же странице. Я не хочу думать об этом как о «кэшировании» и делать несколько запросов, просто имея в виду, что запросы кэшируются и дешевы, но скорее думая об этом как о получении профилей из магазина и имея в виду, что профили извлекаются при необходимости, я имею в виду какое-то ленивое обновление.
Реализация должна выглядеть как крюк, т. е.
// use pattern
const client = useProfile(userId);
// I can also put console.log here to see if the component is getting updated
let outputProfileName;
if( client.state==='pending' ) {
outputProfileName = 'loading...';
} else if( client.state==='succeeded' ) {
outputProfileName = <span>{client.data.name}</span>
} // ... etc
поэтому я поместил свой код в use-profile.js, имея срез redux-инструментария в profile-slice.js
profile-slice.js
import {
createSlice,
//createAsyncThunk,
} from '@reduxjs/toolkit';
const entityInitialValue = {
data: undefined,
state: 'idle',
error: null
};
export const slice = createSlice({
name: 'profile',
initialState: {entities:{}},
reducers: {
updateData: (state,action) => {
// we received data, update the data and the status to 'succeeded'
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: action.payload.data,
state: 'succeeded',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchPendStart: (state,action) => {
// no data - indicates we started fetching
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: null,
state: 'pending',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchError: (state,action) => {
state.entities[action.payload.id] = {
//...entityInitialValue,
...state.entities[action.payload.id],
data: null,
state: 'failed',
error: action.payload.error
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
},
extraReducers: {
}
});
export const {updateData,dispatchPendStart,dispatchError} = slice.actions;
// export const selectProfile... not used
export default slice.reducer;
use-profile.js
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector, useStore } from 'react-redux';
import {
updateData as actionUpdateData,
dispatchPendStart as actionDispatchPendStart,
dispatchError as actionDispatchError,
} from './profile-slice';
//import api...
function useProfile(userId) {
const dispatch = useDispatch();
const actionFunction = async () => {
const response = await client.get(`... api endpoint`);
return response;
};
const store = useStore();
// versionControl is a dummy variable added for testing to make sure the component is updated;
// it is updated: I tried adding console.log to my component function (where I have const client = useProfile(clientId)...)
const [versionControl,setVersionControl] = useState(0);
const updateVersion = () => setVersionControl(versionControl 1);
// TODO: useSelector not working
const updateData = newVal => { dispatch(actionUpdateData({id:userId,data:newVal})); updateVersion(); };
const dispatchPendStart = newVal => { dispatch(actionDispatchPendStart({id:userId})); updateVersion(); };
const dispatchError = newVal => { dispatch(actionDispatchError({id:userId,error:newVal})); updateVersion(); };
const [
getDataFromStoreGetter,
getLoadingStateFromStoreGetter,
getLoadingErrorFromStoreGetter,
] = [
() => (store.getState().profile.entities[userId]||{}).data,
() => (store.getState().profile.entities[userId]||{}).state,
() => (store.getState().profile.entities[userId]||{}).error,
];
const [
dataFromUseSelector,
loadingStateFromUseSelector,
loadingErrorFromUseSelector,
] = [
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].data : undefined ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingError : undefined ),
];
useEffect( async () => {
if( !(['pending','succeeded','failed'].includes(getLoadingStateFromStoreGetter())) ) {
// if(requestOverflowCounter>100) { // TODO: protect against infinite loop of calls
dispatchPendStart();
try {
const result = await actionFunction();
updateData(result);
} catch(e) {
dispatchError(e);
throw e;
}
}
})
return {
versionControl, // "versionControl" is an approach to force component to update;
// it is updating, I added console.log to the component function and it runs, but the values
// from useSelector are the same all the time, never updated; the problem is somewhere else; useSelector is just not working
// get data() { return getDataFromStoreGetter(); }, // TODO: useSelector not working; but I need subscribtions
// get loadingState() { return getLoadingStateFromStoreGetter(); },
// get loadingError() { return getLoadingErrorFromStoreGetter(); },
data: dataFromUseSelector,
loadingState: loadingStateFromUseSelector,
loadingError: loadingErrorFromUseSelector,
};
}
export default useProfile;
store.js
import { configureStore,combineReducers } from '@reduxjs/toolkit';
import profileReducer from '../features/profile/profile-slice';
// import other reducers
export default configureStore({
reducer: {
profile: profileReducer,
// ... other reducers
},
});
component.js — actually see the use pattern above, there’s nothing interesting besides the lines posted.
So
Когда я экспортирую состояние загрузки (я имею в виду последние строки в use-profile.js; Я могу подавить последние три строки и раскомментировать остальные три). Итак, если я использую getLoadingStateFromStoreGetter (значения, полученные через store.getState ()…), то некоторые имена профилей отображают имена, которые были извлечены, а некоторые удерживают «загрузка…» и застряли навсегда. В этом есть смысл. Правильные данные извлекаются из магазина redux, и у нас нет подписок.
Когда я экспортирую другую версию, созданную с помощью useSelector, я всегда получаю ее начальное состояние. Я никогда не получаю имя пользователя или значение, указывающее «загрузка».
Я прочитал много ответов на StackOverflow. Некоторые распространенные ошибки включают:
- Некоторые говорят, что ваш компонент не обновляется. Это не тот случай, я протестировал его на консоли.войдите в код и добавьте переменную VersionControl (см. В коде), чтобы убедиться, что она обновляется.
- Некоторые ответы говорят о том, что вы неправильно обновляете хранилище с помощью редукторов, и оно по-прежнему содержит тот же объект. Это не так, я попробовал оба подхода, чтобы вернуть новый новый объект {…состояние,сущности:{…состояние.сущности…и т. Д.}} и изменить существующий прокси — объект-в обоих случаях мои редукторы должны предоставлять новый объект, а redux должен уведомлять об изменениях.
- Иногда создается несколько экземпляров магазина, и все перепутывается. Это определенно не так, у меня есть один вызов configureStore() и один компонент.
- Также я не вижу нарушения правил крючка в своем коде. У меня есть оператор if внутри useSelector fn, но сам крючок useSelector вызывается безоговорочно.
Я понятия не имею, какие еще причины заставляют выбор использования просто не работать. Кто-нибудь может помочь мне понять?
Комментарии:
1. Это очень странно. Я не сразу вижу твою ошибку. Подробные сведения об исследовании общих проблем перед публикацией. Мутация состояния является распространенной причиной такого рода вещей, но для вас это не проблема, потому что вы используете redux-инструментарий.
2. Опс, я вижу, здесь опечатка. В любом случае, большое вам спасибо, что посмотрели на это. Добавил ответ, но пока не могу его принять, поэтому заставляет меня ждать еще два дня, пока я не смогу принять свой ответ.
Ответ №1:
Ops, как обычно, причина в очень простой опечатке. Так много потраченных часов. Очень жаль тех, кто потратил время, пытаясь взглянуть на это, и спасибо за ваше время.
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' )
Там должно быть не .loadingState, а .state. Это оно.