Update a specific objects values in an array of objects redux - reactjs

My goal is to update the animatedPhotoUrl value for chatId #1 (or the first object in the chats array.
const chats = [
{"admin": "590",
"animatedPhotoUrl": "",
"chatId": "1"},
{"admin": "680",
"animatedPhotoUrl": "",
"chatId": "2"},
{"admin": "420",
"animatedPhotoUrl": "",
"chatId": "2"}
]
However, when I console.log(chats) after I update in the reducer, I get the error:
[undefined, undefined]
Here is my dispatch:
dispatch({
type: "UPDATE_CHAT_PROFILE_PICTURE_URL",
payload: {
profilePictureUrl: res.imageUrl,
animatedPhotoUrl: "",
chatId: chat.chatId,
},
});
And here is the code in my reducer:
case "UPDATE_CHAT_PROFILE_PICTURE_URL":
return {
...state,
chats: state.chats.map((chat) => {
chat.chatId === action.payload.chatId
? {
...chat,
animatedPhotoUrl: action.payload.animatedPhotoUrl,
}
: chat;
}),
};

You need to add return here:
chats: state.chats.map((chat) => {
return chat.chatId === action.payload.chatId
? {
...chat,
animatedPhotoUrl: action.payload.animatedPhotoUrl,
}
: chat;
}),
or you need to drop braces to get implicit return:
chats: state.chats.map((chat) =>
chat.chatId === action.payload.chatId
? {
...chat,
animatedPhotoUrl: action.payload.animatedPhotoUrl,
}
: chat;
),

Related

How to transform object to array before parsing in Zod

I do have an external URL endpoint that returns an array of field object when it is more than 2 and an object when there is only one, see the snippet below:
Return when the field count is one:
{
"fields": { "fullName": "fieldFullname", "type": "fieldType" }
}
Return when the field is more than one:
{
"fields": [
{ "fullName": "fieldFullname", "type": "fieldType" },
{ "fullName": "fieldFullname", "type": "fieldType" }
]
}
Currently, this is my schema using zod:
export const sObjectMetadataSchema = z.object({
fields: z.array(metadataFieldSchema).optional()
});
export const metadataFieldSchema = z.object({
fullName: z.string().optional(),
type: z.string().optional(),
});
It is configured that it will only accept an array of objects. When it returns only one field it throws an error:
{
"code": "invalid_type",
"expected": "array",
"received": "object",
"path": [],
"message": "Expected array, received object"
}
My goal is if it returns a single object it will convert it to an array of objects during runtime. Currently trying to implement using transform but still not working:
An initial implementation using transform:
export const sObjectMetadataSchema = z.object({
fields: z.unknown().transform((rel) => {
return Array.isArray(rel)
? z.array(metadataFieldSchema).optional()
: 'Convert the rel to Array?';
}),
});
I didn't test that, but seems like should work:
const FieldsSchema = z.object({
fullName: z.string(),
type: z.string()
});
export const sObjectMetadataSchema = z.object({
fields: z.union([FieldsSchema, FieldsSchema.array()]).transform((rel) => {
return Array.isArray(rel)
? rel
: [rel];
}),
});
Thanks for the answer #Konrad !
I improved it a little bit in typescript, so it is also typed correctly:
const arrayFromString = <T extends ZodTypeAny>(schema: T) => {
return z.preprocess((obj) => {
if (Array.isArray(obj)) {
return obj;
} else if (typeof obj === "string") {
return obj.split(",");
} else {
return [];
}
}, z.array(schema));
};

How to change data of children with useState?

