How to prevent adding duplicate objects into array in state in ReactJS? - javascript

I am fetching from an API that returns data in the form of an array. Lets say I have an array of 5 objects in this array which are exactly the same. I want to prevent duplicates by checking if duplicate exists before adding to my restaurants State. Below are snippets of my code:
const [restaurants, setRestaurants] = useState([])
const checkForDuplicateBusiness = (businessArray)=>{
businessArray.forEach(singleBusiness=>{
if(!restaurants.some(restaurant=> restaurant.id === singleBusiness.id)){
setRestaurants(restaurants=>[...restaurants, singleBusiness])
console.log(restaurants) //returns []
}
})
}
The problem is that when I am checking with this line
if(!restaurants.some(restaurant=> restaurant.id === singleBusiness.id))
the restaurants state is always empty. I understand that setState is async so the state is not updated by the time im checking it for the next iteration in the forEach loop. Im not sure what to do.
When I console log the restaurants via useEffect like:
useEffect(()=>{
console.log(restaurants)
},[restaurants])
it will show 5 identical objects.
[
0: {id: 'sDlYSTdgZCx0-3cRetKn8A'}
1: {id: 'sDlYSTdgZCx0-3cRetKn8A'}
2: {id: 'sDlYSTdgZCx0-3cRetKn8A'}
3: {id: 'sDlYSTdgZCx0-3cRetKn8A'}
4: {id: 'sDlYSTdgZCx0-3cRetKn8A'}
]
I am confused on this behavior because if i have exactly 5 copies of the object, that means my setRestaurants(restaurants=>[...restaurants, singleBusiness]) was working properly indicative from the spread operator. but my check if statement isnt?
The other solution I've thought of is to store my fetched data in a temporary array and perform the "preventDuplicate" logic in there, and then set it to my restaurants. But I'm not sure if this is the most efficient method. Is there a better way or the "React" way for this?
Any suggestions are appreciated. Thank you!

The setStateSetter function gives you the updated state. Use it higher:
function uniqueById(items) {
const set = new Set();
return items.filter((item) => {
const isDuplicate = set.has(item.id);
set.add(item.id);
return !isDuplicate;
});
}
const updateRestaurantsUnique = (newItems) => {
setRestaurants((restaurants) => {
return uniqueById([...restaurants, ...newItems]);
});
};

Related

redux react update array after copying array

I have a question I'm facing a problem that I can't update array after copying it.
If I copy an array and don't update the id, when I type something on input the text will appear in the same way where I copy it.
here is my Initialstate
const initialState = [
{
id: random numbers,
options: [{ id: random numbers , value: '' },
],
},
];
it will have a lot of option
and I just would like to update options id
this is what i tried
case COPY_QUESTION: {
const newArray = [...state];
const copyQuestion = newArray[action.payload];
copyQuestion.options.map((option) =>
Object.assign({}, option, {
id: random number,
}),
);
return [...state, copyQuestion];
}
thanks for reading my question.
it's caused due to call-by-reference.
As I can see in your code,
You are copying the reference of an array which might have the reason to overwrite details of the original array when you are typing. You can copy the values of the original array by using Javascript Object Prototype
so in that, you need to destruct your array or break your reference in the duplicate array.
example
let A = [a,b,c] //original Array
let B = JSON.parse(JSON.strigify(A)) // duplicate Array

how i can update the value of object of array by use state

I recently started learning react.js for making my final year project using MERN stack
during this i am facing this issue, i.e. i have an array
let projectTech = ['js','node','react'];
and i want to concat it with technologies array present in following useState, please tell me how can i do it.
const [projectObj, setProjectObj] = useState({
title: '',
type: '',
link: '',
technologies:[],
role:'',
description: ''
});
i already tried following combinations but it's not working, after console.log(projectObj.technologies) i got empty array.
setProjectObj({...projectObj,technologies: projectTech});
setProjectObj({...projectObj,technologies:[...projectTech]});
setProjectObj({...projectObj,technologies:[...projectObj.technologies,projectTech]});
please help me, how can i fix this?
You can try using the prevState of the setState function.
setProjectObj((prevState) => ({...prevState, technologies: projectTech }))
check out this: https://codesandbox.io/s/nifty-browser-wmoseu?file=/src/App.js
Nothing is wrong with that code. You only have to change how you console.log your result.
Since setting state is async, try to avoid logging state after setting it.
Add this in your component
const handleSetState = () => {
setProjectObj({...projectObj,technologies: projectTech});
console.log(projectObj) //WRONG, will console.log previous data
}
const handleSetState2 = () => {
const newObj = {...projectObj,technologies: projectTech}
setProjectObj(newObj);
console.log(newObj) //OK, but not actual state.
}
//BEST
useEffect(() => {
console.log(projectObj) //Console.log state
}, [projectObj]) //But only if state changes

Why is this working: updating array element in state with spread operator

