How to set state within a reducer - javascript

I have an array of product objects inside my reducer and I have an empty array of brand as well. I want to add all the unique brands from my products object array into my brands array in my reducer, is there any way I can do that?
My Reducer:
import * as actionTypes from './shop-types';
const INITIAL_STATE = {
products: [
{
id: 1,
brand:"DNMX"
},
{
id: 2,
brand: "Aeropostale",
},
{
id: 3,
brand: "AJIO",
},
{
id: 4,
brand: "Nike",
},
],
cart: [],
brands: [], //<---- I want to add all the unique brands inside this array
currentProduct: null,
};
const shopReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.ADD_TO_CART:
const product = state.products.find(
(product) => product.id === action.payload.id
);
if (product !== null) {
return {
...state,
cart: state.cart.concat(product),
};
} else {
return null;
}
default:
return state;
}
};
export default shopReducer;

Brands is essentially derived data, meaning it's based off, or reliant on other data. Because of this, you don't actually need to set it in state, and instead rather, just derive it.
I'd normally recommend using Redux Toolkit as it's far simpler, but as you're using old-school Redux, I'd recommend using a library called Reselect. It's a library for creating memoized selectors that you can consume in your component.
For your example, I'd try something like:
// import the createSelector function
// we'll use to make a selector
import { createSelector } from "reselect"
// create a "getter". this is just a simplified
// way of accessing state
const selectBrands = (state) => state.products
// create the selector. this particular selector
// just looks at `products` in state (from your
// getter), and filters out duplicate values
// and returns a unique list
const uniqueBrands = createSelector(selectBrands, (items) =>
items.filter(
(item, idx, arr) =>
arr.findIndex((brand) => brand.name === item.name) === idx
)
)
Then in your component code, you can access this in mapStateToProps:
const mapStateToProps = (state) => ({
uniqueBrands: uniqueBrands(state),
})
This is currently untested, but should be what you're looking for.

It's not really clear if you mean unique brands by cart or products, but it shouldn't change the patterns you'll use to solve this.
First assuming the product list isn't changing, you can simply add them as part of the initial state.
const ALL_PRODUCTS = [
{ id: 1, brand:"DNMX" },
{ id: 2, brand: "Aeropostale" },
{ id: 3, brand: "AJIO" },
{ id: 4, brand: "Nike" }
];
const distinct = (array) => Array.from(new Set((array) => array.map((x) => x.brand)).entries());
const ALL_BRANDS = distinct(ALL_PRODUCTS.map((x) => x.brand));
const INITIAL_STATE = {
products: ALL_PRODUCTS,
cart: [],
brands: ALL_BRANDS,
currentProduct: null,
};
If you will have an action that will add a new products and the brands have to reflect that you just apply the above logic during the state updates for the products.
const reducer = (state = INITIAL_STATE, action) => {
switch(action.type) {
case (action.type === "ADD_PRODUCT") {
const products = [...state.products, action.product];
return {
...state,
products,
brands: distinct(products.map((x) => x.brand))
}
}
}
return state;
};
Now for the idiomatic way. You might note that brands can be considered derived from the products collection. Meaning we probably dont even need it in state since we can use something called a selector to create derived values from our state which can greatly simplify our reducer/structure logic.
// we dont need brands in here anymore, we can derive.
const INITIAL_STATE = {
products: ALL_PRODUCTS,
cart: [],
currentProduct: null;
};
const selectAllBrands = (state) => {
return distinct(state.products.map((x) => x.brand))
};
Now when we add/remove/edit new products we no longer need to update the brand slice. It will be derived from the current state of products. On top of all of that, you can compose selectors just like you can with reducers to get some really complex logic without mucking up your store.
const selectCart = (state) => state.cart;
const selectAllBrands = (state) => {...see above}
const selectTopBrandInCart = (state) => {
const cart = selectCart(state);
const brands = selectBrands(brands);
// find most popular brand in cart and return it.
};
I would highly recommend you check out reselect to help build composable and performant selectors.

Related

How do I update the quantity without duplicate in redux?

