Immer does not support setting non-numeric properties on arrays - javascript

I'm trying to update a piece of state with an array of data that I'm getting from the server. This is my reducer:
const schoolsDataReducer = (state = { data: [] }, action) =>
produce(state, draft => {
switch (action.type) {
case SET_INITIAL__DATA:
draft.data = [...action.payload.data]
break
}
})
I get this error:
"Immer does not support setting non-numeric properties on arrays: data"
How am I supposed to store an array of objects?
Are arrays in the state considered bad practice?
Am I missing something?

This happens when you pass something not an object for state. Make sure state is an object.

Related

How to check action payload before state update?

I'm learning redux for my first react-redux application. How do I manage to verify payload value before changing my state ? For example the code below:
todoExample = {name: 'learn redux', author: 'myself'}
wrongTodoExample = {name: 'learn redux'}
dispatch(addTodos({todo: todoExample}))
dispatch(addTodos({todo: wrongTodoExample }))
With the above code, I add 2 todo items to my state but they don't have the same keys.
Is there a way to check the payload value in order to authorize the first addTodos but not the second one in my reducer?
I've searched on the internet but I couldn't find an answer. I'm sorry if my question is redundant.
You can use redux middleware to verify things, that is absolutely one of the intended use cases for middleware. Any middleware can inspect and modify any action going through the pipeline before it reaches the reducers, and even prevent an action from continuing on.
const verifyPayload = store => next => action => {
if (isVerifyPayload(action.payload)) {
return next(action);
} else {
return store.dispatch({ type: 'NOT_AUTHORIZED' })
}
}
const store = createStore(
initialState,
applyMiddleware(verifyPayload)
)
Not so clear about your description about same key, you mean name or author, or other specific keys like code\id.
You can try to validate your todos before dispatch or within the addTodos
function addTodos(payload) {
if (!payload.todo.code) return;
// simply return,
// otherwise throw an error to indicate that your todos miss a specific key
}
You can use a ternary operator in your reducer along with some util function to validate your todo. If the todo is valid, then transform your state to include the new todo, if not return the same state (effectively doing nothing).
const isValidTodo = (todo) => {
//Implement your validations. E.g: A valid todo will have a name and an author
return todo.name && todo.author;
}
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return isValidTodo(action.payload) ?
[
...state,
{
name: action.payload.name,
author: action.payload.text,
completed: false
}
]
: state
default:
return state
}
}
I've found a solution that suited well my needs and it's TypeScript. Now I have Payload Type wich allow me to define keys that I need in my action.payload without any validation function.
Thanks all for your asnwers.

Issue with Immutable Update Pattern in Redux

I am trying to update a nested array filed in redux reducer, here is my array i want to update ( just add updating:true filed ) my array looks like
I want to add a updating:true filed on room object after number filed
items: Array(1)
0:
name: (2) [{…}, {…}]
rooms: Array(2)
0: {_id: "5d494e5b11b962065632c760", number: "100" …}
1: {_id: "5d494e6211b962065632c761", number: "102" …}
look like
items :
[{
name: [{}],
rooms : [{}]
}]
Here is my redux reducer code, i am trying to update
case Constants.Status_ADD_REQUEST:
var ID = ID; // action id
return {...state,
items:[{...state.items,
rooms:[{...state.items.rooms,
rooms:{...state.items.rooms.map(x=> x._id === ID ? {...x,updating: true} : x )}}]
}]
};
anyway its not working :) please guide me to correct it
Have a second look at the spread Syntax in JavaScript, especially spread for array literals. For example in your reducer, you create a new state with an items array containing only one fix object (it would still be only one object, even if you would add more items in your example). This item object contains your previous converted and spread items array and rooms property - not really your state shape.
How you could create the state instead:
Full example repo (with TypeScript)
const initialState = {...}
function rootReducer(state = initialState, action) {
switch (action.type) {
case Constants.Status_ADD_REQUEST:
return {
...state,
items: state.items.map(item =>
item.rooms.some(i => i.id === action.id)
? {
...item,
rooms: item.rooms.map(room =>
room.id === action.id ? { ...room, updating: true } : room
)
}
: item
)
};
default:
return state;
}
}
in the style of "Updating an Item in an Array" Redux docs.
If you have to ensure immutable patterns this way and manually, things will get ugly fast. Reason: The key to updating nested data is that every level of nesting must be copied and updated appropriately (structural sharing).
To circumvent those problems, your goal should be to keep your state flattened and to compose reducers. If cloning your state still becomes too complex, you could make use of Immutable Update Utility Libraries. I personally can recommend immer, as you can change state in a mutable way, while the library takes care of deep clones/structural sharing via JavaScript proxies transparently.
PS: It could also make sense to think of another name for "update room", if it is some of your domain/app data.

What's the best alternative to update nested React state property with setState()?

