Load initialState dynamically with createSlice in Redux Toolkit - reactjs

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.

Related

how can I access state from another slice with redux toolkit

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 :)

useSelector not updating with Redux Toolkit

I've been battling this all day long and I'd appreciate any help.
I have a redux store built with Redux Toolkit and createSlice that looks like so:
const initialState = {
analiticaNumber: "",
animal: {},
tests: [{ key: "G9116", value: "DERMATOFITOS PCR/ MUESTRA" }],
};
const PeticionSlice = createSlice({
name: "peticion",
initialState,
reducers: {
addTest: (state, action) => {
state.tests.push(action.payload);
},
},
});
export const { addTest: addTestActionCreator } = PeticionSlice.actions;
export const testsArray = (state) => state.Peticion.tests;
export default PeticionSlice.reducer;
I also have a root reducer that imports the rest of the slices and names them as such
import { combineReducers } from "redux";
import NavigationSlice from "./NavigationSlice";
const RootReducer = combineReducers({
Peticion: PeticionSlice,
});
export default RootReducer;
When I add tests to the tests array it works fine and shows in the redux devtools.
The promblem comes that react does not see the change in the store and won't update the child component:
import { testsArray } from "./Store/PeticionSlice";
That's how I import namely the testsArray to call with the useSelector.
The tradicional way of const { tests } = useSelector( (state) => state.Peticion) doesn't work either.
function App() {
const tests = useSelector(testsArray);
useEffect(() => {
console.log("tests");
}, [tests]);
return (
<StylesProvider injectFirst>
<div className="App">
<nav>
<Navbar />
</nav>
{tests.map((test) => (
<p>{test.key}</p>
))}
</div>
</StylesProvider>
);
}
I belive it has to do something with the mutability of the state, but I thought the toolkit took care of that, and for the life of me I don't know how to solve this.
Any help??? Thanks a lot.
** UPDATE **
I believe it has to do with the way I dispatch the actions. Because I needed to add several boundaries to what the app does, I decided to have an external function that filters and dispatches accordingly. It is not a react component.
import { configureStore } from "#reduxjs/toolkit";
import { addTestToList, addTestActionCreator } from "../Store/PeticionSlice";
import RootReducer from "../Store/RootReuder";
const PruebasToSubmitArray = [];
const store = configureStore({
reducer: RootReducer,
});
const handleTestList = (test, action) => {
const anatomia = "A";
const microbiologia = "M";
function oneBiopsia() {
while (test.key.toString().charAt(0) === anatomia) {
return PruebasToSubmitArray.some(
(pruebaInArray) => pruebaInArray.key.toString().charAt(0) === anatomia
);
}
return false;
}
if (!oneBiopsia() && action === "add") {
switch (test.key.toString().charAt(0)) {
case anatomia:
// console.log("Open pdf for anatomia");
store.dispatch(addTestActionCreator(test));
break;
case microbiologia:
// console.log("Open pdf for micro");
store.dispatch(addTestActionCreator(test));
break;
default:
// console.log("add test to the list, ", test);
store.dispatch(addTestActionCreator(test));
break;
}
} else if (action === "remove") {
// console.log("remove test from the list, ", test);
} else if (oneBiopsia()) {
console.log("Only one biopsia per peticion, ", newState);
}
return null;
};
export default handleTestList;
I added a button on App component and it worked as expected (i showed the updated state), as is right now redux updates the state but the component won't reflect it.
Code SandBox as complete as I can
Very odd behavior in my case.
I did
state = action.payload
and that didn't work.
Once I switched to
state.viewer = action.payload.viewer
everything worked!
Multiple Instances of Store
You create a store variable in your index.js file and pass that store to the react-redux Provider component. This is the store instance which all react-redux useSelector and useDispatch hooks will interact with.
In your HandleTestList.js file you create a different store variable. You then dispatch actions to that store, but those actions won't be reflected in your React app because this isn't the store that your React app uses.
handleTestList needs to either A) import the same global store variable. You will want to move this out of index.js and into store.js to avoid circular dependencies. or B) accept dispatch as an argument.

Redux state gets overwritten when calling 2 API using useEffect

