Skip to content
Advertisement

Unable to access properties on Typed React Redux store

I been searching up and down google, official docs, stack overflow for a way to access my Redux store, from a React functional component (Typescript). In theory this should be easy to do with the various guides out there, but no matter what I do I keep getting undefined errors. I am able to get the store object with a custom typed selector hook and console.log it and see the properties as expected, but when accessing them, I am still getting access denied. Strangely enough, the only way I was able to get this to work was stringifying and then parsing the store object as JSON. While that did work that is not ideal and I trying to figure out the proper way to achieve this. I believe I have narrowed it down to some Typing issues. I should also note that I have no issues dispatching actions to update the values in the store. I may not be explaining my scenario well, so here is my code and examples to better demonstrate:

Setup

/src/state/action-types/index.ts:

export enum ActionType {
    UPDATE_LOADING_STATUS = 'update_loading_status',
    UPDATE_ONLINE_STATUS = 'update_online_status',
    UPDATE_APP_LAUNCH_COUNT = 'update_app_launch_count',
}

/src/state/actions/index.ts:

import { ActionType } from '../action-types'

export interface UpdateLoadingStatus {
    type: ActionType.UPDATE_LOADING_STATUS
    payload: boolean
}

export interface UpdateOnlineStatus {
    type: ActionType.UPDATE_ONLINE_STATUS
    payload: boolean
}

export interface UpdateAppLaunchCount {
    type: ActionType.UPDATE_APP_LAUNCH_COUNT
}

export type Action = UpdateLoadingStatus | UpdateOnlineStatus | UpdateAppLaunchCount

/src/state/reducers/AppStateReducer.ts:

import produce from 'immer'
import { ActionType } from '../action-types'
import { Action } from '../actions'

interface AppState {
    isLoading: boolean
    isOnline: boolean
    isAppVisible: boolean | null
    entitlements: string[] | null
    persona: string | null
    theme: 'light' | 'dark' | 'default'
    appLaunchCount: number
}

const initialState: AppState = {
    isLoading: true,
    isOnline: false,
    isAppVisible: null,
    entitlements: null,
    persona: null,
    theme: 'default',
    appLaunchCount: 0,
}

export const reducer = produce((state: AppState = initialState, action: Action) => {
    switch (action.type) {
        case ActionType.UPDATE_LOADING_STATUS:
            state.isLoading = action.payload
            return state
        case ActionType.UPDATE_ONLINE_STATUS:
            state.isOnline = action.payload
            return state
        case ActionType.UPDATE_APP_LAUNCH_COUNT:
            state.appLaunchCount = state.appLaunchCount + 1
            return state
        default:
            return state
    }
}, initialState)

/src/state/index.ts:

import { combineReducers } from 'redux'
import { reducer as AppStateReducer } from './reducers/AppStateReducer'

export const reducers = combineReducers({
    appstate: AppStateReducer,
})

export type RootState = ReturnType<typeof reducers>

/src/state/store.ts:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { reducer } from './reducers'
import { composeWithDevTools } from 'redux-devtools-extension'

export const store = createStore(
    reducer,
    {
        isLoading: true,
        isOnline: false,
        isAppVisible: null,
        entitlements: null,
        persona: null,
        theme: 'default',
        appLaunchCount: 0,
    },
    composeWithDevTools(applyMiddleware(thunk))
)

/src/index.tsx:

import * as ReactDom from 'react-dom'
import { Provider } from 'react-redux'
import { store } from './state/store'
import { App } from './components/App'

ReactDom.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.querySelector('#root')
)

/src/components/App.tsx:

import { useEffect } from 'react'
import { useActions } from '../hooks/useActions'
import { useTypedSelector } from '../hooks/useTypedSelector'
import { RootState } from '../state'

export const App: React.FC = () => {
    const { updateLoadingStatus, updateOnlineStatus, updateAppLaunchCount } = useActions()

    const stateA = useTypedSelector((state) => state)

    console.log(state)

    return (
        ...content...
    )
}

src/hooks/useTypedSelector.ts

import { useSelector, TypedUseSelectorHook } from 'react-redux'
import { RootState } from '../state'

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector

Examples

Ok this is where the fun begins.

If I do this: const stateA = useTypedSelector((state) => state) I get the overall store object in the console.log:

{isLoading: false, isOnline: true, isAppVisible: null, entitlements: null, persona: null, …}
appLaunchCount: 2
entitlements: null
isAppVisible: null
isLoading: false
isOnline: true
persona: null
theme: "default"
__proto__: Object

But if I try to do: const stateA = useTypedSelector((state) => state.appLaunchCount) I get this error, even though the output is logged properly.

Property 'appLaunchCount' does not exist on type 'CombinedState<{ appstate: AppState; }>'

I was still getting an output logged on the store object, I had an the idea to stringify and parse that state object, and then I was able to access the properties: const stateB = JSON.parse( useTypedSelector(( state ) => JSON.stringify( state )))

However, the documentation I find online says should be able to access properties like this: const stateC= useTypedSelector((state) => state.appstate.appLaunchCount), but instead I get this error:

Uncaught TypeError: Cannot read property 'appLaunchCount' of undefined

I suspect it may be an issue with the shape or Type of the store, but I am not sure what else I can try. Last clue I have is that if I hover over the RootState object is this:

(alias) type RootState = EmptyObject & {
    appstate: AppState;
}

Not sure that empty object is about and/or if it preventing me from accessing the properties. Please assist.

Advertisement

Answer

You have a mismatch in your reducer file and your store setup file.

In src/state/index.ts:, you have:

import { combineReducers } from 'redux'
import { reducer as AppStateReducer } from './reducers/AppStateReducer'

export const reducers = combineReducers({
    appstate: AppStateReducer,
})

export type RootState = ReturnType<typeof reducers>

and in src/state/store.ts, you have:

import { reducer } from './reducers'
import { composeWithDevTools } from 'redux-devtools-extension'

export const store = createStore(
    reducer,
    // etc
)

If you look very carefully… you imported reducer into your store file. That’s the individual “app state” slice reducer, not your combined “root reducer”. But, the TS type you’re exporting is the combined root reducer.

So, you set up the types correctly, but got the runtime behavior doing something else.

Change import { reducer } to import { reducers }, and fix what you’re passing to the store to match, and it should work okay.

As a side note: you should really be using our official Redux Toolkit package and following the rest of our TS setup guidelines. That would completely eliminate all the “actions” files, part of the code in the reducer, and the rest of the config setup in the store file.

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement