Update object within object using Redux action - javascript

I'm making a kind of twitter-style clone using React and Redux. I currently have an object of posts ("lings"), that each contain a nested object of replies. They look like this:
{
id: 5,
userName: "Gordon Maloney",
lingBody: "this is a ling",
lingRepliesObj: [
{
replyId: 0,
replyBody: "this is a reply",
},
{
replyId: 1,
replyBody: "this is a second reply",
},
],
},
I'm trying to build an action that allows users to add a reply, by adding to that nested object. The action pushes the ID of the post being replied to, and then the reply itself - when I console log them, it comes through fine, but I just cannot for the life of me work out:~
a) how to be adding to the right item in the object - it's being passed to the action as "parentId" - but I can't work out how to get it from there into the action reducer
b) I am also definitely doing something wrong trying to add the action payload into the array itself.
The action reducer looks like this (currently just pushing the array index of 5 arbitrarily, but that's what I'm wanting to replace with the parentId)
case ActionTypes.POST_REPLY:
console.log(...state.lings[5].lingRepliesObj);
console.log(action.payload);
return {
...state,
lings: [...state.lings[5].lingRepliesObj, {...action.payload}],
};
And the action itself like this:
export const postReply = (values) => {
console.log("You posted a new reply: ", values);
return {
type: "POST_REPLY",
payload: {
replyId: "new-id",
replyAuthor: "reply tester",
replyType: "correction",
correctionBody: values.replyCorrection,
replyBody: values.replyReply,
},
};
};
Am I even close? šŸ˜£
My GitHub for it is here: https://github.com/gordonmaloney/Lingr and the relevant files are:
src/components/actions/newReplyAction
src/components/reducers/lings
src/components/LingReply

Related

React Redux - Reducer with CRUD - best practices?

