how can I access state from another slice with redux toolkit - reactjs

I have multiple slice and I want to access state from slice in another slice, so how can I access state from productsSlice in filterSlice
productsSlice
i want to access products state from this slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
products: ["data"], // i want to access products in filterSlice
};
export const productsSlice = createSlice({
name: "products",
initialState,
reducers: {},
});
filterdSlice
add products state to filterdProduct
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
filteredProduct: [// access products from proudctsSlice ],
}
export const filterSlice = createSlice({
name : "filter",
initialState,
reducers: { }
})

Two possible directions for you -
Reducers only have access to the state of the slice they are part of and I think you should keep it that way. So may be try redesigning it in a way you won't have to do the state sharing between slices.
Have look at this FAQ question "how can I share state between reducers?" for more details.
And if you still want to go ahead, then you can try importing the store itself in the intended module and do const reduxStore = store.getState();. And you access anything you want :)

Related

Composing Redux Toolkit generic higher-order reducers into a meaningful state hierarchy with configureStore

I'm returning to Redux + Redux Toolkit to manage some React app state.
I'm trying to get my head around generic reducers, specifically for slices of state that share the same reducer logic and state structure. Namely an "uploaded files" slice, representing files that have been uploaded to a server/local store. Having read this section of the docs about re-using reducer logic, I came up with this higher-order reducer:
import { createSlice, PayloadAction, SliceCaseReducers, ValidateSliceCaseReducers } from '#reduxjs/toolkit'
interface UploadFilesState {
files: {
type: 'font'|'image'
fileName: string
filePath: string
}[]
maxFiles: number
}
export const createUploadFilesSlice = <
Reducers extends SliceCaseReducers<UploadFilesState>
>({
name = '',
initialState,
reducers,
}: {
name: string
initialState: UploadFilesState
reducers?: ValidateSliceCaseReducers<UploadFilesState, Reducers>
}) => {
return createSlice({
name,
initialState,
reducers: {
fileUploaded(state: UploadFilesState, action: PayloadAction<UploadFilesState['files'][0]>) {
if (state.files.length < state.maxFiles) {
state.files.push(action.payload)
}
},
...reducers,
},
})
}
But I'm stumped as to how to use it with configureStore() and the state structure I would like. For example, my global state should look something like this:
// Written as a Typescript interface to visualise the shape of the store
{
appName: string
defaults: {
background: {
colour: string
images?: /* Using my generic uploadFileSlice here */
}
fonts: /* Using my generic uploadFileSlice here */
}
}
How would I utilise my genric slices with Redux Toolkit's configureStore() method? For example, something like this:
export default configureStore({
reducer: {
appName: /* Some string reducer */
defaults: {
background: {
colour: /* some colour reducer */
images: createUploadFilesSlice({ name: 'defaultBackgroundImages', initialState: { files: [], maxFiles: 1}})
}
fonts: createUploadFilesSlice({ name: 'defaultFonts', initialState: { files: [], maxFiles: 2}})
}
},
})
The above throws. Is it that I can't be allowed to nest reducers in object literals in this way? Should I be instead creating very flat structures like:
{
appName: string
defaultBackgroundColour: string
defaultBackgroundImages: /* Using my generic uploadFileSlice here */
defaultFonts: /* Using my generic uploadFileSlice here */
}
It's been a while since I've used Redux so I'm unfamiliar with what the limitations are.
The primary issue here is that the Redux core combineReducers method is not recursive - it accepts an object with a single level of sliceName: someSliceReducer pairs. When the reducer field in configureStore is given an object, it really just passes that on to combineReducers so you don't have to call that yourself.
If you want to have some additional levels of nesting that way, you'd need to call combineReducers yourself a couple of times, like this:
const backgroundReducer = combineReducers({
color: colorReducer,
images: createSomeReusableReducer("images")
})
const defaultsReducer = combineReducers({
background: backgroundReducer,
fonts: createSomeReusableReducer("fonts")
})
const store = configureStore({
reducer: {
appName: appNameReducer,
defaults: defaultsReducer
}
})

redux-undo that wraps all slices in RTK?

I am implementing redux-undo library introduced in redux docs in my RTK project. The example in the docs guides you to wrap single slice (or reducer) to make it 'undoable'.
// todos.js
import undoable from 'redux-undo'
/* ... */
const todos = (state = [], action) => {
/* ... */
}
const undoableTodos = undoable(todos)
export default undoableTodos
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter
})
export default todoApp
You may wrap one or more reducers in undoable at any level of the reducer composition hierarchy. We choose to wrap todos instead of the top-level combined reducer so that changes to visibilityFilter are not reflected in the undo history.
This example, as described above, only restores changes in todos slice. Anything happened to visibilityFilter is not undoable.
What I want is to wrap the entire store with undoable() so that with one undo() function call, you can revoke all changes to the store.
Below is my attempt in redux toolkit and I want to know if this is the right way to go.
import { configureStore } from "#reduxjs/toolkit";
import undoable from 'redux-undo'
import todoSlice from "../features/todo/todoSlice";
import visibilityFilterSlice from "../features/visibilityFilter/visibilityFilterSlice";
const store = configureStore({
reducer: {
todos: todoSlice,
visibilityFilter: visibilityFilterSlice,
},
});
export default undoable(store);
No, your attempt wraps the store instance, not a reducer - redux-undo won't know what to make of that. Try this instead (using combineReducers with RTK is totally fine):
const rootReducer = combineReducers({
todos: todoSliceReducer,
visibilityFilter: visibilityFilterSliceReducer,
})
const undoableRootReducer = undoable(rootReducer)
const store = configureStore({
reducer: undoableRootReducer,
});
For TypeScript it is important not to do reducer: undoable(rootReducer),, but to do this in a variable declaration above the configureStore call.

