How to mutate state with redux toolkit - javascript

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

Related

How to increase/decrease value with reducer?

I am working on a temperature control app with react native. To get the value of the temperature I use redux toolkit. My problem is that my code for increasing/decreasing the initial value with the reducers doesn't work. I get the 20 as value but using handlers to dispatch(in/decreaseTemp()) doesn't do anything. What am I doing wrong?
Reducers:
import { createSlice } from '#reduxjs/toolkit';
const initialStateValue = {
value: 20
}
const tempSlice = createSlice({
name: 'temp',
initialState: initialStateValue,
reducers: {
increaseTemp: (state = initialStateValue) => {
state = state + 1;
},
decreaseTemp: (state = initialStateValue) => {
state = state - 1;
}
}
});
export const {increaseTemp, decreaseTemp} = tempSlice.actions;
export default tempSlice.reducer;
The handlers:
function increaseTempHandler() {
dispatch(increaseTemp());
}
function decreaseTempHandler() {
dispatch(decreaseTemp());
}
The issue is a mismatch between how you've defined your state
const initialStateValue = {
value: 20
};
and how you're using it:
increaseTemp: (state = initialStateValue) => {
state = state + 1;
},
If state is an object that has a value property then your reducer should be returning a new state that has the updated value property.
e.g.
increaseTemp: (state = initialStateValue) => {
const { value } = state;
return { ...state, value: value+1 }
},

Easy-Peasy access other models

I am using easy-peasy as the state manager of react application
In the actionOn I need to access state of another model, How can I access todos.items in the notes.onAddNote ?
import { createStore, StoreProvider, action, actionOn } from 'easy-peasy';
const todos= {
items: [],
addTodo: action((state, text) => {
state.items.push(text)
})
};
const notes = {
items: [],
addNote: action((state, text) => {
state.items.push(text)
}),
onAddNote: actionOn(
(actions, storeActions) => storeActions.notes.addNote,
(state, target) => {
// HOW TO READ todos items
}
),
};
const models = {
todos,
notes
};
const store = createStore(models);
making onAddNote a thunkOn instead of actionOn

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

React useState hook affecting default variable used in state regardless spread operators, Object.assign and etc

I am experienced js/React developer but came across case that I can't solve and I don't have idea how to fix it.
I have one context provider with many different state, but one state looks like following:
const defaultParams = {
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
}
const InnerPageContext = createContext()
export const InnerPageContextProvider = ({ children }) => {
const [params, setParams] = useState({ ...defaultParams })
const clearParams = () => {
setParams({...defaultParams})
}
console.log(defaultParams)
return (
<InnerPageContext.Provider
value={{
params: params,
setParam: setParam,
clearParams:clearParams
}}
>
{children}
</InnerPageContext.Provider>
)
}
I have one button on page, which calls clearParams function and it should reset params to default value.
But it does not works
Even when i console.log(defaultParams) on every provider rerendering, it seems that defaultParams variable is also changing when state changes
I don't think it's normal because I have used {...defaultParams} and it should create new variable and then pass it to useState hook.
I have tried:
const [params, setParams] = useState(Object.assign({}, defaultParams))
const clearParams = () => {
setParams(Object.assign({}, defaultParams))
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams(defaultParams)
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams({
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
})
}
None of above method works but 3-rd where I hard-coded same object as defaultParams.
The idea is to save dafult params somewhere and when user clears params restore to it.
Do you guys have some idea hot to make that?
Edit:
This is how I update my params:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type][key] = value
} else old[key] = value
console.log('Params', old)
return { ...old }
})
}
please show how you update the "params".
if there is something like this in the code "params.attrs.test = true" then defaultParams will be changed
if old[type] is not a simple type, it stores a reference to the same object in defaultParams. defaultParams.attrs === params.attrs. Since during initialization you destructuring an object but not its nested objects.
the problem is here: old[type][key] = value
solution:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type] = {
...old[type],
key: value,
}
} else old[key] = value
return { ...old }
})
}

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