I wrote simple reducer for User entity, and now I want to apply best practices for it, when switching action types and returning state. Just to mention, I extracted actions types in separate file, actionsTypes.js.
Content of actionsTypes.js :
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';
export const GET_USER_SUCCESS = 'GET_USER_SUCCESS';
export const ADD_USER_SUCCESS = 'ADD_USER_SUCCESS';
export const EDIT_USER_SUCCESS = 'EDIT_USER_SUCCESS';
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
First question, is it mandatory to have actions types for FAILED case? For example, to add GET_USERS_FAILED and so on and handle them inside usersReducer?
Root reducer is:
const rootReducer = combineReducers({
users
});
There is code of usersReducer, and I put comments/questions inside code, and ask for answers (what are best practices to handle action types):
export default function usersReducer(state = initialState.users, action) {
switch (action.type) {
case actionsTypes.GET_USERS_SUCCESS:
// state of usersReducer is 'users' array, so I just return action.payload where it is array of users. Will it automatically update users array on initial state?
return action.payload;
case actionsTypes.GET_USER_SUCCESS:
// What to return here? Just action.payload where it is just single user object?
return ;
case actionsTypes.ADD_USER_SUCCESS:
// what does this mean? Can someone explain this code? It returns new array, but what about spread operator, and object.assign?
return [...state.filter(user => user.id !== action.payload.id),
Object.assign({}, action.payload)];
case actionsTypes.EDIT_USER_SUCCESS:
// is this ok?
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
case actionsTypes.DELETE_USER_SUCCESS:
// I'm not sure about this delete part, is this ok or there is best practice to return state without deleted user?
return [...state.filter(user => user.id !== action.user.id)];
default:
return state;
}
}
I'm not an experienced developer but let me answer your questions what I've learned and encountered up to now.
First question, is it mandatory to have actions types for FAILED case?
For example, to add GET_USERS_FAILED and so on and handle them inside
usersReducer?
This is not mandatory but if you intend to give a feedback to your clients it would be good. For example, you initiated the GET_USERS process and it failed somehow. Nothing happens on client side, nothing updated etc. So, your client does not know it failed and wonders why nothing happened. But, if you have a failure case and you catch the error, you can inform your client that there was an error.
To do this, you can consume GET_USERS_FAILED action type in two pleases for example. One in your userReducers and one for, lets say, an error or feedback reducer. First one returns state since your process failed and you can't get the desired data, hence does not want to mutate the state anyhow. Second one updates your feedback reducer and can change a state, lets say error and you catch this state in your component and if error state is true you show a nice message to your client.
state of usersReducer is 'users' array, so I just return
action.payload where it is array of users. Will it automatically
update users array on initial state?
case actionsTypes.GET_USERS_SUCCESS:
return action.payload;
This is ok if you are fetching whole users with a single request. This means your action.payload which is an array becomes your state. But, if you don't want to fetch all the users with a single request, like pagination, this would be not enough. You need to concat your state with the fetched ones.
case actionsTypes.GET_USERS_SUCCESS:
return [...state, ...action.payload];
Here, we are using spread syntax.
It, obviously, spread what is given to it :) You can use it in a multiple ways for arrays and also objects. You can check the documentation. But here is some simple examples.
const arr = [ 1, 2, 3 ];
const newArr = [ ...arr, 4 ];
// newArr is now [ 1, 2, 3, 4 ]
We spread arr in a new array and add 4 to it.
const obj = { id: 1, name: "foo, age: 25 };
const newObj = { ...obj, age: 30 };
// newObj is now { id: 1, name: "foo", age: 30 }
Here, we spread our obj in a new object and changed its age property. In both examples, we never mutate our original data.
What to return here? Just action.payload where it is just single user
object?
case actionsTypes.GET_USER_SUCCESS:
return ;
Probably you can't use this action in this reducer directly. Because your state here holds your users as an array. What do you want to do the user you got somehow? Lets say you want to hold a "selected" user. Either you can create a separate reducer for that or change your state here, make it an object and hold a selectedUser property and update it with this. But if you change your state's shape, all the other reducer parts need to be changed since your state will be something like this:
{
users: [],
selectedUser,
}
Now, your state is not an array anymore, it is an object. All your code must be changed according to that.
what does this mean? Can someone explain this code? It returns new
array, but what about spread operator, and object.assign?
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), Object.assign({}, action.payload)];
I've already tried to explain spread syntax. Object.assign copies some values to a target or updates it or merges two of them. What does this code do?
First it takes your state, filters it and returns the users not equal to your action.payload one, which is the user is being added. This returns an array, so it spreads it and merges it with the Object.assign part. In Object.assign part it takes an empty object and merges it with the user. An all those values creates a new array which is your new state. Let's say your state is like:
[
{ id: 1, name: "foo" },
{ id: 2, name: "bar" },
]
and your new user is:
{
id: 3, name: "baz"
}
Here what this code does. First it filters all the user and since filter criteria does not match it returns all your users (state) then spread it (don't forget, filter returns an array and we spread this array into another one):
[ { id: 1, name: "foo"}, { id: 2, name: "bar" } ]
Now the Object.assign part does its job and merges an empty object with action.payload, a user object. Now our final array will be like this:
[ { id: 1, name: "foo"}, { id: 2, name: "bar" }, { id: 3, name: "baz" } ]
But, actually Object.assign is not needed here. Why do we bother merging our object with an empty one again? So, this code does the same job:
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), action.payload ];
is this ok?
case actionsTypes.EDIT_USER_SUCCESS:
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
It seems ok to me. You don't mutate the state directly, use spread syntax to create a new one, update the related part and finally set your state with this new one.
I'm not sure about this delete part, is this ok or there is best
practice to return state without deleted user?
case actionsTypes.DELETE_USER_SUCCESS:
return [...state.filter(user => user.id !== action.user.id)];
Again, it seems ok to me. You filter the deleted user and update your state according to that. Of course there are other situations you should take into considiration . For example do you have a backend process for those? Do you add or delete users to a database? If yes for all the parts you need to sure about the backend process success and after that you need to update your state. But this is a different topic I guess.

How to access an attribute in an object array in Redux state?