const [blocks, setBlocks] = useState({
"time": null,
"blocks": [
{
"key": 1,
"data": "This is some data"
}
]
});
How can I use setBlocks to change "data" to "this is some other data"?
set method of useState not only accepts data, but also accepts functions.
setState(prevState => {
return {...prevState, ...updatedValues};
});
So you can write something like this.
setBlocks(prevState => ({
...prevState,
blocks: prevState.blocks.map(v => {
return (v.key === 1)
? {...v, data: 'this is some other data'}
: v;
}),
});

Returning values from an array in reactjs without using this.state

I have 3 arrays of elements(Users, Comments, Ratings) and I need for 2 of them to output the coresponding value from the first. Something like:
functionComments(users_id)
{
returns the coresponding comments after iterating through}
functionRatings(users_id){returns the coresponding ratings after iterating}
{this.state.Users.map(u=>{
{<Card >{functionComments(u.id)<Card/>}
{<Card >{functionRatings(u.id)<Card/>}
})}
But I have no idea how to return the value without using this.state
elements in a User array look like this
Users: {
"firstName": "diana",
"lastName": "diana",
"age": "0",
"email": "mari#yahoo.com",
"password": "123456",
"name": "diana",
"username": "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
"description": "12346",
"address": null,
"id":45
}
Comments:
{
"username": "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
"commentsList": [
{
"comment": "loc",
"id": 40,
"username": "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
"movieID": "tt6048922"
}
]
}
Ratings
{
"username": "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
"ratingsList": [
{
"rating": 5,
"id": 50,
"username": "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
"movieID": "tt3089630"
}
]
}
The output JSON I want to get:
{
username: usernameleters,
firstName: userFirstName,
commentsList:[
{
comments:"comm"
movieID:"movie"
}],
ratingsList:[
{
rating:"rrt"
movieID:"mm"
}]
}
The fetches for the 3 arrays look like this :
async reloadObjectiveList() {
await UserApiService.fetchUsers()
.then((res) => {
this.setState({
Users: res.data.result
})
});
await CommentApiService.fetchComments()
.then((res) => {
console.log( res.data.result)
this.setState({
Comments: res.data.result
})
}); console.log(this.state.Comments)
await RatingApiService.fetchrating()
.then((res) => {
console.log( res.data.result)
this.setState({
Ratings: res.data.result
})
});
}
componentDidMount()
{
this.reloadObjectiveList();
const userData = (this.mergeData(this.state.Users, this.state.Comments, this.state.Ratings));
console.log("user data "+ this.userData)
this.setState({UserData:this.userData})
}
maybe you can follow this simple approach
I'm Assuming your inputs looks like this
const users = [
{
firstName: 'diana',
lastName: 'diana',
age: '0',
email: 'mari#yahoo.com',
password: '123456',
name: 'diana',
username: 'Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2',
description: '12346',
address: null,
id: 45,
},
];
const Comments = [
{
username: 'Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2',
commentsList: [
{
comment: 'loc',
id: 40,
username: 'Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2',
movieID: 'tt6048922',
},
],
},
];
const Ratings = [
{
username: 'Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2',
ratingsList: [
{
rating: 5,
id: 50,
username: 'Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2',
movieID: 'tt3089630',
},
],
},
];
this will generate the response as you expected
const getUserdata = () => {
const finalUsersData = users.map(user => {
const userOutput = {
username: user.username,
firstName: user.firstName,
};
const userComments = Comments.find(comment => comment.username === user.username);
userOutput.commentsList = userComments ? userComments.commentsList : [];
const userRatings = Ratings.find(rating => rating.username === user.username);
userOutput.userRatings = userRatings ? userRatings.ratingsList : [];
return userOutput;
});
return finalUsersData;
};
Output will be like this
{
username: usernameleters,
firstName: userFirstName,
commentsList:[
{
comments:"comm"
movieID:"movie"
}],
ratingsList:[
{
rating:"rrt"
movieID:"mm"
}]
}
please update the references as necessary. If you are storing users , comments and ratings in state make sure you refrence then using this.state.users and so on.
I think there's no way around using local component state to help improve the efficiency of your component. Using a utility function to merge all the data into an initial state object, you can run this once and won't have to recompute it again and again, for each render cycle. This especially avoids the nested O(n) search-looping through each array to find the corresponding array element matching username, making the runtime of the render function really O(n^2)(!!) since you're already looping over the user array once per render cycle.
You can create a utility function that takes the three arrays and gracefully merges comments and ratings lists into the user array.
const mergeData = (users, comments, ratings) => {
const mapData = (arr, userKey, propertyKey) => arr.reduce((map, curr) => {
map[curr[userKey]] = (curr[propertyKey] || []).map(el => {
const elCopy = {...el}
delete elCopy[userKey]; // remove duplicated key from child array elements
return elCopy;
});
return map;
}, {});
// Generate map objects
const commentsMap = mapData(comments, 'username', 'commentsList');
const ratingsMap = mapData(ratings, 'username', 'ratingsList');
return users.map(user => ({
...user,
// Grab comments and ratings lists from maps using current username
commentsList: commentsMap[user.username],
ratingsList: ratingsMap[user.username],
}));
};
const users = [
{
firstName: "diana",
lastName: "diana",
age: "0",
email: "mari#yahoo.com",
password: "123456",
name: "diana",
username: "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
description: "12346",
address: null,
id: 45
}
];
const comments = [
{
username: "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
commentsList: [
{
comment: "loc",
id: 40,
username: "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
movieID: "tt6048922"
}
]
}
];
const ratings = [
{
username: "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
ratingsList: [
{
rating: 5,
id: 50,
username: "Rn9h8fJfs1XvTv1Y9Sr1mXWB7ib2",
movieID: "tt3089630"
}
]
}
];
const mergeData = (users, comments, ratings) => {
const mapData = (arr, userKey, propertyKey) => arr.reduce((map, curr) => {
map[curr[userKey]] = (curr[propertyKey] || []).map(el => {
const elCopy = {...el}
delete elCopy[userKey];
return elCopy;
});
return map;
}, {});
const commentsMap = mapData(comments, 'username', 'commentsList');
const ratingsMap = mapData(ratings, 'username', 'ratingsList');
return users.map(user => ({
...user,
commentsList: commentsMap[user.username],
ratingsList: ratingsMap[user.username],
}));
};
console.log(mergeData(users, comments, ratings));
When the component mounts you can pass these three arrays to mergeData and set initial state (i.e. in componentDidMount or a state initializer in useState hook in functional components).
function App() {
const [userData] = useState(mergeData(users, comments, ratings));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{userData.map(({ name, username, commentsList, ratingsList }) => (
<div key={username}>
<div>User: {name}</div>
<Card>
Comments:{" "}
{commentsList.map(({ movieID, comment }) => (
<div key={movieID}>
Movie: {movieID}, Comment: {comment}
</div>
))}
</Card>
<Card>
Ratings:{" "}
{ratingsList.map(({ movieID, rating }) => (
<div key={movieID}>
Movie: {movieID}, Rating: {rating}
</div>
))}
</Card>
</div>
))}
</div>
);
}
Edit
For fetch utility and componentDidMount
You are mixing up async/await with promise chains. You need only await the resolved fetch responses, and you can pass the response data to the mergeData utility, then update state. componentDidMount can simply call reloadObjectiveList.
By using a promise chain and updating state intermittently you are queuing up state updates that are only accessible in later render cycles, componentDidMount has already exited by then.
async function reloadObjectiveList() {
try {
const usersRes = await UserApiService.fetchUsers();
const commentsRes = await CommentApiService.fetchComments();
const ratingsRes = await RatingApiService.fetchrating();
const userData = this.mergeData(
usersRes.data.result,
commentsRes.data.result,
ratingsRes.data.result,
);
this.setState({ userData });
} catch {
// Error, maybe set some error state for the UI?
}
}
function componentDidMount() {
this.reloadObjectiveList();
}

map over multiple arrays and only return specific ones

I currently have an axios get request that fetches data from a nasa API and returns it into a list of arrays.
getDataHandler= () => {
axios.get('https://api.nasa.gov/neo/rest/v1/neo/browse?api_key=DEMO_KEY',)
.then((response) => {
const restructuredData = response.data.near_earth_objects.map(
({ name, estimated_diameter, close_approach_data }) => {
const close_approaches = close_approach_data && close_approach_data.length
? close_approach_data.map(({ orbiting_body }) => orbiting_body)
: ["no orbited planet"] // If the array doesn't exist, just use an empty array.
return [
name,
estimated_diameter.kilometers.estimated_diameter_min,
estimated_diameter.kilometers.estimated_diameter_max,
close_approaches[0]
]
})
})
It returns a list of arrays that look like this:
0: (4) ["21277 (1996 TO5)", 1.6016033798, 3.5812940302, "Mars"]
1: (4) ["162038 (1996 DH)", 1.2721987854, 2.844722965, "no orbited planet"]
2: (4) ["189058 (2000 UT16)", 1.332155667, 2.978790628, "Earth"]
3: (4) ["276274 (2002 SS41)", 0.9650614696, 2.1579430484, "Earth"]
4: (4) ["322913 (2002 CM1)", 1.214940408, 2.7166893409, "Jupiter"]
5: (4) ["435730 (2008 UK90)", 0.4411182, 0.9863702813, "no orbited planet"]
Then it gets the list and setState it.
Problem is I have a dropDown menu to only show data from specific planets. So I was wondering if it's possible to map of it again and only keep the ones that are equal to the current selected planet.
And if no planets are selected return all of them.
code i have so far
class MainPage extends Component {
state = {
data: [['name', 'min estimated diameter', 'max estimated diameter', { role: "planet" }]],
dropDownOptions: [
{ value: 'all', label: 'All' },
{ value: 'earth', label: 'Earth' },
{ value: 'mars', label: 'Mars' },
{ value: 'mercury', label: 'Mercury' },
{ value: 'venus', label: 'Venus' },
{ value: 'saturn', label: 'Saturn' },
{ value: 'jupiter', label: 'Jupiter' },
{ value: 'no orbited planet', label: 'No orbited planet'}
],
SelectedDropDownOption: { value: 'all', label: 'All' },
}
componentDidMount() {
this.getDataHandler()
}
getDataHandler= () => {
axios.get('https://api.nasa.gov/neo/rest/v1/neo/browse?api_key=DEMO_KEY',)
.then((response) => {
const restructuredData = response.data.near_earth_objects.map(
({ name, estimated_diameter, close_approach_data }) => {
const close_approaches = close_approach_data &&
close_approach_data.length
? close_approach_data.map(({ orbiting_body }) => orbiting_body)
: ["no orbited planet"]
return [
name,
estimated_diameter.kilometers.estimated_diameter_min,
estimated_diameter.kilometers.estimated_diameter_max,
close_approaches[0]
]
}
)
const joined = this.state.data.concat(restructuredData)
this.setState({ data: joined })
})
.catch(function (error) {
console.log(error);
})
}
DropDownChangeHandler= (SelectedDropDownOption) => {
console.log("hello")
this.setState({SelectedDropDownOption});
}
render () {
console.log(this.state.data)
console.log(this.state.SelectedDropDownOption)
console.log(this.state.SelectedDropDownOption.value)
return (
<React.Fragment>
<DropDown options={this.state.dropDownOptions} onChange={this.getPlanetInformation}/>
<Chart chartData={this.state.data} />
</React.Fragment>
);
}
}
export default MainPage;
You can use filter method to achieve your goal. You loop over every sub array and you keep only those which includes the require planet name passed as function parameter.
const arrayList = [["21277 (1996 TO5)", 1.6016033798, 3.5812940302, "Mars"], ["162038 (1996 DH)", 1.2721987854, 2.844722965, "no orbited planet"], ["189058 (2000 UT16)", 1.332155667, 2.978790628, "Earth"],["276274 (2002 SS41)", 0.9650614696, 2.1579430484, "Earth"], ["322913 (2002 CM1)", 1.214940408, 2.7166893409, "Jupiter"]]
const getPlanetInformation = (planet) => {
const information = arrayList.filter(item => item.includes(planet))
console.log(information)
return information.length ? information : arrayList
}
If there is no planet selected from your dropdown value or the selected doesn't exists inside your array, you can just return the initial value.

How to update a state which is a array of objects?

My state is as follows
this.state = {
todos: [{
title: 'asas',
status: 'incomplete',
uuid: 11
}, {
title: 'asas',
status: 'incomplete',
uuid: 12
}, {
title: 'asas',
status: 'complete',
uuid: 13
}],
currentTab: "Show All"
}
and whenever a user clicks on any of the todo items's checkBox i want to update the state status of the checkbox and i have written the following code for it
this.state.todos.map(todo => {
if (todo.uuid === uuid) todo.status = (todo.status === 'complete') ? 'incomplete' : 'complete'
});
this.forceUpdate();
Is Using forceUpdate a good approach here? as i have updating only a single value inside an array of objects. Is there a better solution for this problem?
either of the following will call setState with the updated state without modifying the current state.
https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#inserting-and-removing-items-in-arrays
using the spread operator:
edit.. actually, this is the hard way 8)
see https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#updating-an-item-in-an-array
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return {
todos: [
...prevState.todos.slice(0, idx),
{
...prevState.todos[idx],
status: prevState.todos[idx].status === "complete" ? "incomplete" : "complete",
}
...prevState.todos.slice(idx + 1),
]
}
});
or using immer:
import produce from "immer";
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return produce(prevState, draft => {
draft.todos[idx].status = prevState.todos[idx].status === "complete" ? "incomplete" : "complete"
});
});

Resources