Search code examples
react-nativereact-reduxredux-toolkit

Redux Toolkit useSelector state not updating even though action is called


I've been banging my head against this, hopefully getting another pair of eyes on it will help.

I'm using Redux + Redux Toolkit in a React Native App, in a pretty simple way. I can tell (through a log statement) that my action is being called and the state is getting set, but my useSelector on the state never updates. I've tried it with shallowEqual as well, but that shouldn't be needed, since Redux Toolkit uses Immer and the object shouldn't pass an equality check after updating (most of the other similar issues I researched were due to that)

Here's my main slice, followed by all the related code. Pardon the code dump, but I want to give a full picture:


export interface Metadata {
    title: string
    author: string
    firstLines: string
    id: string
}

type MetadataState = Record<string, Metadata>

export const metadataSlice = createSlice({
    name: "metadata",
    initialState: {} as MetadataState,
    reducers: {
        setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
            state = action.payload
            console.log("new metadata: ", state)
        },
        addMetadata: (state: MetadataState, action: PayloadAction<Metadata>) => {
            state[action.payload.id] = action.payload
        }
    }
});

I have an async action to load the metadata from AsyncStorage (like LocalStorage on mobile), as follows:

export function loadMetadata() {
    return async (dispatch: AppDispatch, getState: () => RootState) => {
        const maybeMetadata = await AsyncStorage.getItem("metadata");
        if(maybeMetadata) {
            dispatch(metadataSlice.actions.setMetadata(JSON.parse(maybeMetadata)))
            return true
        } else {
            return false
        }
    }
}

And I dispatch that in my main component as follows:

  const dispatch = useAppDispatch()

  useEffect(() => {
    dispatch(loadMetadata())
  }, [])

In another component, I'm trying to access the state simply by doing:

const metadata = useAppSelector(state => state.metadata)

Any idea what's going on? The state just never seems to update, even though I see my action being called and update the state within it. Is it not being dispatched correctly? I tried directly accessing the state with store.getState() and the state seems empty, is it somehow just not being set?

I'm honestly pretty lost, any help is appreciated.


Solution

  • The issue had to do with how Immer (which Redux Toolkit leverages for allowing mutable operations) works.

    setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
      state = action.payload
      console.log("new metadata: ", state)
    }
    

    Instead of mutating state, I reassigned it, which messed up the way Immer keep track of draft states. The console.log statement returned the new state, but it didn't work with Immer. Instead, I needed to do this:

    setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
      // simply return the new state, since I'm changing the whole state
      return action.payload
    }
    

    And it works fine now. I'm kind of surprised I didn't see this documented (it may be somewhere) or get some sort of warning, but good to know for the future!