Combine redux reducers without adding nesting - javascript

I have a scenario where I have 2 reducers that are the result of a combineReducers. I want to combine them together, but keep their keys at the same level on nesting.
For example, given the following reducers
const reducerA = combineReducers({ reducerA1, reducerA2 })
const reducerB = combineReducers{{ reducerB1, reducerB2 })
I want to end up with a structure like:
{
reducerA1: ...,
reducerA2: ...,
reducerB1: ...,
reducerB2: ...
}
If I use combineReducers again on reducerA and reducerB like so:
const reducer = combineReducers({ reducerA, reducersB })
I end up with a structure like:
{
reducerA: {
reducerA1: ...,
reducerA2: ...
},
reducerB: {
reducerB1: ...,
reducerB2: ...
}
}
I can't combine reducerA1, reducerA2, reducerB1 and reducerB2 in a single combineReducers call as reducerA and reducerB are being provided to me already combined from different npm packages.
I have tried using the reduce-reducers library to combine them togethers and reduce the state together, an idea I got from looking at the redux docs, like so:
const reducer = reduceReducers(reducerA, reducerB)
Unfortunately this did not work as the resulting reducer from combineReducers producers a warning if unknown keys are found and ignores them when returning its state, so the resulting structure only contains that of reducerB:
{
reducerB1: ...,
reducerB2: ...
}
I don't really want to implement my own combineReducers that does not enforce the structure so strictly if I don't have to, so I'm hoping someone knows of another way, either built-in to redux or from a library that can help me with this. Any ideas?
Edit:
There was an answer provided (it appears to have been deleted now) that suggested using flat-combine-reducers library:
const reducer = flatCombineReducers(reducerA, reducerB)
This was one step closer than reduce-reducers in that it managed to keep the keep the state from both reducerA and reducerB, but the warning messages are still being produced, which makes me wonder if the vanishing state I observed before was not combineReducers throwing it away, but rather something else going on with the reduce-reducers implementation.
The warning messages are:
Unexpected keys "reducerB1", "reducerB2" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "reducerA1", "reducerA2". Unexpected keys will be ignored.
Unexpected keys "reducerA1", "reducerA2" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "reducerB1", "reducerB2". Unexpected keys will be ignored.
If I do a production build, the warning disappear (such is the way for many react/redux warnings), but I'd rather them not appear at all.
I've also done some more searching for other libraries and found redux-concatenate-reducers:
const reducer = concatenateReducers([reducerA, reducerB])
This has the same result as flat-combine-reducers so the search continues.
Edit 2:
A few people have made some suggestions now but none have worked so far, so here is a test to help:
import { combineReducers, createStore } from 'redux'
describe('Sample Tests', () => {
const reducerA1 = (state = 0) => state
const reducerA2 = (state = { test: "value1"}) => state
const reducerB1 = (state = [ "value" ]) => state
const reducerB2 = (state = { test: "value2"}) => state
const reducerA = combineReducers({ reducerA1, reducerA2 })
const reducerB = combineReducers({ reducerB1, reducerB2 })
const mergeReducers = (...reducers) => (state, action) => {
return /* your attempt goes here */
}
it('should merge reducers', () => {
const reducer = mergeReducers(reducerA, reducerB)
const store = createStore(reducer)
const state = store.getState()
const expectedState = {
reducerA1: 0,
reducerA2: {
test: "value1"
},
reducerB1: [ "value" ],
reducerB2: {
test: "value2"
}
}
expect(state).to.deep.equal(expectedState)
})
})
The goal is to get this test to pass AND not produce any warnings in the console.
Edit 3:
Added more tests to cover more cases, including handling an action after the initial creation and if the store is created with initial state.
import { combineReducers, createStore } from 'redux'
describe('Sample Tests', () => {
const reducerA1 = (state = 0) => state
const reducerA2 = (state = { test: "valueA" }) => state
const reducerB1 = (state = [ "value" ]) => state
const reducerB2 = (state = {}, action) => action.type == 'ADD_STATE' ? { ...state, test: (state.test || "value") + "B" } : state
const reducerA = combineReducers({ reducerA1, reducerA2 })
const reducerB = combineReducers({ reducerB1, reducerB2 })
// from Javaguru's answer
const mergeReducers = (reducer1, reducer2) => (state, action) => ({
...state,
...reducer1(state, action),
...reducer2(state, action)
})
it('should merge combined reducers', () => {
const reducer = mergeReducers(reducerA, reducerB)
const store = createStore(reducer)
const state = store.getState()
const expectedState = {
reducerA1: 0,
reducerA2: {
test: "valueA"
},
reducerB1: [ "value" ],
reducerB2: {}
}
expect(state).to.deep.equal(expectedState)
})
it('should merge basic reducers', () => {
const reducer = mergeReducers(reducerA2, reducerB2)
const store = createStore(reducer)
const state = store.getState()
const expectedState = {
test: "valueA"
}
expect(state).to.deep.equal(expectedState)
})
it('should merge combined reducers and handle actions', () => {
const reducer = mergeReducers(reducerA, reducerB)
const store = createStore(reducer)
store.dispatch({ type: "ADD_STATE" })
const state = store.getState()
const expectedState = {
reducerA1: 0,
reducerA2: {
test: "valueA"
},
reducerB1: [ "value" ],
reducerB2: {
test: "valueB"
}
}
expect(state).to.deep.equal(expectedState)
})
it('should merge basic reducers and handle actions', () => {
const reducer = mergeReducers(reducerA2, reducerB2)
const store = createStore(reducer)
store.dispatch({ type: "ADD_STATE" })
const state = store.getState()
const expectedState = {
test: "valueAB"
}
expect(state).to.deep.equal(expectedState)
})
it('should merge combined reducers with initial state', () => {
const reducer = mergeReducers(reducerA, reducerB)
const store = createStore(reducer, { reducerA1: 1, reducerB1: [ "other" ] })
const state = store.getState()
const expectedState = {
reducerA1: 1,
reducerA2: {
test: "valueA"
},
reducerB1: [ "other" ],
reducerB2: {}
}
expect(state).to.deep.equal(expectedState)
})
it('should merge basic reducers with initial state', () => {
const reducer = mergeReducers(reducerA2, reducerB2)
const store = createStore(reducer, { test: "valueC" })
const state = store.getState()
const expectedState = {
test: "valueC"
}
expect(state).to.deep.equal(expectedState)
})
it('should merge combined reducers with initial state and handle actions', () => {
const reducer = mergeReducers(reducerA, reducerB)
const store = createStore(reducer, { reducerA1: 1, reducerB1: [ "other" ] })
store.dispatch({ type: "ADD_STATE" })
const state = store.getState()
const expectedState = {
reducerA1: 1,
reducerA2: {
test: "valueA"
},
reducerB1: [ "other" ],
reducerB2: {
test: "valueB"
}
}
expect(state).to.deep.equal(expectedState)
})
it('should merge basic reducers with initial state and handle actions', () => {
const reducer = mergeReducers(reducerA2, reducerB2)
const store = createStore(reducer, { test: "valueC" })
store.dispatch({ type: "ADD_STATE" })
const state = store.getState()
const expectedState = {
test: "valueCB"
}
expect(state).to.deep.equal(expectedState)
})
})
The above mergeReducers implementation passes all the tests, but still producers warnings to the console.
Sample Tests
✓ should merge combined reducers
✓ should merge basic reducers
Unexpected keys "reducerB1", "reducerB2" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "reducerA1", "reducerA2". Unexpected keys will be ignored.
Unexpected keys "reducerA1", "reducerA2" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "reducerB1", "reducerB2". Unexpected keys will be ignored.
✓ should merge combined reducers and handle actions
✓ should merge basic reducers and handle actions
✓ should merge combined reducers with initial state
✓ should merge basic reducers with initial state
✓ should merge combined reducers with initial state and handle actions
✓ should merge basic reducers with initial state and handle actions
It is important to note that the warnings being printed are for the test case immediately after and that combineReducers reducers will only print each unique warning once, so because I'm reusing the reducer between tests, the warnings are only shown for the first test case to produce it (I could combine the reducers in each test to prevent this, but as the criteria I'm looking for it to not produce them at all, I'm happy with this for now).
If you are attempting this, I don't mind if mergeReducers accepts 2 reducers (like above), an array of reducers or an object of reducers (like combineReducers). Actually, I don't mind how it is achieved as long as it doesn't require any changes to the creation of reducerA, reducerB, reducerA1, reducerA1, reducerB1 or reducerB2.
Edit 4:
My current solution is modified from Jason Geomaat's answer.
The idea is to filter the state being provided to the reducer using the keys of previous calls by using the following wrapper:
export const filteredReducer = (reducer) => {
let knownKeys = Object.keys(reducer(undefined, { type: '##FILTER/INIT' }))
return (state, action) => {
let filteredState = state
if (knownKeys.length && state !== undefined) {
filteredState = knownKeys.reduce((current, key) => {
current[key] = state[key];
return current
}, {})
}
let newState = reducer(filteredState, action)
let nextState = state
if (newState !== filteredState) {
knownKeys = Object.keys(newState)
nextState = {
...state,
...newState
}
}
return nextState;
};
}
I merge the result of the filtered reducers using the redux-concatenate-reducers library (could have used flat-combine-reducers but the merge implementation of the former seems a bit more robust). The mergeReducers function looks like:
const mergeReducers = (...reducers) => concatenateReducers(reducers.map((reducer) => filterReducer(reducer))
This is called like so:
const store = createStore(mergeReducers(reducerA, reducerB)
This passes all of the tests and doesn't produce any warnings from reducers created with combineReducers.
The only bit I'm not sure about is where the knownKeys array is being seeded by calling the reducer with an INIT action. It works, but it feels a little dirty. If I don't do this, the only warning that is produced is if the store is created with an initial state (the extra keys are not filtered out when resolving the initial state of the reducer.

Ok, decided to do it for fun, not too much code... This will wrap a reducer and only provide it with keys that it has returned itself.
// don't provide keys to reducers that don't supply them
const filterReducer = (reducer) => {
let lastState = undefined;
return (state, action) => {
if (lastState === undefined || state == undefined) {
lastState = reducer(state, action);
return lastState;
}
var filteredState = {};
Object.keys(lastState).forEach( (key) => {
filteredState[key] = state[key];
});
var newState = reducer(filteredState, action);
lastState = newState;
return newState;
};
}
In your tests:
const reducerA = filterReducer(combineReducers({ reducerA1, reducerA2 }))
const reducerB = filterReducer(combineReducers({ reducerB1, reducerB2 }))
NOTE: This does break with the idea that the reducer will always provide the same output given the same inputs. It would probably be better to accept the list of keys when creating the reducer:
const filterReducer2 = (reducer, keys) => {
let lastState = undefined;
return (state, action) => {
if (lastState === undefined || state == undefined) {
lastState = reducer(state, action);
return lastState;
}
var filteredState = {};
keys.forEach( (key) => {
filteredState[key] = state[key];
});
return lastState = reducer(filteredState, action);
};
}
const reducerA = filterReducer2(
combineReducers({ reducerA1, reducerA2 }),
['reducerA1', 'reducerA2'])
const reducerB = filterReducer2(
combineReducers({ reducerB1, reducerB2 }),
['reducerB1', 'reducerB2'])

OK, although the problem was already solved in the meantime, I just wanted to share what solution I came up:
import { ActionTypes } from 'redux/lib/createStore'
const mergeReducers = (...reducers) => {
const filter = (state, keys) => (
state !== undefined && keys.length ?
keys.reduce((result, key) => {
result[key] = state[key];
return result;
}, {}) :
state
);
let mapping = null;
return (state, action) => {
if (action && action.type == ActionTypes.INIT) {
// Create the mapping information ..
mapping = reducers.map(
reducer => Object.keys(reducer(undefined, action))
);
}
return reducers.reduce((next, reducer, idx) => {
const filteredState = filter(next, mapping[idx]);
const resultingState = reducer(filteredState, action);
return filteredState !== resultingState ?
{...next, ...resultingState} :
next;
}, state);
};
};
Previous Answer:
In order to chain an array of reducers, the following function can be used:
const combineFlat = (reducers) => (state, action) => reducers.reduce((newState, reducer) => reducer(newState, action), state));
In order to combine multiple reducers, simply use it as follows:
const combinedAB = combineFlat([reducerA, reducerB]);

Solution for those using Immutable
The solutions above don't handle immutable stores, which is what I needed when I stumbled upon this question. Here is a solution I came up with, hopefully it can help someone else out.
import { fromJS, Map } from 'immutable';
import { combineReducers } from 'redux-immutable';
const flatCombineReducers = reducers => {
return (previousState, action) => {
if (!previousState) {
return reducers.reduce(
(state = {}, reducer) =>
fromJS({ ...fromJS(state).toJS(), ...reducer(previousState, action).toJS() }),
{},
);
}
const combinedReducers = combineReducers(reducers);
const combinedPreviousState = fromJS(
reducers.reduce(
(accumulatedPreviousStateDictionary, reducer, reducerIndex) => ({
...accumulatedPreviousStateDictionary,
[reducerIndex]: previousState,
}),
{},
),
);
const combinedState = combinedReducers(combinedPreviousState, action).toJS();
const isStateEqualToPreviousState = state =>
Object.values(combinedPreviousState.toJS()).filter(previousStateForComparison =>
Map(fromJS(previousStateForComparison)).equals(Map(fromJS(state))),
).length > 0;
const newState = Object.values(combinedState).reduce(
(accumulatedState, state) =>
isStateEqualToPreviousState(state)
? {
...state,
...accumulatedState,
}
: {
...accumulatedState,
...state,
},
{},
);
return fromJS(newState);
};
};
const mergeReducers = (...reducers) => flatCombineReducers(reducers);
export default mergeReducers;
This is then called this way:
mergeReducers(reducerA, reducerB)
It produces no errors. I am basically returning the flattened output of the redux-immutable combineReducers function.
I have also released this as an npm package here: redux-immutable-merge-reducers.

There is also combinedReduction reducer utility
const reducer = combinedReduction(
migrations.reducer,
{
session: session.reducer,
entities: {
users: users.reducer,
},
},
);

Related

How to mutate state with redux toolkit

The data is provided dynamically and I don't know its value to assign it in initialState. It causes me a bug that I can't deal with.
How to update the object in the state if there was no object in initialState?
ERROR
filteringSlice.ts:12
Uncaught TypeError: Cannot read properties of undefined (reading 'products')
at addFilter (filteringSlice.ts:12:1)
at createReducer.ts:280:1
at produce (immerClass.ts:94:1)
at createReducer.ts:279:1
at Array.reduce (<anonymous>)
at reducer (createReducer.ts:246:1)
at reducer (createSlice.ts:325:1)
at combination (redux.js:560:1)
at k (<anonymous>:2235:16)
at D (<anonymous>:2251:13)
CALL ACTION
onChange={(selectedValue) => {
dispatch(
addFilter({
products: { category__name: { filter: selectedValue } },
})
);
}}
SLICE
import { createSlice } from "#reduxjs/toolkit";
const initialState = {} as any;
const filteringSlice = createSlice({
name: "filtering",
initialState,
reducers: {
addFilter: (state, action) => {
const key = Object.keys(action.payload)[0];
const key2 = Object.keys(action.payload[key])[0];
const values = Object.values(action.payload[key])[0];
//#ts-ignore
state.filters[key][key2] = { ...state.filters[key][key2], ...values };
},
},
});
const { reducer, actions } = filteringSlice;
export const { addFilter } = actions;
export default reducer;
So your state is an empty object at first:
const initialState = {} as any;
But then you're accessing at as if it had a more deeply nested structure:
state.filters[key][key2] = ...
That doesn't work because there is no state['filters'], and no state['filters']['products'], etc.
You need to create every level of this nesting manually for this to work (or think about a better, flatter structure for your state):
/*
action.payload = {
products: {
category__name: {
filter: selectedValue
}
}
}
*/
const key = Object.keys(action.payload)[0]; // 'products'
const key2 = Object.keys(action.payload[key])[0]; // 'category__name'
const values = Object.values(action.payload[key])[0]; // selectedValue
if (!state.filters) {
state.filters = {};
}
if (!state.filters[key]) {
state.filters[key] = {};
}
if (!state.filters[key][key2]) {
state.filters[key][key2] = {};
}
state.filters[key][key2] = { ...state.filters[key][key2], ...values };

React - Update non stateful data inside reducer

I am implementing a context that manages all the messages of a conversation.
To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).
This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.
export function MessagesProvider({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, initialState);
const sectionsRef = useMemo(() => new Map(), []);
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift));
};
const addMessage = (message) => addMessages([message]);
const deleteMessage = (messageId) => {
dispatch(actionCreators.deleteMessage(messageId));
};
const value = useMemo(() => ({
messages,
addMessages,
deleteMessage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [messages]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.
Is it correct to pass it as a payload to my reducer actions?
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};
To simplify my problem, imagine this is the real code:
//
// Reducer action
//
function reducerAction(state, messages, sectionsRef, title) {
state.push(...messages);
sectionsRef.set(title, state.length - 1);
}
//
// Context code
//
const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();
reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");
console.log(state);
console.log(sectionsRef.get("second section"));
I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?
Is it correct to pass it as a payload to my reducer actions?
No: reducers must be pure functions.
Redux describes reducers using a short list which I think is very useful:
Rules of Reducers​
We said earlier that reducers must always follow some special rules:
They should only calculate the new state value based on the state and action arguments
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
They must not do any asynchronous logic or other "side effects"
The second and third items together describe pure functions, and the first one is just a Redux-specific convention.
In your example, you are violating two rules of pure functions:
mutating state with state.push(...messages) (rather than creating a new array and returning it), and
performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)
Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).
Here's how you can refactor your code to achieve your goal:
Keep the reducer data pure:
// store.js
export function messagesReduer (messages, action) {
switch (action.type) {
case 'ADD': {
const {payload, unshift} = action;
return unshift ? [...payload, ...messages] : [...messages, ...payload];
}
case 'DELETE': {
const {payload} = action;
return messages.filter(m => m.id !== payload);
}
}
}
export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});
export const sections = new Map();
Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:
// MessagesContext.jsx
import {
createContext,
useCallback,
useMemo,
useReducer,
} from 'react';
import {
creators,
messagesReduer,
sections,
} from './store';
export const MessagesContext = createContext();
export function MessagesProvider ({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, []);
const addMessages = useCallback((title, messages, unshift = false) => {
dispatch(creators.add(messages, unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const addMessage = useCallback((title, message, unshift = false) => {
dispatch(creators.add([message], unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const deleteMessage = useCallback((id) => {
dispatch(creators.delete(id));
}, [creators.delete, dispatch]);
const value = useMemo(() => ({
addMessage,
addMessages,
deleteMessage,
messages,
sections,
}), [
addMessage,
addMessages,
deleteMessage,
messages,
sections,
]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
Use the context:
// App.jsx
import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';
function Messages () {
const {
// addMessage,
// addMessages,
// deleteMessage,
messages,
// sections,
} = useContext(MessagesContext);
return (
<ul>
{
messages.map(({id}, index) => (
<li key={id}>Message no. {index + 1}: ID {id}</li>
))
}
</ul>
);
}
export function App () {
return (
<MessagesProvider>
<Messages />
</MessagesProvider>
);
}
Additional notes:
Make sure your dependency lists (e.g. in useMemo, etc.) are exhaustive. Those lint warnings are there to help prevent you from making mistakes. In general, you should never need to suppress them.

Preserve state value on client side navigation - NextJs - Next-Redux-Wrapper

So I am trying to fix the hydrating issue I am facing when using wrapper.getServerSideProps. When I reroute with the current setup the store is cleared out and then the new data is added, which results in a white page since a lot of important data is no longer there (i.e, translations and cms data).
Screenshot from redux-dev-tools Hydrate action diff:
Screenshot is taken after routing from the homepage to a productpage, so that there was an existing store. Everything is reset to the initial app state.
What I am trying to do
In the store.js I create the store and foresee a reducer to handle the Hydrate call. The downside of this approach is that the payload will always be a new store object since it is called on the server. I was thinking to check the difference between the 2 json's and then only apply the difference instead of the whole initial store.
Get the difference between the client and server state.
Make the next state, overwrite clientstate with patched serverstate so this includes, updated state from hydrate and the existing client state.
Currently results in a white page.
You can see the reducer code below in the store.js
//store.js
import combinedReducer from './reducer';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const clientState = { ...state };
const serverState = { ...action.payload };
if (state) {
// preserve state value on client side navigation
// Get the difference between the client and server state.
const diff = jsondiffpatch.diff(clientState, serverState);
if (diff !== undefined) {
// If there is a diff patch the serverState, with the existing diff
jsondiffpatch.patch(serverState, diff);
}
}
// Make next state, overwrite clientstate with patched serverstate
const nextState = {
...clientState,
...serverState,
};
// Result, blank page.
return nextState;
}
return combinedReducer(state, action);
};
export const makeStore = () => {
const cookies = new Cookies();
const client = new ApiClient(null, cookies);
const middleware = [
createMiddleware(client),
thunkMiddleware.withExtraArgument(cookies),
];
return createStore(reducer, bindMiddleware(middleware));
};
const wrapper = createWrapper(makeStore);
export default wrapper;
//_app.jsx
const App = (props) => {
const { Component, pageProps, router } = props;
return (
<AppComponent cookies={cookies} locale={router.locale} location={router}>
<Component {...pageProps} />
</AppComponent>
);
};
App.getInitialProps = async ({ Component, ctx }) => {
return {
pageProps: {
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
},
};
};
App.propTypes = {
Component: PropTypes.objectOf(PropTypes.any).isRequired,
pageProps: PropTypes.func,
router: PropTypes.objectOf(PropTypes.any).isRequired,
};
App.defaultProps = {
pageProps: () => null,
};
export default wrapper.withRedux(withRouter(App));
// Product page
export const getServerSideProps = wrapper.getServerSideProps(
async ({ query, store: { dispatch } }) => {
const productCode = query.id?.split('-', 1).toString();
await dispatch(getProductByCode(productCode, true));
});
const PDP = () => {
const { product } = useSelector((state) => state.product);
return (
<PageLayout>
<main>
<h1>{product?.name}</h1>
<div
className="description"
dangerouslySetInnerHTML={{ __html: product?.description }}
/>
</main>
</PageLayout>
);
};
export default PDP;
Oke, so I solved my issue through not overthinking the concept. Went back to the drawing board and made a simple solution.
Came to the conclusion that there are only a few state objects that need to persist during client navigation.
I only had to make a change to my i18n, to make it dynamic since we fetch translations on page basis.
This is the final reducer for anyone that might, in the future run into a similar problem.
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const clientState = { ...state };
const serverState = { ...action.payload };
const nextState = { ...clientState, ...serverState };
const locale = nextState.i18n.defaultLocale || config.i18n.defaultLocale;
const nextI18n = {
...state.i18n,
locale,
messages: {
[locale]: {
...state.i18n.messages[locale],
...nextState.i18n.messages[locale],
},
},
loadedGroups: {
...state.i18n.loadedGroups,
...nextState.i18n.loadedGroups,
},
};
if (state) {
nextState.i18n = nextI18n;
nextState.configuration.webConfig = state.configuration.webConfig;
nextState.category.navigation = state.category.navigation;
}
return nextState;
}
return combinedReducer(state, action);
};

Access the state of my redux app using redux hooks

I am migrating my component from a class component to a functional component using hooks. I need to access the states with useSelector by triggering an action when the state mounts. Below is what I have thus far. What am I doing wrong? Also when I log users to the console I get the whole initial state ie { isUpdated: false, users: {}}; instead of just users
reducers.js
const initialState = {
isUpdated: false,
users: {},
};
const generateUsersObject = array => array.reduce((obj, item) => {
const { id } = item;
obj[id] = item;
return obj;
}, {});
export default (state = { ...initialState }, action) => {
switch (action.type) {
case UPDATE_USERS_LIST: {
return {
...state,
users: generateUsersObject(dataSource),
};
}
//...
default:
return state;
}
};
action.js
export const updateUsersList = () => ({
type: UPDATE_USERS_LIST,
});
the component hooks I am using
const users = useSelector(state => state.users);
const isUpdated = useSelector(state => state.isUpdated);
const dispatch = useDispatch();
useEffect(() => {
const { updateUsersList } = actions;
dispatch(updateUsersList());
}, []);
first, it will be easier to help if the index/store etc will be copied as well. (did u used thunk?)
second, your action miss "dispatch" magic word -
export const updateUsersList = () =>
return (dispatch, getState) => dispatch({
type: UPDATE_USERS_LIST
});
it is highly suggested to wrap this code with { try } syntax and be able to catch an error if happened
third, and it might help with the console.log(users) error -
there is no need in { ... } at the reducer,
state = intialState
should be enough. this line it is just for the first run of the store.
and I don't understand where { dataSource } comes from.

Higher order reducer applied on mutiple reducers

I m learning deeper redux and I m having some trouble dealing with higher order reducers.
I m trying to understand how it works using a simple example of pagination.
NB : The below code is just a quick example of redux in nodejs context, without transpilation and good practices, and thus, I don't have access to spread / destruc operator, so I m using it statefully, while it's not a good practice at all, and I know that
So, let's imagine that I have a paginable higher order reducer :
const paginable = (reducer, options) => {
const PAGE_OFFSET = options.limit;
const ATTRIBUTE_TO_SLICE = options.attr;
const initialState = {
all: reducer(undefined, {}),
displayed: [],
limit: PAGE_OFFSET,
currentPage: 1
};
const _actionHandler = {
'CHANGE_PAGE': (state, newPage) => ({all: state.all, displayed: state.displayed, currentPage: newPage, limit: PAGE_OFFSET}),
'CHANGE_DISPLAYED': state => ({
all: state.all, currentPage: state.currentPage, limit: PAGE_OFFSET,
displayed: state.all[ATTRIBUTE_TO_SLICE].slice((state.currentPage - 1) * PAGE_OFFSET,
state.currentPage * PAGE_OFFSET)
})
};
return (state = initialState, action) => {
const handler = _actionHandler[action.type];
if (handler) {
return handler(state, action.payload);
}
const newAll = reducer(state.all, action);
state.all = newAll;
return state;
};
};
module.exports = paginable;
That I want to apply on these two reducers :
const _actionHandler = {
'ADD': (state, item) => ({list: [...state.list, item]})
};
const initialState = {
list: ['a', 'b', 'c', 'd', 'e']
};
const listReducer = (state = initialState, action) => {
const handler = _actionHandler[action.type];
return handler ? handler(state, action.payload) : state;
};
module.exports = listReducer;
and
const initialState = {
arr: ['z', 'x', 'y', 'b', 'b', 'c', 'd']
};
const arrayReducer = (state = initialState) => {
return state;
};
module.exports = arrayReducer;
I create my store as following :
const redux = require('redux');
const listReducer = require('./reducer/list');
const arrayReducer = require('./reducer/arrayOfX');
const paginable = require('./reducer/paginable');
const reducers = redux.combineReducers({
list: paginable(listReducer, {limit: 2, attr: 'list'}),
arr: paginable(arrayReducer, {limit: 3, attr: 'arr'})
});
const store = redux.createStore(reducers);
My problem now, is that each time I will dispatch an action like CHANGE_PAGE or CHANGE_DISPLAYED, it always will be handled by the two reducers arr and list, that I don't want.
I had in mind to create new actions like CHANGE_DISPLAYED_LIST and CHANGE_DISPLAYED_ARRAY but it would force me to manage more actions in the paginable reducer that I absolutely dont want to... I m probably missing something important out there.
Any suggestions ?
You dont need 2 reducers for this actually. A single Higher order reducer can do the job.
We can pass the type to the parent wrapper and return a function from it. This creates 2 entries in your state.
So, lets create the higher order reducer first:-
const initialState = {
all: {},
displayed: [],
limit: PAGE_OFFSET,
currentPage: 1
};
export default function wrapper(type) {
return function(state=initialState,action) {
//using es6 literals to concatenate the string
case `CHANGE_DISPLAYED_${type}`:
// update your state
case `CHANGE_PAGE_${type}`:
// update your state
}
}
Now, call the reducers in following way
const indexReducer = combineReducers({
"arrayType": wrapper("array"),
"listType" : wrapper("list")
})
For more info you can check out for reusing reducer logic here.
Let me know if you face any issues.

Categories

Resources