Javascript imports (redux toolkit) - javascript

In the official redux toolkit documentation/tutorial, there's this file (counterSlice.js)
import { createSlice } from '#reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
The reducer is then imported in the store:
import { configureStore } from '#reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
As I cannot see any reference to 'counterReducer' in the counterSlice.js file, I assume the 'counterReducer' in the import:
import counterReducer from '../features/counter/counterSlice'
is an arbitratry name and could be any other name of our choice, for example:
import counterSliceReducer from '../features/counter/counterSlice'
Is that correct?
Also, where is this 'reducer' in the default export coming from?
export default counterSlice.reducer
The counterSlice element contains 'reducers' object, not 'reducer'. Is that pulled from under the hood?
Thanks

You can use any name if the imported module uses the export default xxx method to export the module.
createSlice will return an object that looks like:
{
name : string,
reducer : ReducerFunction,
actions : Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>
}
Take a look at this docs

I assume the 'counterReducer' in the import:
import counterReducer from '../features/counter/counterSlice'
is an arbitratry name and could be any other name of our choice
You are correct, it is an ES6 feature, a default export can be imported with any name. see: MDN Page
Also, where is this 'reducer' in the default export coming from?
export default counterSlice.reducer
The counterSlice element contains 'reducers' object, not 'reducer'. Is that pulled from under the hood?
createSlice API returns an object in a form:
{
name : string,
reducer : ReducerFunction,
actions : Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>
}
counterSlice.reducer is the reducer function, it needs to be exported and then passed to the store.

Related

How does Redux Toolkit determine property names on the state object?

import { createSlice } from '#reduxjs/toolkit'
export const countersSlice = createSlice({
name: 'based? based on what',
initialState: [0, 0, 0, 0],
reducers: {
updateCounter: (state, action) => {
var id = action.payload.id
var value = action.payload.value
state[id] += value
}
}
})
export const { updateCounter } = countersSlice.actions
export const selectCount = id => state => state.counter[id]
export default countersSlice.reducer
Why is it that, in the selectCount line, I have to use state.counter when .counter isn't referenced anywhere else in the slice? I do like that it's .counter but I just want to understand how it's coming up with that property.
The name property in createSlice is used internally by redux-toolkit to create the names for your actions. If the name is 'counter' then the updateCounter action will have { type: 'counter/updateCounter' }. If it's 'abc' then your action will have { type: 'abc/updateCounter' }. This name doesn't matter. As long as it's different from any other reducers in your app then you're fine.
If I change .counter to something else, it breaks the entire project
Now you are talking about something else, which is how you select your data from the root state of your app.
export const selectCount = id => state => state.counter[id]
This selector function assumes that the reducer from this slice will be on the counter property of your root reducer. The location of this reducer relative to the root state is determined by the property key when you combine your reducers with configureStore or combineReducers.
Your current selector assumes that your store looks like this:
import {configureStore} from '#reduxjs/toolkit';
import counterReducer from './yourReducerFile.js'
export default configureStore({
reducer: {
counter: counterReducer
}
});
This property key often matches the name of your slice, but it doesn't have to match.
You can use any property key as long as the selector function uses the same one. For example:
export default configureStore({
reducer: {
someOtherProperty: counterReducer
}
});
export const selectCount = id => state => state.someOtherProperty[id]

slice reducer for key "pageName" returned undefined during initialization

I am creating redux app using createSlice(), but get the error:
Error: The slice reducer for key "pageName" returned undefined during
initialization. If the state passed to the reducer is undefined, you
must explicitly return the initial state. The initial state may not be
undefined. If you don't want to set a value for this reducer, you can
use null instead of undefined.
Here is the minimal example for this error.
pageNameSlice.js:
import { createSlice } from "#reduxjs/toolkit";
const pageNameSlice = createSlice({
name: 'pageName',
initalState: "",
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
setPageName: (state, newName) => {
state.pageName = newName
}
}
});
export default pageNameSlice.reducer;
store.js:
import { configureStore } from '#reduxjs/toolkit';
import pageNameReducer from '../features/pageNameSlice';
export const store = configureStore({
reducer: {
pageName: pageNameReducer
},
});
You have a typo there - initalState should be initialState. (This is more common than you might think ^^)

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.
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.

