Related
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;
),
My data is undefined when the app is started but after the refresh, the data comes perfectly.
For startup
It gives me [Unhandled promise rejection: TypeError: Object.entries requires that input parameter not be null or undefined]
But after the refresh, the data comes perfectly and everything working.
This is part of my data
Object {
"attributes": Object {
"htmlName": null,
"id": 0,
"items": Array [
Object {
"htmlName": "r_1",
"name": "m2 (Brüt)",
"numeric": true,
"options": Object {},
"order": 0,
"required": true,
},
Object {
"htmlName": "r_2",
"name": "m2 (Net)",
"numeric": true,
"options": Object {},
"order": 0,
"required": true,
},
Object {
"htmlName": "r_164",
"name": "Arsa Alanı (m2)",
"numeric": true,
"options": Object {},
"order": 0,
"required": true,
},
Object {
"htmlName": "a_137",
"name": "Oda Sayısı",
"numeric": false,
"options": Object {
"12": "1+0",
"13": "1+1",
"14": "1.5+1",
"15": "2+0",
"16": "2+1",
"17": "2.5+1",
"18": "2+2",
"19": "3+1",
"20": "3.5+1",
"21": "3+2",
"22": "4+1",
"226": "0+1",
"23": "4.5+1",
"24": "4+2",
"25": "4+3",
"26": "4+4",
"27": "5+1",
"28": "5+2",
"29": "5+3",
"30": "5+4",
"31": "6+1",
"32": "6+2",
"33": "6+3",
"34": "7+1",
"35": "7+2",
"36": "7+3",
"37": "8+1",
"38": "8+2",
"39": "8+3",
"40": "8+4",
"41": "9+1",
"42": "9+2",
"43": "9+3",
"44": "9+4",
"45": "9+5",
"46": "9+6",
"47": "10+1",
"48": "10+2",
"49": "10 Üzeri",
},
"order": 0,
"required": true,
},
api.js
export const getData = function () {
return axios
.get(
"blabla",
{
headers: {
Authorization: `blabla`,
},
}
)
.then((json) => {
if (json && json.status === 200) {
//console.log(json);
return json.data;
}
})
.catch((e) => {
console.log(e);
});
};
App.js
const [data, setData] = useState({});
const [roomValue, setRoomValue] = useState(null);
const [roomCount, setRoomCount] = useState([]);
const [isFocus, setIsFocus] = useState(false);
useEffect(() => {
getDataFunc();
//setDropdown(data.attributes.items[3].options);
}, []);
const getDataFunc = async () => {
const res = await getData();
//console.log(res);
setData(res);
console.log(data);
};
function setDropdown(query) {
const response = query;
try {
const entries = Object.entries(response);
const tempArray = [];
for (let i = 0; i < entries.length; i++) {
var key;
var value;
(key = entries[i][0]), (value = entries[i][1]);
tempArray.push({ key: value, value: key });
}
setRoomCount(tempArray);
//console.log(roomCount);
} catch (error) {
//console.log(error);
}
}
How can I fix that ?
Add a seperate useEffect to check wheather the data has been set and then only set the dropdown values
useEffect(() => {
getDataFunc();
}, []);
useEffect(() => {
if(data && data.attributes?.items[3]){
setDropdown(data.attributes.items[3].options);
}
}, [data]);
const getDataFunc = async () => {
const res = await getData();
//console.log(res);
setData(res);
console.log(data);
};
It seems like the error is caused by the attributes property being empty when you try to access it. But when you assign them one by one then it loads because the data is loaded per nested property before assigning it to the variable. Means it hasn't fully loaded yet
const response = data.attributes.items[3].options;
It outputs an error because attributes is undefined. So it's not an object, therefore, attributes.items is considered invalid
// sample
const data = {
/* attributes: {
items: {
1: {
options: 'option1'
},
2: {
options: 'option2'
},
3: {
options: 'option3'
}
}
} */
}
const specificData = data.attributes.items[3].options
console.log(specificData) //
So one solution would be using the optional chaining operator to avoid the error, it's just basically a question mark (?) after the object you are trying to access.
The response would be then 'undefined'. That way even if the attributes is empty or not, data will be assigned to the response constant then you can just add some more checking outside of that.
// sample
const data = {
/* attributes: {
items: {
1: {
options: 'option1'
},
2: {
options: 'option2'
},
3: {
options: 'option3'
}
}
} */
}
const specificData = data.attributes?.items[3].options
console.log(specificData) // outputs undefined instead of an error
Let me know if this works btw. maybe you could provide the actual api or maybe a sample api endpoint so we could test it directly. Or maybe the full code?
I've encoutered this before though I'm not 100% sure this is all I've done. But for the error I'm sure the optional chaining operator will prevent it
Try calling getData inside an async function and wait for the process to complete like this in your App.js
const [data, setData] = useState([]);
const [roomCount, setRoomCount] = useState([]);
useEffect(() => {
getDataFunc()
}, []);
const getDataFunc = async() => {
await getData(setData);
const response = data;
console.log(response);
const entries = Object.entries(response);
const tempArray = [];
for (let i = 0; i < entries.length; i++) {
var key;
var value;
(key = entries[i][0]), (value = entries[i][1]);
tempArray.push({ key: value, value: key });
}
setRoomCount(tempArray);
console.log(roomCount);
}
note: The best practice is not to directly pass the setData function to getData api call instead return the response from api and assign the response in main code like below
const response = await getData();
setData(response)
From what I see, your data.attributes has undefined value.
Please double-check everything, it is technically impossible to get data directly if data.attributes is undefined
Here, using an array I have mapped the input boxes, and the value which has entered in that array should be stored in an usestate hook and on click of submit button it should console the array of objects. How to acheive this? Thank You.
const AddCheckPoint = ({ master }) => {
const [addChecklist, setAddChecklist] = useState([])
const [addChecklistValues, setAddChecklistValues] = useState({})
const submit = () => {
setShow(false)
console.log(JSON.stringify(addChecklist))
}
const getFormDetails = (val, id) => {
setAddChecklistValues(prevState => ({
...prevState,
ch_id: id,
value: val
}));
setAddChecklist(prevArray => [...prevArray, addChecklistValues])
}
return (
<>
<Row><Col><b>SLno</b></Col><Col><b>Checkpoint</b></Col><Col><b>Value</b></Col></Row>
{master && master.map((r, i) => {
return (<Form.Group key={i} className="mb-3" controlId={i}>
<Row><Col><Form.Label>{r.slno}</Form.Label></Col><Col><Form.Label>{r.checkPoint}</Form.Label></Col><Col><Form.Control type="text" placeholder="Enter Username" onChange={e => getFormDetails(e.target.value, r._id)} /></Col></Row>
</Form.Group>)
})}
<Button variant="primary" onClick={submit}>Submit</Button>
</>
);
}
export default AddCheckPoint
And the prop "master" looks like the follows
[
{
"_id": "61028558b45073399077becd",
"slno": "A1",
"checkPoint": "Position of adaptor CAT",
"cellName": "PC - 1",
"createdAt": "2021-07-29T10:39:20.902Z",
"updatedAt": "2021-07-29T10:39:20.902Z",
"__v": 0
},
{
"_id": "61028567b45073399077becf",
"slno": "A2",
"checkPoint": "Flush height of Adaptor CAT position",
"cellName": "PC - 1",
"createdAt": "2021-07-29T10:39:35.752Z",
"updatedAt": "2021-07-29T10:39:35.752Z",
"__v": 0
}
]
Just add simple by create a new value and pass it into setAddChecklistValues and setAddChecklist
const getFormDetails = (val, id) => {
const newValue = {
ch_id: id,
value: val,
};
setAddChecklistValues(newValue);
setAddChecklist((prevArray) => [...prevArray, newValue]);
};
Here as you are using onchange u must use debouncing technique to get correct the array of the objects. Refer Following url:
https://www.telerik.com/blogs/debouncing-and-throttling-in-javascript or you can replace below function in your code :
const getFormDetails = (val, id) => {
setAddChecklistValues((prevState) => ({
...prevState,
ch_id: id,
value: val
}));
setAddChecklist((prevArray) => {
const k = prevArray.filter((e) => {
return e.ch_id === addChecklistValues.ch_id;
});
if (k.length > 0) {
prevArray.map((e) => {
if (e.ch_id === addChecklistValues.ch_id) {
e.value = addChecklistValues.value;
}
return e;
});
return prevArray;
}
return [...prevArray, addChecklistValues];
});
};
My Component looks like this:
import cloneDeep from "clone-deep";
import { Context } from "../../context";
const Component = () => {
const context = useContext(Context);
const [state, setState] = useState(
{
_id: "123",
users: [
{
_id: "1",
points: 5
},
{
_id: "2",
points: 8
}
]
}
);
useEffect(() => {
context.socket.emit("points");
context.socket.on("points", (socketData) => {
setState(prevState => {
const newState = {...prevState};
const index = newState.users
.findIndex(user => user._id == socketData.content._id);
newState.users[index].points = socketData.content.points;
return newState;
})
});
return () => context.socket.off("points");
}, []);
return <div>(There is table with identificators and points)</div>
};
I wonder if this is the right approach. I just want to write the code in the right way.
Or maybe it's better with the use of deep cloning? Does it matter?
setState(prevState => {
const newState = cloneDeep(prevState);
const index = newState.users
.findIndex(user => user._id == "2");
newState.users[index].points++;
return newState;
})
EDIT: I added the rest of the code to make it easier to understand.
In your current code:
useEffect(() => {
setState((prevState) => {
const newState = { ...prevState };
const index = newState.users.findIndex((user) => user._id == "2");
newState.users[index].points++;
console.log({ prevState, newState });
return newState;
});
}, []);
You can see that prevState is being mutated (points is 9):
{
_id: "123",
users: [
{
_id: "1",
points: 5
},
{
_id: "2",
points: 9 // Mutated!
}
]
}
To avoid mutating the state, you have to use not mutating methods such as spread operator or map function:
useEffect(() => {
setState((prevState) => {
const newState = ({
...prevState,
users: prevState.users.map((user) =>
user._id === "2"
? {
...user,
points: user.points + 1
}
: user
)
})
console.log({ prevState, newState });
return newState
}
);
}, []);
Now you can see that the prevState is not mutated:
{
_id: "123",
users: [
{
_id: "1",
points: 5
},
{
_id: "2",
points: 8 // Not mutated :)
}
]
}
Your code will work, but the problem will start when your state becomes bigger.
You currently have only two properties on the state, the _id and the users. If in the future will add more and more properties like loggedUser and settings and favorites, and more... your application will render everything on every state change.
At this point you will have to start thinking about other solutions to state management, like redux, mobx, or just split the state to smaller useState, also you can look into useReducer in complex structures.
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();
}