I'm very new to Redux, and confused as to how to update nested state.
const initialState = {
feature: '',
scenarios: [{
description: '',
steps: []
}]
}
I know that to just push to an array in an immutable way, we could do,
state = {
scenarios: [...state.scenarios, action.payload]
}
And to push into a specific attribute, as this SO answer suggests
How to access array (object) elements in redux state, we could do
state.scenarios[0].description = action.payload
But my question is, how would we access a particular attribute in an object array without mentioning the index? is there a way for us to push it to the last empty element?
All suggestions are welcome to help me understand, thank you in advance :)
Redux helps to decouple your state transformations and the way you render your data.
Modifying your array only happens inside your reducers. To specify which scenario's description you want to modify is easy to achieve by using an identifier. If your scenario has in id, it should be included in your action, e.g.
{
"type": "update_scenario_description",
"payload": {
"scenario": "your-id",
"description": "New content here"
}
}
You can have a reducer per scenario. The higher level reducer for all scenarios can forward the action to the specific reducer based on the scenario id, so that only this scenario will be updated.
In your ui, you can use the array of scenarios and your scenario id to render only the specific one you're currently viewing.
For a more detailed explanation, have a look at the todo example. This is basically the same, as each todo has an id, you have one reducer for all todos and a specific reducer per todo, which is handled by it's id.
In addition to the accepted answer, I'd like to mention something in case someone still wants to "access a particular attribute in an object array without mentioning the index".
'use strict'
const initialState = {
feature: '',
scenarios: [{
description: '',
steps: []
}]
}
let blank = {}
Object.keys(initialState.scenarios[0]).map(scene => {
if (scene === 'steps'){
blank[scene] = [1, 2]
} else {
blank[scene]=initialState.scenarios[0][scene]
}
})
const finalState = {
...initialState,
scenarios: blank
}
console.log(initialState)
console.log(finalState)
However, if scenarios property of initialState instead of being an object inside an array, had it been a simple object like scenarios:{description:'', steps: []}, the solution would have been much simpler:
'use strict'
const initialState = {
feature: '',
scenarios: {
description: '',
steps: []
}
}
const finalState = {
...initialState,
scenarios: {
...initialState.scenarios, steps: [1, 2, 4]
}
}
console.log(initialState)
console.log(finalState)

How to update the value of a single property within a state object in React.js?

So I have the following object structure:
const SamplePalette = {
id: 1,
name: "Sample Palette",
description: "this is a short description",
swatches: [
{
val: "#FF6245",
tints: ["#FFE0DB", "#FFA797"],
shades: ["#751408", "#C33F27"]
},
{
val: "#FFFDA4",
tints: ["#FFFFE1"],
shades: ["#CCCB83"]
},
{
val: "#BFE8A3",
tints: ["#E7FFD7"],
shades: ["#95B77E"]
}
]
}
Let's imagine that this object is managed by the state of my app like this:
this.state = {
currentPalette: SamplePalette,
}
My question is how would I go about updating the val property of a given swatch object in the swatches array? Or more generally - how do I only update pieces of this object?
I tried using the update helper as well as to figure out how Object.assign() works, however I've been unsuccessful and frankly can't really grasp the syntax by just looking at examples.
Also, since I'm going to be modifying this object quite a lot, should I look into maybe using Redux?
[EDIT]
I tried #maxim.sh suggestion but with no success:
this.setState(
{ currentPalette: {...this.state.currentPalette,
swatches[0].val: newValue}
})
Consider you have new new_swatches
I think the clearer way is to get array, update it and put back as:
let new_swatches = this.state.currentPalette.swatches;
new_swatches[0].val = newValue;
this.setState(
{ currentPalette:
{ ...this.state.currentPalette, swatches: new_swatches }
});
Also you have : Immutability Helpers or https://github.com/kolodny/immutability-helper
Available Commands
{$push: array} push() all the items in array on the target.
{$unshift: array} unshift() all the items in array on the target.
{$splice: array of arrays} for each item in arrays call splice() on the target with the parameters provided by the item.
{$set: any} replace the target entirely.
{$merge: object} merge the keys of object with the target.
{$apply: function} passes in the current value to the function and updates it with the new returned value.

Properly returning nested state with Object.assign