Dispatching a Redux Thunk action outside of a React component with TypeScript

I'm in the process of converting a React Native app into TypeScript and I'm having trouble with dispatching thunk actions outside of the store. Here is how my store is set up currently:
store/index.ts
import { createStore, applyMiddleware, combineReducers, Reducer, Store } from 'redux';
import thunk, { ThunkMiddleware } from 'redux-thunk';
export interface State { ... }
export interface ActionTypes { ... } // All of the non-thunk actions
const reducer: Reducer<State> = combineReducers({ ... });
export default (): Store<State> => {
return applyMiddleware(
thunk as ThunkMiddleware<State, ActionTypes>
)(createStore)(reducer);
}
index.tsx
import { Provider } from 'react-redux';
import createStore from './store/index';
import { registerStore } from './store/registry';
const store = createStore();
registerStore(); // Registers the store in store/registry.ts
AppRegistry.registerComponent(appName, () => () => (
<Provider store={store}>
<App />
</Provider>
));
store/registry.ts
import { Store } from 'redux';
import { State } from './index';
let store: Store<State>;
export const registerStore = (newStore: Store<State>) => {
store = newStore;
};
export const getStore = () => store;
So when the store is created, I'm storing it in the store registry so I can call getStore() from anywhere.
This works fine in components (where I'm not using the registry), for example in my App.tsx:
import { connect } from 'react-redux';
import { ThunkDispatch } from 'redux-thunk';
import { checkAuthStatus as checkAuthStatusAction } from './store/modules/auth/actions';
import { ActionTypes, State as AppState } from './store/index';
interface State = { ... }
interface StateProps { ... }
interface DispatchProps {
checkAuthStatus: () => Promise<boolean>;
}
type Props = StateProps & DispatchProps;
class App extends Component<Props, State> {
async componentDidMount() {
const promptSkipped: boolean = await checkAuthStatus(); // Thunk action, works fine!
}
...
}
const mapStateToProps = ...;
const mapDispatchToProps = (dispatch: ThunkDispatch<AppState, null, ActionTypes>): DispatchProps => ({
checkAuthStatus: () => dispatch(checkAuthStatusAction()),
});
export default connect<StateProps, DispatchProps, {}, AppState>(
mapStateToProps,
mapDispatchToProps,
)(App);
The problem comes when I want to use the registry to dispatch a thunk action:
lib/notacomponent.ts
import { getStore } from '../store/registry';
import { checkAuthStatus, setLoggedIn } from '../store/modules/auth/actions'
const someFunction = () => {
const store = getStore();
const { auth } = store.getState(); // Accessing state works fine!
store.dispatch(setLoggedIn(true)); // NON-thunk action, works fine!
store.dispatch(checkAuthStatus()); // Uh-oh, thunk action doesn't work.
}
This gives me the error:
Argument of type 'ThunkAction<Promise<boolean>, State, null, Action<any>>' is
not assignable to parameter of type 'AnyAction'.
Property 'type' is missing in type 'ThunkAction<Promise<boolean>, State, null, Action<any>>'
but required in type 'AnyAction'. ts(2345)
As far as I am aware, using thunk as ThunkMiddleware<State, ActionTypes> as a middleware allows Redux Thunk to replace the stores dispatch method with one that makes it possible to dispatch thunk actions and normal actions.
I think I need to somehow type the registry in a way that TypeScript can see that the dispatch method is not the default one that only allows normal actions. I am, however, at a loss on how to do this. I can't find any examples of anyone else doing the same thing.
Any help is appreciated.
Edit: The suggested duplicate of How to dispatch an Action or a ThunkAction (in TypeScript, with redux-thunk)?
doesn't solve my issue. I can dispatch thunk actions fine inside components. I'm only having issues outside components, using the store registry defined above.
Edit 2: So it seems I can use the following type assertion when dispatching the thunk action to get rid of the error:
(store.dispatch as ThunkDispatch<State, void, ActionTypes>)(checkAuthStatus())
That is very impractical though. I'm yet to find a way to make it so TypeScript knows that the dispatch method should always be able to dispatch a thunk action.
Your code is almost correct. You incorrectly set return type of your default export.
export default (): Store<State> => {
return applyMiddleware(
thunk as ThunkMiddleware<State, ActionTypes>
)(createStore)(reducer);
}
In case of middleware usage, function above should return not Store<State>, but Store<S & StateExt, A> & Ext where Ext will be overloaded dispatch which will be able to dispatch functions (as redux-thunk do).
To simplify, just remove exact return type and let TypeScript infer type itself
export default () => {
return applyMiddleware(
thunk as ThunkMiddleware<State, ActionTypes>
)(createStore)(reducer);
}
Thats solve your problem.
Alternatively, you may use more classical approach to store creation.
export default () => {
return createStore(reducer, applyMiddleware(thunk as ThunkMiddleware<State, ActionTypes>));
}
Essential things here:
Use createStore as recommended by Redux official doc. createStore called with middleware as second argument will itself call it. But TypeScript declaration files for redux and redux-thunk are preconfigured for such use of createStore. So returned store will have modified version of dispatch. (Make note of Ext and StateExt type parameters of StoreEnhancer<Ext, StateExt>. They will be intersected with resulting store and add overloaded version of dispatch which will accepts functions as arguments).
Also return type of default exported fucntion will be inferred from createStore's return type. It will NOT be Store<State>.
const boundActions = bindActionCreators( { checkAuthStatus }, store.dispatch);
boundActions.checkAuthStatus();
This works and does not look as hacky as 'Edit2'

NGRX: The right way to create a "global" action that multiple reducer react upon?

What is to correct way to declare an "INIT_ACTION" that all reducers in the
ActionReducerMap can react upon?
this an example of how my code, I need all 3 reducers (images, tags, addedTags) to react to a single action
:
import { ActionReducerMap, createFeatureSelector } from '#ngrx/store';
import * as fromRoot from '../../../store';
import * as fromImages from './image.reducer';
import * as fromTags from './tag.reducer';
import * as fromAddedTags from './added-tag.reducer';
export interface AnalysisState {
images: fromImages.State;
tags: fromTags.State;
addedTags: fromTags.State;
}
export interface State extends fromRoot.State {
analysis: AnalysisState;
}
export const reducers: ActionReducerMap<AnalysisState> = {
images: fromImages.reducer,
tags: fromTags.reducer,
addedTags: fromAddedTags.reducer
};
export const getAnlysisState = createFeatureSelector<AnalysisState>('analysis');
this is an example actions file:
import { Action } from '#ngrx/store';
import { Tag } from '../../models/tag.model';
export const enum AddedTagActionTypes {
ADD_TAG = '[ANALYSIS] - Add Tag'
}
export class AddTag implements Action {
readonly type = AddedTagActionTypes.ADD_TAG;
constructor(public payload: Tag) {}
}
export type AddedTagActions = AddTag;
and this is an example reducer:
import { EntityState, EntityAdapter, createEntityAdapter } from '#ngrx/entity';
import * as fromAddedTag from '../actions/added-tag.actions';
import { Tag } from '../../models/tag.model';
export interface State extends EntityState<Tag> {}
export const adapter: EntityAdapter<Tag> = createEntityAdapter<Tag>({
selectId: tag => tag.id
});
export const initialState: State = adapter.getInitialState({});
export function reducer(state = initialState, action: fromAddedTag.AddedTagActions): State {
switch (action.type) {
case fromAddedTag.AddedTagActionTypes.ADD_TAG: {
return adapter.addOne(action.payload, state);
}
default: {
return state;
}
}
}
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
export const getAllAddedTags = selectAll;
An action passes through all reducers. Therefore, if you simply put a case statement for that action in all 3 reducers then it will hit all 3 case statements.
If there is no case statement for a specific action in a reducer then the default case statement will be triggered which simply returns the original state, leaving it unmodified.
This assumes that you are not lazy loading reducers. A reducer will of course only be triggered if it is loaded.

Categories