My store consists of an array of objects as such:
const INIT_STATE = {
users:[],
contacts : []
};
And i am attempting to change mutate the store array like this:
const App = (state = INIT_STATE, action) => {
switch (action.type) {
case BLOCK_CONTACT:
state.contacts.map((contact, index) => {
if (contact._id === action.payload) {
state.contacts[index].status = "blocked;
return {
...state
};
}
return {
...state
};
})
return {
...state
};
}
}
But my store value does not change.
When i log the state value after the if statement, i get the correct state values but my state is not being updated. I think it might have something to do with my return statement but at this point i have tried different variations with no luck.
Any help will be appreciated.
Also if your app is in the start of the way, you can read this article about jotai state management (Jotai)
I figured it out. Incase anyone else that might need help on this, here is the code...
const App = (state = INIT_STATE, action) => {
switch (action.type) {
case BLOCK_CONTACT:
var list = state.contacts.map(contact => {
var blockList = {
...contact
};
if (contact._id === action.payload) {
blockList.status = 1;
}
return blockList;
})
return {
...state,
contacts: list
};
}
}
I would like to merge two reducers, the first being created as a generic one and the second one would be more specific to it's own state. Both these reducers would not handle the same cases. Merging these would only result in default case being duplicated, the default case always returning the default state anyways. This would help as I would only test the generic one once.
In case you were thinking about reduceReducers or combineReducers, that would not work since I have many "special" reducers with every one of them having the same action type to handle and all of those reducers have a different part of the state to modify.
const initialState = {
byId : {},
ids: []
}
const dogsReducer = ({ dogs: state = initialState, ...restOfState }, action) => {
switch (action.type) {
case INITIALIZE:
return {
byId : _.keyBy(state.dogs, 'id'),
ids: state.map(({id}) => id)
}
case RESET:
return initialState
case SPECIFIC_DOG_ACTION:
...
default:
return state
}
}
const catsReducer = ({ cats: state = initialState, ...restOfState}, action) => {
switch (action.type) {
case INITIALIZE:
return {
byId : _.keyBy(state, 'id'),
ids: state.map(({id}) => id)
}
case RESET:
return initialState
case SPECIFIC_CAT_ACTION:
...
default:
return state
}
}
I want to isolate the following cases : INITIALIZE and RESET in a generic switch/case function or a generic reducer, so I would only have to test those cases once and not in every reducer. There would be more generic cases in the future, that's why I want to avoid repetition.
This is the expected result :
const genericReducer = (state = initialState, action) => {
switch (action.type) {
case INITIALIZE:
return {
byId : _.keyBy(state.dogs, 'id'),
ids: state.map(({id}) => id)
}
case RESET:
return initialState
default:
return state
}
}
const dogsReducer = ({ dogs: state = initialState, ...restOfState }, action) => {
switch (action.type) {
case SPECIFIC_DOG_ACTION:
...
default:
return state
}
}
const catsReducer = ({ cats: state = initialState, ...restOfState}, action) => {
switch (action.type) {
case SPECIFIC_CAT_ACTION:
...
default:
return state
}
}
const finalCatsReducer = mergeReducers(catsReducer, genericReducer)
const finalDogsReducer = mergeReducers(dogsReducer, genericReducer)
I could imagine using the following however I want to say that would have a similar effect to just putting them all flat. The only added benefit I can see is that the specific switch cases won't be verified unless the general cases fail.
const genericSwitch = type => {
switch (type) {
case 1:
do something x
default: //specificSwitch
switch (type) {
case 2:
do something y
default:
return z
}
}
}
The simplest solution is to wrap into the upper method:
const combinedSwitch = type => {
const result = genericSwitch(type);
return result === z ? specificSwitch(type) : result;
}
const genericSwitch = type => {
switch (type) {
case 1:
do something x
default:
return z
}
}
const specificSwitch = type => {
switch (type) {
case 2:
do something y
default:
return z
}
}
I have a component which takes data from mapStateToProps() method. Component's code is:
handleClick = () => {
if (this.props.data.status) {
this.props.changeIpStatus(index, !this.props.data.status);
} else {
this.props.changeIpStatus(index, !this.props.data.status);
}
}
render() {
if (this.props.data.status) {
this.switchClasses = `switcher blocked`;
this.dotClasses = `dot blocked`;
} else {
this.switchClasses = `switcher`;
this.dotClasses = `dot`;
}
return (
<div className="block">
<div onClick={this.handleClick} className={this.switchClasses}>
<div className={this.dotClasses}></div>
</div>
</div>
)
}
}
My Redux connection looks like:
const mapStateToProps = (state) => ({
data: state.ipTable.data.clicks,
})
const mapDispatchToProps = (dispatch) => {
return {
changeIpStatus: (index, status) => {
return dispatch(changeIpStatus(index, status));
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BlockSwitcher)
When I click switcher it should re-render because the data is changed. I see that the data is changed through my console log. But it doesn't invoke re-render. Why? My component have mapStateToProps with data that changing and action import is correct (checked).
UPDATE:
This is my reducer:
const initialState = {
data: {}
}
const ipReducer = (state = initialState, action) => {
switch (action.type) {
case `SET_CLICKS`:
return {
...state,
data: action.data
}
case `CHANGE_IP_STATUS`:
let newState = Object.assign({}, state);
newState.data.clicks[action.index].status = action.status;
return newState;
default: return state;
}
}
export default ipReducer;
You can use JSON.parse(JSON.stringify(...)) method but be aware of that if your state includes a non-serializable property then you lose it.
Here is an alternative approach. You can see this method more frequently.
// map the clicks, if index match return the new one with the new status
// if the index does not match just return the current click
const newClicks = state.data.clicks.map((click, index) => {
if (index !== action.index) return click;
return { ...click, status: action.status };
});
// Here, set your new state with your new clicks without mutating the original one
const newState = { ...state, data: { ...state.data, clicks: newClicks } };
return newState;
The second alternative would be like that. Without mapping all the clicks we can use Object.assign for the clicks mutation.
const newClicks = Object.assign([], state.data.clicks, {
[action.index]: { ...state.data.clicks[action.index], status: action.status }
});
const newState = { ...state, data: { ...state.data, clicks: newClicks } };
return newState;
The problem was with deep copy of an object. In JavaScrip for copying object without any reference between them we have to use, for example:
let newState = JSON.parse(JSON.stringify(state));
not this:
let newState = Object.assign({}, state); // <-- this is do not return a standalone new object. Do not use it in your reducer.
Thanks to #kind user!
P.S This is an article with examples why Object.assign() do not work in this case.
Normally we will do following thing to define a set redux action&reducer with redux-actions library
export const {
open,
close,
send
} = createActions({
OPEN: payload => ({
payload
}),
CLOSE: payload => ({
payload
}),
SEND: payload => ({
payload,
})
});
export const combinedReducer = createActions({
[open]: (state, action) => { /*you do someting*/ },
[close]: (state, action) => { /*you do someting*/ },
[send]: (state, action) => { /*you do someting*/ }
});
/*in some where , we are going to handle the a triggered action type respectively in a switch statement.
but we have to use string concatenation to make the switch express strict equal pass , any graceful solution ? */
switch (action.type) {
case open + "":
//do someting
break;
case close + "":
//do someting
break;
case send + "":
//do someting
break;
}
Variables open , close , send generated above are actually function type and their toString() are overrided by redux-action lib to export a string like "OPEN","CLOSE","send"
however if we would like to reuse these action type inside switch statement, we have to concatenate with '' in such an awkward way just to pass the switch express.
Are any graceful ways out there to avoid this kind of stupid code when dealing with switch statement parse which enforces strict equal compare ===
thanks in advance.
I may have misunderstood your question, so my previous answer might not get close to what you want.
Another technique I've used, employing both createActions() and switch/case looked like this:
import { createAction } from 'redux-actions'
// your action creators
export const myAction = createAction('ACTION_TYPE')
// your store slice/reducers
const defaultSliceState = {}
export const slice = (state = defaultSliceState, action) => {
switch(action.type) {
case myAction().type:
return Object.assign({}, state, { someValue: action.payload })
}
}
UPDATE
If you need to enforce strict equality, and don't feel so concerned about following typical/cute/neat patterns, you can use this:
// your store slice/reducers
const ACTION_TYPE_1 = 'ACTION_TYPE_1'
const ACTION_TYPE_2 = 'ACTION_TYPE_2'
// add your action creators here: const myAction = createAction(ACTION_TYPE)
// ...
const defaultSliceState = {}
export const slice = (state = defaultSliceState, action) => {
switch(true) {
case action.type === ACTION_TYPE_1:
return Object.assign({}, state, { someValue: action.payload })
case action.type === ACTION_TYPE_2:
return Object.assign({}, state, { otherValue: doSomething(action.payload) })
}
}
... but you don't even need to do this if your ONLY concern is strict equality. By a little experiment, I found switch/case already uses strict equality checks.
function match (arg) {
switch(arg) {
case 1: return "matched num"
case '1': return "matched str"
}
}
match(1) // -> matched num
match('1') // -> matched str
One way uses the handleAction or handleActions functions, also provided by redux-actions. Rather than write a long switch statement with [action.type] cases, you instead map your action creators to reducers using these functions.
This resembles how I like to do it:
import { createAction, handleActions } from 'redux-actions'
// your action creators
export const myAction = createAction('ACTION_TYPE`)
// your reducer (this example doesn't do anything useful)
const myReducer = (state, action) => Object.assign({}, state, { someValue: action.payload })
const sliceDefaultValue = {}
export const slice = handleActions({
[myAction]: myReducer
}, sliceDefaultValue)
The handleAction function documentation lives here: https://redux-actions.js.org/api/handleaction.
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,
},
},
);