I have the add and Remove items state and it sends to the cart. Im my cart, when I add, it duplicates. Do I need to create a reducer for this increment so it can update from 1 to 2 instead of duplicating it?
const INITIAL_STATE = []
export const addItem = createAction('ADD_ITEM')
export const increment = createAction('INCREMENT')
export const removeItem = createAction('REMOVE_ITEM')
export default createReducer(INITIAL_STATE, {
[addItem.type]: (state, action) => [...state, action.payload],
[removeItem.type]: (state, action) => state.filter(item => item._id !== action.payload._id)
})
Here is my products:
const INITIAL_STATE = []
export const addProducts = createAction('ADD_PRODUCTS')
export const addProduct = createAction('ADD_PRODUCT')
export default createReducer(INITIAL_STATE, {
[addProducts.type]: (state , action) => [...state, action.payload],
[addProduct.type]: (state, action) => [...action.payload]
})
My reducer:
export default configureStore({
reducer: {
products: productsReducer,
cart: cartReducer
}
})
It would be necessary that your reducer verify if the item ID exists or not. Case exists, update the item count. Instead of, create a new item with count equal to 1:
export default createReducer(INITIAL_STATE, {
[addItem.type]: (state, action) => {
const pos = state.map((i) => i._id).indexOf(action.payload._id);
if (pos !== -1) {
state[pos] = { ...action.payload, count: state[pos].count + 1 };
} else {
state.push({ ...action.payload, count: 1 });
}
},
[removeItem.type]: (state, action) =>
state.filter((item) => item._id !== action.payload._id),
});
Quantity
Right now your cart state is saving an array of all of the items that are in the cart. There is no quantity property anywhere. One thing that you could do is add a quantity: 1 property to the item object when you add that item to the cart. But...
Normalizing State
You don't really need to save the while item object because you already have that information in products. All that you really need to know is the id of each item in the cart and its quantity. The most logical data structure for that is a dictionary object where the keys are the item ids and the values are the corresponding quantities.
Since you only need the id I would recommend that your action creators addItem, removeItem and increment should just be a function of the id instead of the whole item. This means that you would call them like dispatch(addItem(5)) and your action.payload would just be the id. The addProduct and addProducts actions still need the whole item object.
An array of products is fine and you can always call state.products.find(item => item._id === someId) to find a product by its id. But a keyed object is better! You want to have the keys be the item ids and the values be the corresponding objects. Redux Toolkit has built-in support for this structure with the createEntityAdapter helper.
Create Slice
There's not any problem with defining your actions though createAction, but it's not needed. You can replace createAction and createReducer with createSlice which combines the functionality of both.
Here's another way of writing your products:
import { createSlice, createEntityAdapter } from "#reduxjs/toolkit";
const productsAdapter = createEntityAdapter({
selectId: (item) => item._id
});
const productsSlice = createSlice({
// this becomes the prefix for the action names
name: "products",
// use the initial state from the adapter
initialState: productsAdapter.getInitialState(),
// your case reducers will create actions automatically
reducers: {
// you can just pass functions from the adapter
addProduct: productsAdapter.addOne,
addProducts: productsAdapter.addMany
// you can also add delete and update actions easily
}
});
export default productsSlice.reducer;
export const { addProduct, addProducts } = productsSlice.actions;
// the adapter also creates selectors
const productSelectors = productsAdapter.getSelectors(
// you need to provide the location of the products relative to the root state
(state) => state.products
);
// you can export the selectors individually
export const {
selectById: selectProductById,
selectAll: selectAllProducts
} = productSelectors;
And your cart:
import {createSlice, createSelector} from "#reduxjs/toolkit";
const cartSlice = createSlice({
name: 'cart',
// an empty dictionary object
initialState: {},
reducers: {
addItem: (state, action) => {
const id = action.payload;
// add to the state with a quantity of 1
state[id] = 1;
// you might want to see if if already exists before adding
},
removeItem: (state, action) => {
const id = action.payload;
// you can just use the delete keyword to remove it from the draft
delete state[id];
},
increment: (state, action) => {
const id = action.payload;
// if you KNOW that the item is already in the state then you can do this
state[id]++;
// but it's safer to do this
// state[id] = (state[id] || 0) + 1
}
}
})
export default cartSlice.reducer;
export const {addItem, removeItem, increment} = cartSlice.actions;
// you can select the data in any format
export const selectCartItems = createSelector(
// only re-calculate when this value changes
state => state.cart,
// reformat into an an array of objects with propeties id and quantity
(cart) => Object.entries(cart).map(([id, quantity]) => ({id, quantity}))
)
// select the quantity for a particular item by id
export const selectQuantityById = (state, id) => state.cart[id]
// you can combine the ids with the products, but
// I actually recommend that you just return the ids and get the
// product data from a Product component like <Product id={5} quantity={2}/>
export const selectCartProducts = createSelector(
// has two input selectors
state => state.cart,
state => state.products,
// combine and reformat into an array of objects
(cart, products) => Object.keys(cart).map(id => ({
// all properties of the product
...products.entries[id],
// added quantity property
quantity: cart[id],
}))
)

