Why is this state mutated by immer produce? - redux

I'm receiving a state mutation error when using Immer's produce to manipulate the state.
When I create a test interface manually inside the same reducer and call produce, it seems to work as expected:
export interface ItemSliceState {
items: Array<IItem> | null | undefined;
}
export const initialState: ItemSliceState = { items: [] };
export const updateItem: CaseReducer<
ItemSliceState,
PayloadAction<IItem | null | undefined>
> = (state: ItemSliceState = initialState, action) => {
const testItem: IItem = {
status: 1
};
const testItems: Array<IItems> = [testItem];
const testState: ItemSliceState = { items: testItems };
const nextTestState = produce(testState, draftState => {
if (!draftState || !draftState.items) {
return testState;
}
draftState.items[0].status = 2;
});
const nextState = produce(state, draftState => {
if (!draftState || !draftState.items) {
return state;
}
draftState.items[0].status = 2;
});
...
// testState.items[0].status = 1
// nextTestState.items[0].status = 2
// state.items[0].status = 2
// nextState.items[0].status = 2
Why is 'state' being manipulated, while 'testState' remains unchanged when produce is called in the same way?
Sandbox with state being updated correctly:
https://codesandbox.io/embed/festive-ritchie-5nf31?fontsize=14&hidenavigation=1&theme=dark

The issue here was that the api was returning testItems as a class, which cannot be mutated by immer. The solution was to send interfaces instead, which immer can mutate.

Related

Unexpected mutation in pinia prop when change local component reactive()

When the value of an reactive object is changed, a prop in my Pinia store is mutated. I tried different ways of attributing new value to this store to avoid this problem, but so far I dont't really understand what is happenning. This is my code:
Component
const form = reactive({
name: "",
exercises: [{ exercise: "", method: "", series: "" }],
});
const handleChange = ({ value, name }: any, eventIndex: any) => {
const newEx = form.exercises.map((ex, index) => ({
...ex,
...(index === eventIndex ? { [name]: value } : null),
}));
const newForm = { name: form.name, exercises: newEx };
Object.assign(form, newForm)
};
const onSubmit = async () => {
const { valid } = await formRef.value.validate();
if (!valid) return;
const storeTraining = workoutStore.newWorkout.training || [];
const workout = {
...workoutStore.newWorkout,
training: [...storeTraining, form],
};
workoutStore.setNewWorkout(workout);
isOpen.value = false;
};
Store
import { ref, computed } from "vue";
import { defineStore } from "pinia";
import type { IUser } from "#/domain/users/type";
import type { ITraining, IWorkout } from "#/domain/workouts/types";
export const useWorkoutStore = defineStore("workouts", () => {
const creatingWorkoutStudent = ref<IUser | null>(null);
const newWorkout = ref<Partial<IWorkout>>({});
const setCreatingWorkoutStudent = (newUser: IUser) => {
creatingWorkoutStudent.value = newUser;
};
const setNewWorkout = (workout: Partial<IWorkout>) => {
newWorkout.value = { ...workout };
return newWorkout.value;
};
const addNewTraining = (training: ITraining) => {
newWorkout.value.training
? newWorkout.value.training.push(training)
: (newWorkout.value.training = [training]);
};
const reset = () => {
newWorkout.value = {};
creatingWorkoutStudent.value = null;
};
return {
creatingWorkoutStudent,
setCreatingWorkoutStudent,
newWorkout,
setNewWorkout,
reset,
addNewTraining,
};
});
And this is a example of an input that mutates reactive values
<text-field
label="ExercĂ­cio"
#input="
handleChange(
{ value: $event.target.value, name: 'exercise' },
index
)
"
:value="exercise.exercise"
:rules="[validationRules.required]"
/>
Every time that a input changes a value in const form = reactive(), the same property in Pinia store is changed too.
I've created a fiddle to exemplify my issue here

I cannot understand WHY I cannot change state in Redux slice

I get the array of objects coming from backend, I get it with socket.io-client. Here we go!
//App.js
import Tickers from "./Components/TickersBoard";
import { actions as tickerActions } from "./slices/tickersSlice.js";
const socket = io.connect("http://localhost:4000");
function App() {
const dispatch = useDispatch();
useEffect(() => {
socket.on("connect", () => {
socket.emit("start");
socket.on("ticker", (quotes) => {
dispatch(tickerActions.setTickers(quotes));
});
});
}, [dispatch]);
After dispatching this array goes to Action called setTickers in the slice.
//slice.js
const tickersAdapter = createEntityAdapter();
const initialState = tickersAdapter.getInitialState();
const tickersSlice = createSlice({
name: "tickers",
initialState,
reducers: {
setTickers(state, { payload }) {
payload.forEach((ticker) => {
const tickerName = ticker.ticker;
const {
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
} = ticker;
state.ids.push(tickerName);
const setStatus = () => {
if (ticker.yeild > state.entities[tickerName].yeild) {
return "rising";
} else if (ticker.yeild < state.entities[tickerName].yeild) {
return "falling";
} else return "noChange";
};
state.entities[tickerName] = {
status: setStatus(),
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
};
return state;
});
return state;
},
},
});
But the state doesn't change. I tried to log state at the beginning, it's empty. After that I tried to log payload - it's ok, information is coming to action. I tried even to do so:
setTickers(state, { payload }) {
state = "debag";
console.log(state);
and I get such a stack of logs in console:
debug
debug
debug
3 debug
2 debug
and so on.

Selector returns empty array from the redux state, even though the array has values

I have the following normalized redux state:
rootReducer: {
blocks: {
"key1": {
id: "key1",
beverages: [], // Array of objects
}
}
}
and I'm trying to select the value of beverages for beverage with the id of "key1" using this selector:
export const getBlockBeverages = (state, blockId) => {
console.log("selector", state.blocks[blockId].beverages);
return state.blocks[blockId].beverages;
};
Whenever I add a new beverage into the beverages array, the selector gets called twice, first time with an empty array, second time with proper values:
Initial state
selector []
selector []
Adding new beverage:
selector []
selector [{/*beverage1*/}]
// Adding another beverage
selector []
selector [{/*beverage1*/}, {/*beverage2*/}]
I'd really appreciate any help/explanation why does the selector get called and beverages value for the block instance is an empty array.
Below is the code for reducers I'm using - I don't see where I could be mutating the original state, I used Immer's produce from the beginning and the problem is still present. Then I tried to use lodash.clonedeep to make sure that I return a new state, but the selector still logs that empty array.
const blockReducer = (state = { id: "", beverages: [] }, action) => {
if (action.type === ADD_BEVERAGE_TO_BLOCK) {
const { beverageId } = action.payload;
const newBeverage = { id: uuid4(), beverageId };
return produce(state, (draft) => {
draft.beverages.push(newBeverage);
});
}
return state;
};
const blocks = (state = {}, action) => {
const key = action.payload.key;
if (key && (state[key] || action.type === CREATE_BLOCK)) {
const instanceState = blockReducer(state[key], action);
return produce(state, (draft: any) => {
draft[key] = instanceState;
});
}
return state;
};
Any ideas why the selector returns empty array instead of array of length 0, 1, 2 etc. as I'm adding new beverages? I'm clueless and will appreciate any help.
The problem was in a different selector that I had been using in a wrong way.
export const makeGetBlockBeveragesLength = () => createSelector(
(state, blockId) => getBlockBeverages(state, blockId),
(blockBeverages) => blockBeverages.length,
);
and instead of mapStateToProps I used createMapStateToProps:
const createMapStateToProps = (state, { blockId }) => () => {
const getBlockBeveragesLength = makeGetBlockBeveragesLength();
return {
length: getBlockBeveragesLength(state, blockId),
};
};
export const Component = connect(createMapStateToProps)(MyComponent);
The empty array logged in one of the logs refers to an older state (the initial state in this case).
I fixed the code to this and it works:
export const getBlockBeveragesLength = createSelector(
(state, blockId) => getBlockBeverages(state, blockId),
(blockBeverages) => blockBeverages.length,
);
const mapStateToProps = (state, { blockId }) => ({
length: getBlockBeveragesLength(state, blockId),
});
export const Component = connect(mapStateToProps)(MyComponent);

Why In redux Output is coming like this --> State Changed{}

I am learning redux with simple age Increment and Decrement example here is the code
const { createStore } = require(`redux`);
const initialState = {age: 21};
const reducerOne = (state = initialState, action) => {
const newState = {...state};
if(action.type === `ADD`) {
newState.age = action.value;
}
if(action.type === `SUBTRACT`) {
newState.age = action.value;
}
return newState;
}
const store = createStore(reducerOne);
store.subscribe(() => {
console.log(`State Changed` + JSON.stringify(store.getState()));
})
store.dispatch({type: `ADD`, val: 10});
store.dispatch({type: `SUBTRACT`, val: 5});
But in Output it is showing like this --> State Changed{}
Help how to fix this and to get Output
Your action is posting the val property and your reducer is reading the value property.
Change your actions this way and it will work:
store.dispatch({type: `ADD`, value: 10});
store.dispatch({type: `SUBTRACT`, value: 5});
EDIT:
You are replacing your old age value, but probably you want to add or subtract from it. You have to modify your reducer to achieve such behaviour:
const reducerOne = (state = initialState, action) => {
const newState = {...state};
if(action.type === `ADD`) {
// add to the previous `age` value and do not overwrite it
newState.age = state.age + action.value;
}
if(action.type === `SUBTRACT`) {
// subtract from the previous `age` value and do not overwrite it
newState.age = state.age - action.value;
}
return newState;
}

How to #connect to async loaded data?

I'm writing an app with React, Redux, react-redux, react-router, react-router-redux and redux-async-connect. The special part of the app is that all client-server communication is done over websockets.
My users can read/write several walls, that I store is a walls store with the following reducer and basic helper functions:
const LOAD = 'ws/wall/LOAD';
const LOAD_SUCCESS = 'estisia/wall/LOAD_SUCCESS';
const LOAD_FAIL = 'estisia/wall/LOAD_FAIL';
const ADD_MESSAGE = 'estisia/wall/ADD_MESSAGE';
const initialWallState = {
loaded: false,
messages: []
};
const initialState = {
walls: {}
};
function wall(state = initialWallState, action = {}) {
switch (action.type) {
... // omitted for brevity
default:
return state;
}
}
export default function walls(state = initialState, action = {}) {
if (typeof action.data === 'undefined' || typeof action.data.wallId === 'undefined') return state;
const newState = {
...state.walls,
[action.data.wallId]: wall(state.walls[action.data.wallId], action)
};
return {walls: newState};
}
export function isLoaded(wallId, globalState) {
return globalState.walls.wallId && globalState.walls.wallId.loaded;
}
export function load(wallId) {
return {
type: LOAD,
send: ['wall/messages/page', [wallId, 1]]
};
}
and I have a Wall container where the appropriate wallId is passed in by react-router, but can't figure out how to make the #connect call pass only the relevant wall data to the container. What should I do instead of the #connect meta-code below to get it working?
#asyncConnect([{
deferred: true,
promise: ({store: {dispatch, getState}, params}) => {
if (!isLoaded(params.wallId, getState())) {
return dispatch(loadWall(params.wallId));
}
}
}])
#connect(
state => ({
messages: state.wall[this.params.wallId].messages,
loading: state.wall[this.params.wallId].loading
}))
export default class Wall extends Component {
static propTypes = {
messages: PropTypes.array
}
... // omitted for brevity
}
How can I achieve redux-async-connect to help me out with the above case?

Resources