useSelector не работает, сокращение в реакции

Когда я использую useSelector, переменная всегда сохраняет свое начальное состояние. Такое ощущение, что он хранится в какой-то параллельной галактике и никогда не обновляется. Но когда я получаю значение с помощью const store = useStore (); store.getState () ... он дает правильное значение (но не имеет подписок). Когда я проверяю хранилище в redux devtools, я вижу, что все значения записаны в хранилище правильно. Значения просто не извлекаются из магазина с помощью useSelector.

Я хотел получить некоторый кеш для профилей пользователей, то есть не получать / api / profile / 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-toolkit в 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;

использовать-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 - посмотрите на шаблон использования выше, кроме опубликованных строк ничего интересного нет.

So

Когда я экспортирую состояние загрузки (я имею в виду последние строки в use-profile.js; я могу подавить последние три строки и раскомментировать остальные три). Итак, если я использую getLoadingStateFromStoreGetter (значения, полученные через store.getState () ...), тогда некоторые имена профилей будут отображать имена, которые были получены, а некоторые сохраняют загрузку ... и застревают навсегда. Это имеет смысл. Правильные данные извлекаются из хранилища redux, и у нас нет подписок.

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

Я прочитал много ответов на StackOverflow. Вот некоторые распространенные ошибки:

  • Некоторые говорят, что ваш компонент не обновляется. Это не тот случай, я протестировал его, поместив console.log в код и добавив переменную versionControl (см. В коде), чтобы убедиться, что он обновляется.

  • В некоторых ответах говорится, что вы не обновляете хранилище с помощью редукторов правильно, и он по-прежнему содержит тот же объект. Это не тот случай, я попробовал оба подхода, чтобы вернуть свежий новый объект {... state, entity: {... state.entities ... etc ...}} и изменить существующий прокси-объект - в обоих направлениях редукторы должны предоставлять новый объект, а редукторы должны уведомлять об изменениях.

  • Иногда создается несколько экземпляров магазина, и все происходит в беспорядке. Это определенно не так, у меня есть единственный вызов configureStore () и один компонент.

  • Также я не вижу нарушения правил хуков в своем коде. У меня есть оператор if внутри useSelector fn, но сам хук useSelector вызывается безоговорочно.

Я понятия не имею, по каким еще причинам useSelect просто не работает. Может ли кто-нибудь помочь мне понять?


person Andrey Putilov    schedule 20.06.2021    source источник
comment
Это странно. Я не сразу вижу вашу ошибку. Реквизиты для исследования распространенных проблем перед публикацией. Мутация состояния - частая причина такого рода вещей, но для вас это не проблема, потому что вы используете redux-toolkit.   -  person Linda Paiste    schedule 20.06.2021
comment
Опс, понятно, опечатка. В любом случае, большое спасибо, что посмотрели на это. Добавил ответ, но пока не могу его принять, ТАК заставляет меня ждать еще два дня, пока я не смогу принять свой ответ.   -  person Andrey Putilov    schedule 20.06.2021


Ответы (1)


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

useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' )

Должно быть не .loadingState, а .state. Вот и все.

person Andrey Putilov    schedule 20.06.2021