adding an object element to an immutable array(javascript) [duplicate]

How do I add elements in my array arr[] of redux state in reducer?
I am doing this-
import {ADD_ITEM} from '../Actions/UserActions'
const initialUserState = {
arr:[]
}
export default function userState(state = initialUserState, action)
{
console.log(arr);
switch (action.type)
{
case ADD_ITEM:
return {
...state,
arr: state.arr.push([action.newItem])
}
default:
return state
}
}
Two different options to add item to an array without mutation
case ADD_ITEM :
return {
...state,
arr: [...state.arr, action.newItem]
}
OR
case ADD_ITEM :
return {
...state,
arr: state.arr.concat(action.newItem)
}
push does not return the array, but the length of it (docs), so what you are doing is replacing the array with its length, losing the only reference to it that you had. Try this:
import {ADD_ITEM} from '../Actions/UserActions'
const initialUserState = {
arr:[]
}
export default function userState(state = initialUserState, action){
console.log(arr);
switch (action.type){
case ADD_ITEM :
return {
...state,
arr:[...state.arr, action.newItem]
}
default:return state
}
}
If you need to insert into a specific position in the array, you can do this:
case ADD_ITEM :
return {
...state,
arr: [
...state.arr.slice(0, action.pos),
action.newItem,
...state.arr.slice(action.pos),
],
}
Since this question gets a lot of exposure:
If you are looking for the answer to this question, there is a good chance that you are following a very outdated Redux tutorial.
The official recommendation (since 2019) is to use the official Redux Toolkit to write modern Redux code.
Among other things, that will eliminate string action constants and generate action creators for you.
It will also employ methods that allow you to just write mutating logic in your Reducers created by createReducer or createSlice, so there is no need to write immutable code in Reducers in modern Redux in the first place.
Please follow the official Redux tutorials instead of third-party tutorials to always get the most up-to-date information on good Redux practices and will also show you how to use Redux Toolkit in different common scenarios.
For comparison, in modern Redux this would look like
const userSlice = createSlice({
name: "user",
initialState: {
arr:[]
},
reducers: {
// no ACTION_TYPES, this will internally create a type "user/addItem" that you will never use by hand. You will only see it in the devTools
addItem(state, action) {
// you can use mutable logic in createSlice reducers
state.arr.push(action.payload)
}
}
})
// autogenerated action creators
export const { addItem } = slice.actions;
// and export the final reducer
export default slice.reducer;
If you want to combine two arrays, one after another then you can use
//initial state
const initialState = {
array: [],
}
...
case ADD_ARRAY :
return {
...state,
array: [...state.array, ...action.newArr],
}
//if array = [1,2,3,4]
//and newArr = [5,6,7]
//then updated array will be -> [1,2,3,4,5,6,7]
...
This Spread operator (...) iterates array element and store inside the array [ ] or spreading element in the array, what you can simply do using "for loop" or with any other loop.
I have a sample
import * as types from '../../helpers/ActionTypes';
var initialState = {
changedValues: {}
};
const quickEdit = (state = initialState, action) => {
switch (action.type) {
case types.PRODUCT_QUICKEDIT:
{
const item = action.item;
const changedValues = {
...state.changedValues,
[item.id]: item,
};
return {
...state,
loading: true,
changedValues: changedValues,
};
}
default:
{
return state;
}
}
};
export default quickEdit;
The easiest solution to nested arrays is concat():
case ADD_ITEM:
state.array = state.array.concat(action.paylod)
return state
concat() spits out an updated array without mutating the state. Simply set the array to the output of concat() and return the state.
This worked for me
//Form side
const handleSubmit = (e) => {
e.preventDefault();
let Userdata = { ...userdata, id: uuidv4() };
dispatch(setData(Userdata));
};
//Reducer side
const initialState = {
data: [],
};
export const dataReducer = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.SET_DATA:
return { ...state, data: [...state.data, action.payload] };
default:
return state;
}
};

React Redux update item quantity (more than just one increment)