Load initialState dynamically with createSlice in Redux Toolkit

Is there a well-known pattern for injecting a payload of dynamic initial state into Redux-Toolkit's initialState object?
That is, I would like to do this -
import initialState from './initialState';
function generateSlice(payload = {}){
const postsSlice = createSlice({
name: 'posts',
initialState: {...initialState, ...payload}, /// inject data here
reducers: {...}
})
}
For example, {availableRooms: []} is an empty array, unless injected on init with data {availableRooms: [{...}]}
This pattern doesn't work, however, b/c I want to export actions to be dispatch-able, something like this-
const postsSlice = createSlice({
name: 'posts',
initialState: {...initialState, ...payload},
reducers: {...}
})
export {actionName} from postsSlice.actions;
*****
import {actionName} from '../mySlice'
...
const dispatch = useDispatch();
dispatch(actionName('exampleVal'));
...
I am constrained by the airbnb linting rules, so I can't export on let -
let actions; ///Bad
function generateSlice(payload){
const postsSlice = createSlice({
name: 'posts',
initialState: {...initialState, ...payload},
reducers: {...}
})
actions = postsSlict.actions
}
export actions;
The functionality that I am after is a bit easier without using createSlice. The reason for my question is that I have seen in multiple places that createSlice is recommended over createAction + createReducer, but I don't see any simple way to introduce the dynamic data that I am looking for.
I don't know anything about redux-orm but I think the functionality that I am after is similar to this SO question
Here's my current work-around, which skips createSlice altogether.
In the root render
...
const store = initStore(data);
<Provider store={store}>
<App />
</Provider>
And the init function (pared down for brevity)
import {
configureStore,
getDefaultMiddleware,
combineReducers,
} from '#reduxjs/toolkit';
import reservationReducer from '#reservation/reducer';
import spaceHierarchyReducer from '#map/reducer';
import appStoreReducer from '#app/reducer';
let ReduxStore;
function initStore(
ssrState: Partial<RootStore> = {},
) {
if (ReduxStore) {
return ReduxStore;
}
const slices = {
reservation: reservationReducer,
spaceHierarchy: spaceHierarchyReducer,
appStore: appStoreReducer,
};
const reducer = combineReducers(slices);
const preloadedState = getInitialState(ssrState);
const store = configureStore({
reducer,
middleware,
preloadedState,
});
ReduxStore = store;
initDispatch(store);
return store;
}
In getInitialState, I parse the URL and set-up the store based on business requirements, a mixture of server-side data + url-injectable params. Then, in initDispatch, I invoke store.dispatch() for some init logic based that injected initial state.
Here the usage of Typescript is quite helpful, as it enforces the shape of the data returned from getInitialState as well as the shape of the reducers.
I found a work around with Redux Tool Kit. I'm kind of new to Redux because Context API cannot rerender React Native Navigation Screens as they are not part of the main tree. I don't know if my approach is good enough, but here was my thinking:
generateSlice() wouldn't fill actions variable because at the time the export is made to be used by RTK module, generateSlice hasn't been called yet.
At the beginning, RTK module just need the structure and configuration for createSlice, but not the store object yet. Only the configureStore really care about the store itself. So that with a duplicate call: exporting actions with normal default initialState and then recalling it inside generateSlice(initValue) with the real default initialValue seems to work well.
To keep it simpler for everyone, I'm giving an example with the official short tutorial on RTK https://redux-toolkit.js.org/tutorials/quick-start :
counterSlice.js :
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
value: 0,
};
const slicer = initState =>
createSlice({
name: 'counter',
initialState: initState,
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;
},
},
});
const generateSlice = initState => {
return slicer(initState).reducer;
};
export const counter = slicer(initialState);
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counter.actions;
export default generateSlice;
store.js becoming a function now, rename it getStore if necessary :
import { configureStore } from '#reduxjs/toolkit';
import generateCounterReducer from '../states/reducers/counter';
export const store = states => {
return configureStore({
reducer: {
counter: generateCounterReducer(states.counter),
},
});
};
App.js or index.js where you put the redux Provider:
<Provider
store={store({
counter: { value: 7 },
})}
>
And when I load the component, the value 7 is rendered by default. The only problem with it is that it executes the createSlice 2 times. But since this only happens at the App start, then I see no performance issue with that approach. Maybe the pattern will conflict with advanced usage, so if anyone see any bottleneck, we can discuss it and figure out how to improve it.

state variables are coupled with reducer names in redux