I'm working on a React/Redux application and for the most part, everything has been working smoothly.
Essentially, it's a todo application that has categorization built in.
I'm having trouble properly returning the full existing state in my reducer when the user adds a todo-item inside a category.
The redux state before I dispatch the ADD_ITEM action looks like this:
{
items: {
"HOME": [["Do laundry", "High Priority"],["Feed kids", "Low priority"] ],
"WORK": [["Get promotion", "High priority"],["Finish project", "Medium priority"] ],
"BOTH": [["Eat better", "Medium priority"],["Go for a run", "High priority"] ],
},
settings: {
Test: "test"
}
}
The user navigates to a category(pre-made, haven't implemented creating them yet) and can create a new todo-item with a name and a priority. This dispatches an action that returns an array like [category, name, priority].
Currently in my reducer, I have it where it is successfully adding the item, but it is emptying/overwriting all the existing categories.
My reducer looks like this:
case types.ADD_ITEM:
let cat = action.payload[0];
let name = action.payload[1];
let priority = action.payload[2];
return Object.assign({}, state, { items: { [cat]: [...state.items[cat], [name, priority]]}});
I've tried creating a new object first with all the combined items like so:
let combinedItems = Object.assign({}, state.items, { [cat]: [...state.items[cat], action.payload] });
If I console.log the above combinedItems, I get the exact object that I want items to be. However, I'm struggling to have the final object returned by the reducer to reflect that.
When I tried something like below, I got an object that contained combinedItems as a separate key inside items.
return Object.assign({}, state, { items: { combinedItems, [cat]: [...state.items[cat], [name, priority]]}});
Can anyone help me get my final redux state to contain all the existing categories/items + the user added one? I would really appreciate the help.
I think you should use objects in places where you have arrays. In your action payload, instead of:
[category, name, priority]
You can have:
{category, name, priority}
action.payload.category
I would make the same change with your todo items. Instead of:
[["Eat better", "Medium priority"], ... ]
You can have:
[{ name: "Eat better", priority: "Medium" }, ... ]
Now in terms of whether it's better to make items an object with category keys or an array of items that know its category... well I think the latter is better, that way if you get a single item, you don't need to go up to its parent to find out which category it belongs to. It would also make your problem a bit more manageable.
items: [
{
name: "Eat better",
priority: "Medium",
category: "Both"
}, ...
]
Putting this all together to solve your problem:
case types.ADD_ITEM:
let newItem = {
name: action.payload.name,
priority: action.payload.priority,
category: action.payload.category
}
return Object.assign({}, state, { items: [ ...state.items, newItem ] })
Whatever benefit you had before with categories as keys are trivial to reproduce with this structure.
Get all items in the HOME category:
this.props.items.filter(item => item.category === 'HOME')

Updating nested data in redux store

What's the best/correct way to update a nested array of data in a store using redux?
My store looks like this:
{
items:{
1: {
id: 1,
key: "value",
links: [
{
id: 10001
data: "some more stuff"
},
...
]
},
...
}
}
I have a pair of asynchronous actions that updates the complete items object but I have another pair of actions that I want to update a specific links array.
My reducer currently looks like this but I'm not sure if this is the correct approach:
switch (action.type) {
case RESOURCE_TYPE_LINK_ADD_SUCCESS:
// TODO: check whether the following is acceptable or should we create a new one?
state.items[action.resourceTypeId].isSourceOf.push(action.resourceTypeLink);
return Object.assign({}, state, {
items: state.items,
});
}
Jonny's answer is correct (never mutate the state given to you!) but I wanted to add another point to it. If all your objects have IDs, it's generally a bad idea to keep the state shape nested.
This:
{
items: {
1: {
id: 1,
links: [{
id: 10001
}]
}
}
}
is a shape that is hard to update.
It doesn't have to be this way! You can instead store it like this:
{
items: {
1: {
id: 1,
links: [10001]
}
},
links: {
10001: {
id: 10001
}
}
}
This is much easier for update because there is just one canonical copy of any entity. If you need to let user ā€œedit a linkā€, there is just one place where it needs to be updatedā€”and it's completely independent of items or anything other referring to links.
To get your API responses into such a shape, you can use normalizr. Once your entities inside the server actions are normalized, you can write a simple reducer that merges them into the current state:
import merge from 'lodash/object/merge';
function entities(state = { items: {}, links: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities);
}
return state;
}
Please see Redux real-world example for a demo of such approach.
React's update() immutability helper is a convenient way to create an updated version of a plain old JavaScript object without mutating it.
You give it the source object to be updated and an object describing paths to the pieces which need to be updated and changes that need to be made.
e.g., if an action had id and link properties and you wanted to push the link to an array of links in an item keyed with the id:
var update = require('react/lib/update')
// ...
return update(state, {
items: {
[action.id]: {
links: {$push: action.link}
}
}
})
(Example uses an ES6 computed property name for action.id)

Categories

Resources