slevin
slevin

Reputation: 3896

useSelector updates only the second time, using Redux toolkit

I have the following slice in my store.

//status : idle, loading, succeeded, failed
const initialState = {
    info:'a',
    status:'idle',
    error:null
}

//to get data from server 
export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', async (originalData) => {
    console.log('fetchFeatureInfoById originalData ', originalData);
    const response = await axios.get(FEATURE_URL+'/feature/'+originalData.id+'/'+originalData.category)
    console.log('DATA from server ', response);
    return response.data
})

//slice with reducers 
export const featuresSlice = createSlice({
    name: 'features',
    initialState,
    reducers: { 
        featureInfoById:{
            reducer(state, action){
                //do something with the id 
                console.log('feature state ', ' -- state : ',state.status, ' -- action : ', action);
            },
            prepare(category, id){
                return{
                    payload:{
                        category,
                        id
                    }
                }
            }
        }
    },
    extraReducers(builder){
        builder
        .addCase(fetchFeatureInfoById.pending, (state, action) => {
            state.status = 'loading'
        })
        .addCase(fetchFeatureInfoById.rejected, (state, action) => {
            state.status = 'failed';
            return state;
            // state.error = action.error.message;
            console.log('rejected data store ', state, action); 
        })
        .addCase(fetchFeatureInfoById.fulfilled, (state, action) => { 
            // state.status = 'succeeded';
            // state.info = action.payload;
            let a = {
                info:action.payload,
                status:'succeeded',
                error:null
            }
            return a;
            // console.log('fulfilled data store ', state, action);
            // return action.payload;
        })
    }
  }) 

//exporting just info of feature
export const featureInfo = (state) => state.features.info; 

In my Component, I have

import store from '../redux/store'; 
import {useSelector,useDispatch} from "react-redux"; 
import { featureInfo, featureError, featureStatus, featureInfoById, fetchFeatureInfoById } from "../redux/features";
import { useRef, useEffect, useState } from 'react';  

const featureInfoA = useSelector(featureInfo) 
const clickFeatureFromList =( sl, s, id, e, groupid, c) => (event, isExpanded) =>  { 
    store.subscribe(()=>{
      const state = store.getState()
      if (state.features.status=='succeeded' || state.features.status=='failed') {
        console.log('BINGO SUBSCRIBE ', featureInfoA); 
      } 
    })

    dispatch(fetchFeatureInfoById({id, category:'ex'}))
    .then((e)=>{
      console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);
      accordionDetailsRootGlobal.render(<ContextInfo/>);
    }) 
  }

The first time I click a button to call the clickFeatureFromList the logs I get are

BINGO SUBSCRIBE  a
BINGO THEN  a null idle

this means that somehow the state never changes, I get the initial one

the second time I click a button to call the clickFeatureFromList the logs I get are normal, eg

BINGO SUBSCRIBE  a
BINGO SUBSCRIBE  {success: false, msg: 'resolved error'}
BINGO SUBSCRIBE  {success: false, msg: 'resolved error'}
BINGO THEN  {success: false, msg: 'resolved error'} null succeeded 

this is correct, state changes.

Why state does not change the first time? What am I doing wrong?

EDIT as you can see in .addCase(fetchFeatureInfoById.fulfilled, (state, action) => { I tried different syntaxes, nothing works

EDIT

check this sandbox to see what is wrong. Open the console of the sandbox to see errors

Upvotes: 1

Views: 691

Answers (1)

Drew Reese
Drew Reese

Reputation: 203532

I've looked at the sandbox. Upon clicking the "test" button I see the state update on the first click. This is even confirmed in the redux dev tools.

enter image description here

I think there are at least a couple issues occurring here, both related to Javascript closures. The first is about a stale closure over the selected featureInfoA, featureErrorA, featureStatusA states in the callback (clickFeatureFromList above and testme in the sandbox), while the second is about accessing the current Redux state while still in the callback scope (the same closure).

Using the code from your sandbox this is what I'd suggest as an implementation so you can (a) access the Redux state correctly in the callback Promise chain and (b) log the states in a meaningful way.

Code:

export function Counter() {
  const dispatch = useDispatch();
  const featureInfoA = useSelector(featureInfo);
  const featureErrorA = useSelector(featureError);
  const featureStatusA = useSelector(featureStatus);

  useEffect(() => {
    console.log("Render ", {
      features: {
        info: featureInfoA,
        error: featureErrorA,
        status: featureStatusA
      }
    });
  }, [featureErrorA, featureInfoA, featureStatusA])

  const testme = (id, category) => {
    dispatch(fetchFeatureInfoById({ id, category })).then((e) => {
      const state = store.getState();
      console.log("BINGO THEN closure", {
        features: {
          info: featureInfoA,
          error: featureErrorA,
          status: featureStatusA
        }
      });
      console.log("BINGO THEN state", state);
    });
  };

  return (
    <div>
      <button onClick={() => testme(4, "todos")}>test</button>
    </div>
  );
}
  • The useEffect hook logs the selected state per render cycle
  • The current store's state value is accessed in the .then callback, not outside the Promise chain
  • The .then is also logging the selected state that was closed over in callback scope so you could see its staleness

Running the above code and clicking the "test" button exactly once produces the following log output. I'll annotate the logs to the code.

enter image description here

  1. Initial render Render {"info":"a","status":"idle","error":null}
  2. "test" button clicked
  3. Asynchronous action log fetchFeatureInfoById originalData {id: 4, category: "todos"}
  4. Rerender because action pending Render {"info":"a","status":"loading","error":null}
  5. DATA from server {data: Object, status: 200, statusText: "", headers: AxiosHeaders, config: Object…}
  6. In the Promise chain
    • The stale enclosure BINGO THEN closure {"info":"a","status":"idle","error":null}
    • The current state BINGO THEN state {"features":{"info":{"userId":1,"id":4,"title":"et porro tempora","completed":true},"status":"succeeded","error":null}}
  7. Rerender because action fulfilled, state updated Render {"info":{"userId":1,"id":4,"title":"et porro tempora","completed":true},"status":"succeeded","error":null}

Edit useselector-updates-only-the-second-time-using-redux-toolkit

Upvotes: 2

Related Questions