When reading React Cookbook I've stumbled upon a code snippet, this function gets called when user checks a task as completed in a TODO list:
markAsCompleted = id => {
// Finding the task by id...
const foundTask = this.state.items.find(task => task.id === id);
// Updating the completed status...
foundTask.completed = true;
// Updating the state with the new updated task...
this.setState({
items: [
...this.state.items,
...foundTask
]
});
}
UPD: Somehow I've completely missed the spread operator on foundTask. So what's really happening is the state gets updated with only ...this.state.items (which was mutated), and the ...foundTask part does not go into state, since it's not a valid spread.
At first it looked like it should add a new element to the items array, instead of updating, so I went to the JS console to check:
state = { items: [{id: '0', done: false}, {id: '1', done: false}] }
upd = state.items.find(obj => obj.id === '0') // {id: "0", done: false}
upd.done = true // also updates object inside the array
state = { items: [...state.items, upd] }
/* Indeed, adds a new element:
items: Array(3)
0: {id: "0", done: true}
1: {id: "1", done: false}
2: {id: "0", done: true}
*/
So then I've downloaded the code and ran it locally. And, to my surprise, it worked! State was getting updated without any trouble, no extra elements appeared. I used React DevTools to see the live state while testing.
I searched the web, but couldn't find any examples like in the book, but with a better explanation. Usually all solutions involve using .map() to build a new array, and then replace an existing one (e.g. https://stackoverflow.com/a/44524507/10304479).
The only difference I see between the book code snippet and console test is that React is using .setState(), so maybe this helps somehow. Can anyone help to clarify, why is it working?
Thanks!
Array.find will return the first value matched in the array. Here the array consist of objects and the value returned will be the reference to the object.
const foundTask = this.state.items.find(task => task.id === id);
Here foundTask will have reference to the same object contained in the state.items. So when you modify foundTask you're modifying the same object as in state.items.
For example,
If this.state.items is [{ id: 1 }] and if you do
const foundTask = this.state.items.find(obj => obj.id === 1);
foundTask.id = 2;
console.log(this.state.items); // [{ id:2 }]
In the code,
this.setState({
items: [
...this.state.items,
...foundTask
]
});
This will update the state with updated completed value for the task.
...foundTask will give you an error in the console since foundTask will be an object and you're spreading it in an array.
Here not having ...foundTask will produce same result. Perhaps not with an error.

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 update key value in immutable while filtering over List of Maps

I have an immutable List that looks like this:
this.state = {
suggestedUsers: fromJS([
{
user: {
client_user_id: "1234567890",
full_name: "marty mcfly",
image: "imageURL",
role_name: "Associate Graphic Designer",
selected: false
}
},
{
user: {
client_user_id: "0987654321",
full_name: "doc",
image: "imageURL",
role_name: "Software Engineer",
selected: false
}
}
)]
This is used in a div that displays this information in the UI.
When I click on the div, I have a function that is fired that looks like this:
selectUser(clientUserId){
// set assessments variable equal to the current team from the state
let assessments = fromJS(this.state.suggestedUsers)
let selectAssessor
// set a variable called selectedUsers equal to the results of filtering over the current suggestedUsers from the state
let selectedUsers = assessments.filter((obj) => {
// store immutable retrieval of the client user id in a variable called userId
let userId = obj.getIn(["user", "client_user_id"])
// when the user clicks 'Add' user, if the id of the user matches the selected user id
// the user, represented here by obj, is pushed into the selectedUsers array stored in the state.
if(userId === clientUserId){
return obj.setIn(["user", "selected"], true)
}
// If the user id is not equal to the selected user, that team member is kept in the
// current team array represented by the state.
return userId !== clientUserId
})
// update the state with the current representation of the state determined by the user
// selected team members for assessment requests
this.setState({
suggestedUsers: selectedUsers
})
}
The core of my question is this:
I would like to update the value of the 'selected' key in the users object to false, when this function is invoked.
I'm aware that I can't mutate the List I'm filtering over directly, but I've tried may different approaches to getting the selected value updated (i.e. using updateIn, and setIn). I know I need to set the result of calling setIn to a variable, and return that to the List I'm filtering over, but I can't get the value to update in the existing List. Any help is greatly appreciated. Thanks!
I've verified that this works the way it should when I change the value manually. How can I change it with immutable by updating this one List.
=========================================================================
Thank you to the community for your feedback. Filtering, and mapping did turn out to be overkill. Using immutability-helper, I am able to update the selected value of a particular user at the index that is clicked. One caveat that was not mentioned is using merge to bring your updated data into your previous data. After updating with immutability helper, I push the updated value into an array, then make it a List, and merge it into my original data. Code below:
let users = this.state.teamAssessments
let selectedArray = []
users.map((obj, index) => {
let objId = obj.getIn(["user", "client_user_id"])
if(objId === clientUserId){
const selectedUser = update(this.state.teamAssessments.toJS(), {
[index]: {
user : {
selected: {
$set: true
}
}
}
})
selectedArray.push(selectedUser)
}
})
let updatedArray = fromJS(selectedArray).get(0)
let mergedData = users.merge(updatedArray)
this.setState({
teamAssessments: mergedData
})
You need immutability-helper. Basically, instead of cloning the entire object you just modify small pieces of the object and re-set the state after you are finished.
import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
this.setState({varName: newData});
In other words, I would ditch the fromJS and the modifying of the array while enumerating it. First, enumerate the array and create your updates. Then, apply the updates separately. Also, to me the "selected" var seems redundant as you know if they are selected because the name of the array after filtration is "selectedUsers."
If I understand your question correctly, here's what I would suggest:
selectUser(clientUserId) {
let suggestedUsers = this.state.suggestedUsers.map(
userBlock =>
userBlock.setIn(
['user', 'selected'],
userBlock.getIn(['user', 'client_user_id']) === clientUserId
)
);
this.setState({
suggestedUsers,
});
}
To confirm -- you are just trying to modify state.suggestedUsers to have selected be true for the selected user, and false for everyone else? Sounds perfect for Immutable's map function (rather than filter, which will just return the elements of the list for which your predicate function returns something truthy).
BTW, you have an error in your object passed into fromJS -- you need an extra }, after the first assessor_assessment block.

Categories

Resources