I have a site where the user can increase the quantity on the product before adding it to cart. Now if the user decided to go back to the product and add 3 more by increasing the quantity on the product, then adding to cart - how do I update the quantity of the existing product in basket?
At the moment I get duplicates of the product with different quantities depending on what is selected.
Here is the code I have for my reducer:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
items: [],
};
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
// No idea what to do with this..
state.items.filter((pizza) => pizza.name === payload.name);
// This pushes the item fine, but I get multiple of the same item in the cart instead of just updating its quantity
state.items.push(payload);
// state.items.map((pizza) =>
// pizza.name === payload.name
// ? {
// ...pizza,
// quantity: pizza.quantity + payload.quantity,
// }
// : pizza
// );
},
},
});
export const { addToBasket } = basket.actions;
export const basketItems = (state) => state.basket.items;
export default basket.reducer;
The payload is the specific product, it will be an object:
{
name: "product name",
image: "url.jpeg",
price: "14.99"
}
I can not for the life of me figure out what to do here in order not to mutate the state. Nothing works, I feel like I have tried every possible way but clearly I am missing something.
Any help much appreciated!!!
Thanks
You actually have all the code you need, just need it applied correctly. First check that the item is already in the items array or not. If it is already there then copy the existing state and update the matching element. If it is not included then append the new item to the end of the array.
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
const item = state.items.find((pizza) => pizza.name === payload.name);
if (item) {
state = state.items.map((pizza) =>
pizza.name === payload.name
? {
...pizza,
quantity: pizza.quantity + payload.quantity
}
: pizza
);
} else {
state.items.push(payload);
}
},
},
});
With Redux-Toolkit you can mutate the state objects so you can likely simplify this a bit. Instead of mapping a new array and setting it back to state, jut mutate the found object.
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
const item = state.items.find((pizza) => pizza.name === payload.name);
if (item) {
item.quantity += payload.quantity;
} else {
state.items.push(payload);
}
},
},
});

creating a redux like stores using custom hooks

Here I implemented redux like store using custom hooks. everything goes well and code executed correctly but problem is that in reducer under switch statement "TOGGLE" there I return a updated state which is finally stored in globalstate but if I returned empty object {} instead of {products: updated} still globalstate updating correctly with a change that has been done in reducer...since i am not passing globalstate reference then how it is updated correctly
and what listeners exactly do in dispatch method in code
import MarkFavMulti from "./MarkFavMulti";
import classes from "./MarkFav.module.css";
import useStore from "../HookStore/Store";
import {reducer2} from "../SampleReducer";
const MarkFav = props => {
const [outfit, dispatch] = useStore(reducer2);
const onClicked = (id) => {
dispatch({type: "TOGGLE", id: id});
}
const element = outfit.products.map((item) => {
return <MarkFavMulti cloth={item.name}
favorite={item.favorite}
price={item.price}
key={item.id}
clicked={onClicked.bind(this, item.id)} />
});
return (
<main className={classes.Markfav}>
{element}
</main>
);
};
export default MarkFav;
import {useState, useEffect} from "react";
let globalState = {};
let listeners = [];
const useStore = (reducer) => {
const setState = useState(globalState)[1];
const dispatch = (action) => {
let curr = Object.assign({},globalState);
const newState = reducer({...curr}, action)
globalState = {...globalState,...newState};
for(let listener of listeners) {
listener(globalState);
}
};
useEffect(()=>{
listeners.push(setState);
return () => {
listeners.filter(item => item !==setState);
}
},[setState]);
return [globalState, dispatch];
};
export const initStore = (initialState) => {
if(initialState) {
globalState = {...globalState, ...initialState};
}
}
export default useStore;
let initialState = {
products: [
{ id: 1, name: "shirt", price: "$12", favorite: false },
{ id: 2, name: "jeans", price: "$42", favorite: false },
{ id: 3, name: "coat", price: "$55", favorite: false },
{ id: 4, name: "shoes", price: "$8", favorite: false },
]
}
const configureStore = () => {
initStore(initialState);
};
export default configureStore;
export const reducer2 = (state=initialState, action) => {
switch (action.type) {
case "TOGGLE":
let update = {...state};
let updated = [...update.products];
updated = updated.map(item => {
if(item.id === action.id) {
item.favorite = !item.favorite;
return item;
}
return item;
});
return {products: updated};
//if we return {} ...it will updated correctly in globalstate
default:
throw new Error("not reachable");
}
}
The behavior that you are describing is due to this object assignment right here:
item.favorite = !item.favorite;
Here you are directly mutating the properties of the item object. You probably thought that it would be fine since you are using a copy of the products array.
let update = {...state};
let updated = [...update.products];
What actually happens is that updated is a "shallow copy" of the original array. The array itself is a new array, but the items in that array are the same items as in the state. You can read more about that here.
You need to return a new item object instead of mutating it. Here's a concise way to write it using the ternary operator.
case "TOGGLE":
return {
...state, // not actually necessary since products is the only property
products: state.products.map((item) =>
item.id === action.id
? {
...item,
favorite: !item.favorite
}
: item
)
};

Combine redux reducers without adding nesting

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,
},
},
);

Categories

Resources