I've been thinking about what would be the best way among these options to update a nested property using React setState() method. I'm also opened to more efficient methods considering performance and avoiding possible conflicts with other possible concurrent state changes.
Note: I'm using a class component that extends React.Component. If you're using React.PureComponent you must be extra careful when updating nested properties because that might not trigger a re-render if you don't change any top-level property of your state. Here's a sandbox illustrating this issue:
CodeSandbox - Component vs PureComponent and nested state changes
Back to this question - My concern here is about performance and possible conflicts between other concurrent setState() calls when updating a nested property on state:
Example:
Let's say I'm building a form component and I will initialize my form state with the following object:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
From my research, by using setState(), React will shallow merge the object that we pass to it, which means that it's only going to check the top level properties, which in this example are isSubmitting and inputs.
So we can either pass it a full newState object containing those two top-level properties (isSubmitting and inputs), or we can pass one of those properties and that will be shallow merged into the previous state.
QUESTION 1
Do you agree that it is best practice to pass only the state top-level property that we are updating? For example, if we are not updating the isSubmitting property, we should avoid passing it to setState() in other to avoid possible conflicts/overwrites with other concurrent calls to setState() that might have been queued together with this one? Is this correct?
In this example, we would pass an object with only the inputs property. That would avoid conflict/overwrite with another setState() that might be trying to update the isSubmitting property.
QUESTION 2
What is the best way, performance-wise, to copy the current state to change its nested properties?
In this case, imagine that I want to set state.inputs.username.touched = true.
Even though you could do this:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
You shouldn't. Because, from React Docs, we have that:
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props.
So, from the excerpt above we can infer that we should build a new object from the current state object, in order to change it and manipulate it as we want and pass it to setState() to update the state.
And since we are dealing with nested objects, we need a way to deep copy the object, and assuming you don't want to use any 3rd party libraries (lodash) to do so, what I've come up with was:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
Note that when your state has nested object you also shouldn't use let newState = Object.assign({},state). Because that would shallow copy the state nested object reference and thus you would still be mutating state directly, since newState.inputs === state.inputs === this.state.inputs would be true. All of them would point to the same object inputs.
But since JSON.parse(JSON.stringify(obj)) has its performance limitations and also there are some data types, or circular data, that might not be JSON-friendly, what other approach would you recommend to deep copy the nested object in order to update it?
The other solution I've come up with is the following:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
What I did in this second alternative was to create an new object from the innermost object that I'm going to update (which in this case is the username object). And I have to get those values inside the key username, and that's why I'm using usernameInput['username'] because later I will merge it into a newInputs object. Everything is done using Object.assign().
This second option has gotten better performance results. At least 50% better.
Any other ideas on this subject? Sorry for the long question but I think it illustrates the problem well.
EDIT: Solution I've adopted from answers below:
My TextInput component onChange event listener (I'm serving it through React Context):
onChange={this.context.onChange(this.props.name)}
My onChange function inside my Form Component
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
I can think of a few other ways to achieve it.
Deconstructing every nested element and only overriding the right one :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
Using the deconstructing operator to copy your inputs :
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
EDIT
First solution using computed properties :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs.[field],
[action]: value
}
}
}))
You can try with nested Object.Assign:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
You can also use spread operator:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
This is proper way to update nested property and keep state immutable.
I made a util function that updates nested states with dynamic keys.
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
You pass a state along with a list of key selectors and the new value.
You can also set the autoAssign value to false to return an object that is a copy of the old state but with the new updated field - otherwise autoAssign = true with update the previous state.
Lastly, if the sequence of selectors don't appear in the object, an object and all nested objects with those keys will be created.
Use the spread operator
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})

Add item to an element of an array in Redux

I'm attempting to get my redux reducer to perform something like this:
.
however, I am outputting the following:
The following is the code I am using to attempt this. I've attempted to include action.userHoldings in the coinData array, however, that also ends up on a different line instead of within the same object. My objective is to get userHoldings within the 0th element of coinData similar to how userHoldings is in the portfolio array in the first image.
import * as actions from '../actions/fetch-portfolio';
const initialState = {
coinData: [],
userHoldings: ''
};
export default function portfolioReducer(state = initialState, action) {
if (action.type === actions.ADD_COIN_TO_PORTFOLIO) {
return Object.assign({}, state, {
coinData: [...state.coinData, action.cryptoData],
userHoldings: action.userHoldings
});
}
return state;
}
I guess you want to do something like this.
return Object.assign({}, state, {
coinData: [ {...state.coinData[0],cryptoData: action.cryptoDatauserHoldings: action.userHoldings}, ...state.coinData.slice(1) ],
});
}
slice(1) is to get all elements except 0th. For the first element, you can construct object the way you like. This should do the trick. Slice returns a new array unlike splice/push or others so safe to use in reducers. :)

Remove Item Without Mutating State in Redux

The first thing I tried was this:
const initialState = {
items: {},
showCart: false,
showCheckout: false,
userID: null
};
export default function reducer(state=Immutable.fromJS(initialState), action) {
case 'REMOVE_FROM_CART':
return state.deleteIn(['items', String(action.id)]);
}
When console logging the deleteIn above, it does actually remove the item from the Map correctly. However, the app doesn't re-render again, because I assume I'm mutating the state(?). (mapStateToProps gets called, but no new state).
So next I tried this:
case 'REMOVE_FROM_CART':
const removed = state.deleteIn(['items', String(action.id)]);
const removeItemState = {
...state,
items: { removed }
}
return state.mergeDeep(removeItemState);
But I'm just adding the deleted item to the items again, creating a duplication.
How can I handle this?
Have you tried removing the item after you've deeply cloned the state?
case 'REMOVE_FROM_CART':
const removeItemState = {
...state
items: {
...state.items
}
};
delete removeItemState.items[String(action.id)];
return removeItemState;
How about reduce?
case 'REMOVE_FROM_CART':
return {
...state,
items: Object.keys(state.items).reduce((acc, curr) => {
if (curr !== action.id) acc[curr] = state.items[curr];
return acc;
}, {})
};
Posting more code (such as my reducers setup) may have helped more, but here's what was going on:
First, this code was the right way to remove the item from the state.
return state.deleteIn(['items', String(action.id)]);
However, because I was using the immutable library and not redux-immutable for my combineReducers, my state was not properly being handled. This was allowing me to do things like state.cart.items (in mapStateToProps) where really I should've been using state.getIn(['cart', 'items']).
Changing that magically made the delete work.
Thanks to #jslatts in the Reactiflux Immutable Slack channel for help with figuring this out!

Categories