I have 2 actions that calls different API. I dispatch these actions in a useEffect.
I have 2 reducers files, One for each, to store the data received from the API.
So, Basically I should be able to access both the data individually using useState.
But the secondly called API's data is overwriting the data of the first API. I don't understand how, because they are not even on the same file or even related.
Component
const items = useSelector((state) => state.lostchambers.items);
const lostChambersItems = useSelector((state) => state.sharklostdolsea.itemsLostChamber);
useEffect(() => {
dispatch(fetchingLostChambers());
dispatch(fetchSharkLostDolSea());
}, [dispatch]);
The Action for both the files looks like this I'm only posting here for one file as its the same code
import { FETCH_POSTS } from "./type";
import axios from "../../utils/Request";
export const fetchingLostChambers = () => async (dispatch) => {
const response = await axios.get("API");
const { data = false, status } = response;
if (status === 200) {
if (data) {
dispatch({
type: FETCH_POSTS,
items: data.acf,
});
}
}
};
The Reducer for both the actions looks like this but I'm only posting here for one file as its the same code
import { FETCH_POSTS } from "../actions/lostchambers/type";
const initialState = {
items: [],
};
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_POSTS:
return {
...state,
...action,
};
default:
return state;
}
};
Combined Reducer
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducers from "./reducers";
const initialState = {};
const middleware = [thunk];
const store = createStore(rootReducers, initialState, applyMiddleware(...middleware));
export default store;
RootReducer
import { combineReducers } from "redux";
import venues from "./venues";
import lostchambers from "./lostchambers";
import sharklostdolsea from "./sharklostdolsea";
export default combineReducers({
venues,
lostchambers,
sharklostdolsea,
});
Am I missing something here? I just can't figure out the issue ,I'v been trying four hours now.
The main issue I see here is that you are using the same type constant for both actions and reducers.
The way redux works is that it will pass the actions through all of the reducers that are combined together and will run whatever state changes the reducer says happens. That's why when you set up reducers you need the base case to return state if nothing matches.
By using the same type in the actions, both reducers will see both actions that were dispatched and perform the change. So a race condition occurs and the last one that is returned shows in both parts of state.
You should be able to fix this by just changing the action and reducer type constant for one/both of them.

How to quickly navigate to Redux reducer from an Action in WebStorm?

I have to write a Redux application using #reduxjs/toolkit
Here are the Redux actions in todoActions.js
// todoActions.js
import {createAction} from "#reduxjs/toolkit";
const TodoActions = {
/**
* #function
* #param {String} todo
*/
addTodo: createAction("addTodo"),
/**
* #function
* #param {String} todoId
*/
removeTodo: createAction("removeTodo"),
};
export default TodoActions;
and the reducer stays in todoReducer.js
import {createReducer} from "#reduxjs/toolkit";
import TodoActions from "../todoActions";
const initState = {
todos: [],
};
const reducer = createReducer(initState, {
[TodoActions.addTodo]: (state, action) => {
state.todos = [state.todos, ...action.payload];
},
[TodoActions.removeTodo]: (state, action) => {
state.todos = state.todos.filter(x=>x.id!==action.payload);
},
});
export default reducer;
And here is how I use actions in a React Component
import React from 'react'
import {useDispatch} from 'redux-react-hook'
import TodoActions from './todoActions'
const AddRandomButton () {
const dispatch = useDispatch()
const clickHandler = event => {
dispatch(TodoActions.addTodo("some random todos"))
}
return (
<button onclick={clickHandler}>
Add
</button>
)
}
The problem is that: it is not easy to navigate from an action to its appropriate logic.
When I CTRL+CLICK on the addTodo() inside AddButton component, the IDE will jump to the definition of the addTodo(), which shows nothing about the logic of this function, because the logic is in the reducer.
Question: Is there any plugin or tips how to quickly go to the reducer instead of the action definition in IntelliJ/WebStorm?
I'm a Redux maintainer and creator of Redux Toolkit.
I would strongly recommend that you use createSlice instead of createAction/createReducer directly.
There's no reason to define the action creators separately here, and the fact that you're doing them separately is what's causing the problem you're describing.
In addition, the object wrapping the todos array is unnecessary in this case.
I would write this as:
import {createSlice} from "#reduxjs/toolkit";
const initialState = [];
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
addTodo(state, action) {
state.push(action.payload);
},
removeTodo(state, action) {
return state.filter(todo => todo.id !== action.payload);
}
}
});
export const {addTodo, removeTodo} = todosSlice.actions;
export default todosSlice.reducer;
and then pull it together at the app level like:
import {combineReducers} from "#reduxjs/toolkit";
import todosReducer from "features/todosSlice";
export default combineReducers({
todos: todosReducer,
});
This way, when you right-click and "Go to Definition" on addTodo, it'll jump straight to the file that has the reducer logic as well. (And, you'll have fewer files to deal with.)

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