redux-undo that wraps all slices in RTK? - reactjs

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.

Related

Is it possible to set preloadedState dynamically with RTK configureStore?

I have a unique situation in which my <Provider> component (and entire Redux Store) is exported from a middleware application to multiple front end React apps. The middleware has its own set of reducers but the client apps can inject their own reducers into the store when they call the provider.
It is now being asked that I accept an initial state (preloadedState) object when the Provider is called so that the initial state of the app can be loaded with dynamic initial state. This object will be an arbitrary set of state data (with corresponding reducers) so I'll have the data structures correctly with the shape in the reducers, but I won't know what values they're sending.
Here's the basic ReduxStore set up, changed here for simplicity:
ReduxStore.ts
import { configureStore } from '#reduxjs/toolkit';
import { ExampleReducer } from './slices/ExampleSlice';
export const reducer = {
example: ExampleReducer
};
const ReduxStore = configureStore({
middleware: ...,
reducer,
});
export default ReduxStore;
CoreProvider.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { combineReducers, ReducersMapObject, AnyAction } from '#reduxjs/toolkit';
import ReduxStore, { reducer } from './ReduxStore';
export type ConfigProps = {
newReducers?: ReducersMapObject<unknown, AnyAction>;
preloadedState: Object;
};
const CoreProvider: React.FC<ConfigProps> = ({
children,
newReducers,
}) => {
const newReducer = combineReducers({ ...newReducers, ...reducer });
ReduxStore.replaceReducer(newReducer);
return <Provider store={ReduxStore}>{children}</Provider>;
};
export default CoreProvider;
index.jsx
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<CoreProvider newReducers={{ arbitrary: arbitraryReducer }}>
<App />
</CoreProvider>
</React.StrictMode>,
);
Now, I'm aware of the reasons I should NOT do this, and I am aware that initial state comes from the reducers themselves, which is handled when the reducers are merged with the default reducers when the Provider is called.
I know that configureStore accepts a preloadedState option but where I'm stuck is how to dynamically pass a preloadedState coming in as a prop from the Provider component. I have tried wrapping the configureStore call inside a function that is called within the Provider component, which does indeed set the initial state, but my ReduxStore export within ReduxStore.ts is undefined due to the way the app loads and the configureStore is called.
Provider calling configureStore with preloadedState props:
import { initReduxStore, reducer } from './ReduxStore';
const CoreProvider: React.FC<ConfigProps> = ({
children,
newReducers,
preloadedState
}) => {
const newReducer = combineReducers({ ...newReducers, ...reducer });
const ReduxStore = initReduxStore(preloadedState, newReducer);
return <Provider store={ReduxStore}>{children}</Provider>;
};
export default CoreProvider;
ReduxStore with exported configureStore function and undefined ReduxStore export:
export const reducer = {
example: ExampleReducer
};
let ReduxStore;
export const initReduxStore = (incomingState, incomingReducer) => {
if (ReduxStore) return ReduxStore;
const store = configureStore({
middleware: ...,
preloadedState: incomingState,
reducer: incomingReducer,
});
ReduxStore = store;
return ReduxStore;
};
export default ReduxStore; // This becomes undefined because the file loads before the Provider component calls the initReduxStore function
The ReduxStore export is undefined because the file loads before the Provider component calls the initReduxStore function and ReduxStore is undefined at the time it's exported.
Is there a known way that I have overlooked to easily set the preloaded state object when the provider is called? Should I restructure how the ReduxStore is created?
In short terms, how do I get the preloadedState prop from my Provider to my configureStore, while still exporting the ReduxStore to the rest of the app?
export const ReduxStore = configureStore({
preloadedState,
reducer,
});
// HOW DO I CONNECT THESE
const CoreProvider: React.FC<ConfigProps> = ({
children,
newReducers,
preloadedState
}) => {
return <Provider store={store}>{children}</Provider>;
};
export default CoreProvider;
Any thoughts are appreciated.
The short answer is, you can't. The preloadedState option can only be passed in when you call configureStore(). Once the store is created, the only way to update the store state is to dispatch an action.
On top of that, behavior such as replacing the reducer or dispatching actions would qualify as a side effect, and React components must not have side effects directly in the rendering logic.
The closest suggestion I have would be a useLayoutEffect hook in this component that watches for changes to the provided reducers or state, and does the store.replaceReducer() call.
Also, you could have a wrapping reducer that watches for some kind of a "merge in this additional state" action, and returns the updated state with the additional fields.
But overall, this is a very unusual use case, and not something Redux is really designed for.

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?

Why isn't combineReducers receiving state in Redux?

When I look at a reducer being called via combineReducers, it's not getting the state or action in the arguments.
My reducers file is this:
import { combineReducers } from 'redux';
import nav from './reducers/nav';
import pages from './reducers/pages';
import entities from './reducers/entities';
export default function(initialData) {
return function(state, action) {
if (!state) {
console.log('no state', initialData);
return initialData;
}
// This is showing state as having expected value
console.log('state', state);
return combineReducers({
nav,
pages,
entities
});
}
};
My store initialization is like this:
import reducerWrapper from './reducers';
// Initial state is defined above
var reducers = reducerWrapper(initialState),
store = Redux.createStore(reducers, initialState);
The example code on the site doesn't use a wrapper (which I had seen in some other example). I tried that too and it didn't work. I mean in either example I'm not sure how it would get state/action given what is written out. I feel like I'm missing some magic here.
Updated
Here is the reducers file now:
import { combineReducers } from 'redux';
import nav from './reducers/nav';
import pages from './reducers/pages';
import entities from './reducers/entities';
export default combineReducers({
nav,
pages,
entities
});
followed by store = Redux.createStore(reducerWrapper, initialState), also doesn't work, even when I remove the wrapping inside reducerWrapper and just export default combineReducers`
The answer wasn't easily seen here. The problem is that my reducer did not have a default state.
The store triggers an INIT event here: https://github.com/reactjs/redux/blob/master/src/createStore.js#L204
It then eventually gets to here:
https://github.com/reactjs/redux/blob/master/src/combineReducers.js#L52
Meaning if my reducer is function (state, action) { return state } rather than function (state='anyDefaultValue') { return state }, combineReducer will error saying that the reducer did not return state
combineReducers returns a function that needs to be invoked with state and action. You can just export that directly:
const rootReducer = combineReducers(...);
export default rootReducer;
Or you can wrap it as you currently are:
const rootReducer = combineReducers(...);
export default function (initialData) {
return function (state, action) {
if (!state) {
console.log('no state', initialData);
return initialData;
}
// This is showing state as having expected value
console.log('state', state);
return rootReducer(state, action);
}
}

Resources