This might be a really silly one, but I was hanging my head around it for a while. I have a React project ( with Redux ). In mapStateToProps, state value is coming as undefined if I try to access the state directly as
const mapStateToProps = state => ({ data: state.data });
Instead, I always have to specify my reducer name ( the reducer which handles this particular state in it ) to access the state value :
const mapStateToProps = state => ({ data: state.customReducer.data });
Here is my code :
import { combinedReducer } from 'redux;
import customReducer from './customReducer';
const rootReducer = combineReducer({
customReducer
});
export default rootReducer;
customReducer.js : as follows
const initialState = {};
const customReducer = ( state = initialState, action ) => {
const { type, payload } = action;
switch (type) {
case 'SOME_ACTION':
return {
...state,
data: payload.data
}
break;
default:
return state;
}
}
export default customReducer;
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const configStore = () => {
return createStore (
rootReducer,
applyMiddleware(thunk)
);
}
const store = configStore();
export default store;
Does anyone know what is going wrong with the implementation ? Or is it the way to access different state values from different store ?
How can I directly access any state variable as
data: state.`state_value` , instead of , data : state.`reducerName`.`state_value` ?
Any help on this would be much appreciated.
There's nothing wrong with your implementation, this is just the way that combineReducers works. The reducer names are used to partition your store (e.g. Everything in your users reducer will be under state.users).
If you don't want to have to use the name of the reducer in your mapStateToProps, you can use selectors. However, in the selector, you will still have to use state.reducerName.
If you really don't want to have to go through more than one layer to get to the value you want, you could create a separate reducer for each value (i.e. In your example, the reducer would be named data). This obviously isn't the preferred way of doing it though.
Well, combineReducers isn't really using the reducer name, but the property name you specify for it. If you do:
const rootReducer = combineReducer({
foo: customReducer,
});
You'll be able to access the data at state.foo.data.

Redux store changes connected component props without corresponding action being dispatched

I have a very weird issue, I have redux store and a react component connected to it with connect() function. I am storing a list of user roles coming from my backend in redux. For this I have two different reducers, userRoleReducer and initialUserRolesReducer. I use the first one to handle changes into the roles in the UI before applying the changes with an API call, and the second one is being used to have the initial roles stored separately after backend responses. The issue I am having, is that both of the reducers are changing, even though only the first one is actually being updated by dispatching an action (Sorry if my use of terms is incorrect). Below are the reducers and action dispatchers.
Reducers:
export function userRolesForUsersRequestSuccess(state = {userRoles: []}, action) {
switch(action.type) {
case 'USER_ROLES_FOR_USERS_REQUEST_SUCCESS':
return action.userRoleDataForUsers;
default:
return state;
}
}
export function initialUserRolesForUsersRequestSuccess(state = {userRoles: []}, action) {
switch (action.type) {
case 'INITIAL_USER_ROLES_FOR_USERS_REQUEST_SUCCESS':
return action.initialUserRoleData;
default:
return state;
}
}
These are the action dispatchers, the first one is called from the connected component, and and after backend response. The second one is called only after the backend response.
export function setUserRolesForUsersRequestSuccess(userRoleDataForUsers) {
return {
type: 'USER_ROLES_FOR_USERS_REQUEST_SUCCESS',
userRoleDataForUsers
};
}
export function setInitialUserRolesForUsersRequestSuccess(initialUserRoleData) {
return {
type: 'INITIAL_USER_ROLES_FOR_USERS_REQUEST_SUCCESS',
initialUserRoleData
};
}
I haven't found anything similar to this from anywhere, so I guess this isn't a common problem, and that's why a good guess is that the issue is in my code. But every other reducer I use are working just fine, and believe me, I have tried to change and check everything I can to make these two work normally as well.
Any help is wanted to track the issue down!
EDIT: The code I use to create the store, not sure if it helps.
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';
import createHistory from 'history/createBrowserHistory';
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const history = createHistory();
const middleware = routerMiddleware(history);
const initialState = {};
const store = createStore(
rootReducer,
initialState,
composeEnhancers(
applyMiddleware(middleware, thunk))
);
EDIT 2. rootReducer.js file, reducers are combined here.
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import {
userRoleForMeRequestSuccess,
userRolesForUsersRequestSuccess,
userRoleRequestPending,
userPermissionChangeGetResponseMessage,
initialUserRolesForUsersRequestSuccess } from './Common/userRoleReducer';
const appReducer = combineReducers({
userRoleForMeRequestSuccess,
userRolesForUsersRequestSuccess,
userRoleRequestPending,
userPermissionChangeGetResponseMessage,
initialUserRolesForUsersRequestSuccess,
router: routerReducer
});
const rootReducer = (state, action) => {
if (action.type === LOGIN_LOGOUT_REQUEST_SUCCESS) {
state = undefined;
}
return appReducer(state, action);
};
export default rootReducer;
EDIT 3. After I dug more deeply into this problem, I made an observation that if I just pass completely different data for the first reducer, its data stays intact and doesn't change when the other one changes. So could there be some kind of issue in passing exactly the same data as the first new state after the reducers initial state, and that mixes the reducers somehow to always mirror each other?

Resources