In Reactjs, when doing nested loop nothing is rendered - javascript

I have a props which includes users array and messages array. Each user and message is related through a corresponding ID.
I want to match these IDs so I can render messages with the user who posted it.
The issue is nothing is rendering on screen but in the console.log(), I can see the results. Please, can someone shed some light into this.
Also I'm not sure if I'm doing this the right way, so if you can show me a better way, I'll be glad.
Thanks
React:
import React, { Component } from 'react';
class Messages extends Component {
constructor(props) {
super(props);
}
render() {
let self = this;
return (
<ul>
{
this.props.messages.map((message, msg_key) => {
self.props.members.forEach((member) => {
if (member.id === message.userId) {
console.log('inside foreach message.userId', message.userId); // this shows in console.
console.log('inside foreach member.id', member.id); // this shows in console.
return <p>member.id</p> // nothing shows on screen.
}
})
})
}
</ul>
);
}
}
export default Messages;

forEach has no side-effect. Use map
return (
<ul>
{
this.props.messages.map((message, msg_key) => {
return self.props.members.map((member) => { // <-- this line
if (member.id === message.userId) {
console.log('inside foreach message.userId', message.userId); // this shows in console.
console.log('inside foreach member.id', member.id); // this shows in console.
return <p>member.id</p> // nothing shows on screen.
}
})
})
}
</ul>
)
what I mean by "side-effect" is that it neither returns anything nor modifies the original array - map returns a modified array.
remember to supply a key prop when returning an array.
edit
Ah, actually ^ doesn't do what you want.
Array has a nice method to accomplish what you're attempting, find
return (
<ul>
{
this.props.messages.map((message, msg_key) => {
const member = self.props.members.find((member) => member.id === message.userId)
return <p>{member.id}</p>
})
}
</ul>
)
edit #2
well... it actually does work, but it's not pretty. React won't render null in arrays, so it'll skip the ones where the condition wasn't true...

you should redefine your data structure. What you currently have is not going to be performant.
instead of an array of members.. you should have an object key'd by the member id. This is so you can lookup a member by their id. Anytime you have information that is going to be used for other fields of data you should have a key value storage setup key'd by the unique identifier that maps to the other pieces of information. Heres an example
members: {
1: { id: 1, name: 'foo'},
2: { id: 2, name: 'bar'}
}
when you define a data structure like this you can easily look up members for messages.
render (){
const elems = [];
this.props.messages.forEach((message, msg_key) => {
const member = this.props.members[message.userId];
if (member) {
elems.push(
<li key={msg_key}>
<p>{message.title}</p> /* or whatever you want from the message */
<p>{member.id}</p>
</li>
);
}
})
return (
<ul>
{elems}
</ul>
)
}

I think you might messed up some of the attributes inside the array by having 2 loops at 1 time (map, and forEach).
Try have a helper function to map through (instead of forEach) the member.id attributes. So just call {this.renderMemberList} inside the render().
Do you have a JSFiddle set up? it would be easier

Related

Filter array of a computed property using a method in Vue.js

I hope this is not a stupid question. I have a computed property that lists ALL courses. When the user clicks a button calling a method called courseFilters() I would like to filter the computed property to only show Courses that are not archived.
Here is my computed property:
filterCourses() {
const getUser = this.$store.getters['UserData/getUser']
return this.courses.filter((item) => {
if(this.checkAuthorization(['leader'])) {
return item.createdBy === getUser.uid
} else {
return item
}
})
}
Here is my Method:
courseFilters(which) {
if(which == 'hide-archived') {
this.filterCourses.filter((item) => {
if(!item.archive) {
return item
}
})
}
if(which == 'clear') {
this.getCourses(this.$store.getters['AppData/cid'])
}
}
Currently when I click the button nothing changes to the computed property.
I don't think I fully understand the details of your problem, but here's a sketch for a solution that may inspire you:
export default {
data() {
return { areArchivedCoursesVisible: false };
},
computed: {
authorizedCourses() {
const getUser = this.$store.getters['UserData/getUser'];
// The callback in a filter should return true or false.
// I'm not sure if this is exactly what you want.
// If your original code works, then skip this.
return this.courses.filter(
(c) => this.checkAuthorization(['leader']) && c.createdBy === getUser.uid
);
},
visibleCourses() {
// This is probably what you display in your template.
// It's either all of the authorized courses, or the authorized
// courses which are not archived.
return this.areArchivedCoursesVisible
? this.authorizedCourses
: this.this.authorizedCourses.filter((c) => !c.archive);
},
},
methods: {
toggleVisible() {
// Toggle between showing and not showing the archived courses
this.areArchivedCoursesVisible = !this.areArchivedCoursesVisible;
},
},
};
This just holds some state indicating if the archived courses should be shown (toggled via a method). Then you can combine your computed properties to get the correct answer based on the state. In this example, visibleCourses uses the output of the computed property authorizedCourses + the current state.
Also note that I named the computed properties as nouns and not verbs, which I find makes the code much easier to understand.

VueJS - parent object is affected by changes in deep copy of this object in child

Lets start with explaining the structure. I have the page dedicated to a specific company and a component Classification.vue on this page which displays categories of labels and labels itself which are assigned to the current company. First of all I get all possible categories with axios get request, then I get all labels, which are assigned to the current company, and after all I map labels to respective categories. Here is the Classification.vue:
import DoughnutChart from "#comp/Charts/DoughnutChart";
import ModalDialog from '#comp/ModalDialog/ModalDialog';
const EditForm = () => import('./EditForm');
export default {
components: {
DoughnutChart, ModalDialog, EditForm
},
props: ['companyData'],
async created() {
const companyLabels = await this.$axios.get('/companies/' + this.companyData.id + '/labels');
const allLabelsCategories = await this.$axios.get('/labels/categories');
allLabelsCategories.data.map(cat => {
this.$set(this.labelsCategories, cat.labelCategoryId, {...cat});
this.$set(this.labelsCategories[cat.labelCategoryId], 'chosenLabels', []);
});
companyLabels.data.map(l => {
this.labelsCategories[l.label.labelCategory.labelCategoryId].chosenLabels.push({...l.label, percentage: l.percentage})
});
},
computed: {
portfolioChartData() {
let portfolios = [];
// 35 id stands for 'Portfolio' labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
this.labelsCategories[35].chosenLabels.map(label => {
portfolios.push({label: label.name, value: label.percentage});
});
}
return portfolios;
},
portfolioLabels() {
let portfolios = [];
// 35 id stands for Portfolio labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
return this.labelsCategories[35].chosenLabels;
}
return portfolios;
}
},
data() {
return {
labelsCategories: {}
}
}
}
So far so good, I get the object labelsCategories where keys are ids of categories and values are categories objects which now also have chosenLabels key, which we set up in created(). And as you can see I use computed properties, they are necessary for a chart of 'Portfolio' category. And I used $set method in created() exactly for the purpose of triggering reactivity of labelsCategories object so computed properties can respectively react to this.
Now I have a new component inside Classification.vue - EditForm.vue, which is dynamically imported. In this component I do pretty much the same thing, but now I need to get every possible label for every category, not just assigned. So I pass there prop like this:
<modal-dialog :is-visible="isFormActive" #hideModal="isFormActive = false">
<EditForm v-if="isFormActive" ref="editForm" :labels-categories-prop="{...labelsCategories}" />
</modal-dialog>
And EditForm component looks like this:
export default {
name: "EditForm",
props: {
labelsCategoriesProp: {
type: Object,
required: true,
default: () => ({})
}
},
created() {
this.labelsCategories = Object.assign({}, this.labelsCategoriesProp);
},
async mounted() {
let labels = await this.$axios.get('/labels/list');
labels.data.map(label => {
if (this.labelsCategories[label.labelCategoryId].labels === undefined) {
this.$set(this.labelsCategories[label.labelCategoryId], 'labels', []);
}
this.labelsCategories[label.labelCategoryId].labels.push({...label});
});
},
data() {
return {
labelsCategories: {}
}
}
}
And now the problem. Whenever I open modal window with the EditFrom component my computed properties from Calssification.vue are triggered and chart is animating and changing the data. Why? Quite a good question, after digging a bit I noticed, that in EditForm component I also use $set, and if I will add with $set some dummy value, for example:
this.$set(this.labelsCategories[label.labelCategoryId], 'chosenLabels', ['dummy']);
it will overwrite the labelsCategories value in the parent component (Classification.vue)
How is it even possible? As you can see I tried to pass prop as {...labelsCategories} and even did this.labelsCategorie = Object.assign({}, this.labelsCategoriesProp); but my parent object is still affected by changes in child. I compared prop and labelsCategories objects in the EditForm component by === and by 'Object.is()' and they are not the same, so I am completely confused. Any help is highly appreciated.
Btw, I can solve this issue by passing prop as :labels-categories-prop="JSON.parse(JSON.stringify(labelsCategories))" but it seems like a hack to me.
Okay, I was digging deeper in this issue and learned, that neither {...labelsCategories} nor Object.assign({}, this.labelsCategoriesProp) don't create a deep copy of an object only the shallow one. So, I suppose that was the cause of the problem. In this article I learned about shallow and deep copies of objects: https://medium.com/javascript-in-plain-english/how-to-deep-copy-objects-and-arrays-in-javascript-7c911359b089
So, I can leave my hacky way using JSON.parse(JSON.stringify(labelsCategories)) or I can use a library such as lodash:
_.cloneDeep(labelsCategories)
But according to the article I also can create a custom method. And this one option is quite suitable for me. I already had a vue mixin for processing objects, so I just added deepCopy() function there:
deepCopy(obj) {
let outObject, value, key;
if (typeof obj !== "object" || obj === null) {
return obj; // Return the value if obj is not an object
}
// Create an array or object to hold the values
outObject = Array.isArray(obj) ? [] : {};
for (key in obj) {
value = obj[key];
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = this.deepCopy(value);
}
return outObject;
},

Object assigning in JavaScript

There is an object product which has object manufacturer as its field. After fetching manufacturers from my server, I reassign product's field manufacturer with a new data (for example, fetched manufacturer has additional object avatar as its field).
async componentDidMount() {
strapi.setToken(this.state.user.jwt);
const products = await strapi.getEntries('products');
products.forEach(async product => {
const manufacturer = await strapi.getEntry('manufacturers', product.manufacturer.id); //fetched manufacturers has additional field "avatar"
Object.assign(product.manufacturer, manufacturer);
});
console.log(products); // product.manufacturer.avatar is not null
this.setState({
products
});
Then I'm trying to display avatar in React render().
render() {
if (!this.state.products) return null;
return(
{this.state.products ? this.state.products.map(product => (
// and it says that avatar is null
<img src={product.manufacturer.avatar.url} />
// displays manufacturer with avatar object
{console.log(product.manufacturer)}
// says null!
{console.log(product.manufacturer.avatar)}
))}
)
}
Also when I check state with React Developer Tools, product's field manufacturer has object avatar and it isn't null.
UPDATE:
Thanks to Sergey Suslov
strapi.setToken(user.jwt);
const fetchedProducts = await strapi.getEntries('products');
const promices = fetchedProducts.map(async fetchedProduct => {
const manufacturer = await strapi.getEntry('manufacturers', fetchedProduct.manufacturer.id);
const product = { ...fetchedProduct, manufacturer };
return product;
});
Promise.all(promices).then(products =>
this.setState({
products
})
);
You are assigning with the assign method that does not mutate source object, it returns new object as a result, check this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign.
You need to do semthing like this:
product = {
...product,
manufacturer: your assign statment
}
Main reason is that your callback function in foreach is asynchronous, js does not wait for all foreach function calls to completed, it keep on running, couse async word, try to use Promise for this, or better try to use actions for async requests, thank librariy or sagas.
It's because at first render, you do not have the avatar value. These values are fetched after the first render (componentDidMount).
You need to add a test to take that first render into account.
Also, the reason why your console.log is not consistent, is because you are overwriting the value of product, to be more immutable, you should .map instead of forEach, and return a copy of your product instead of modifying the existing one.
How I would write it:
constructor(props){
super(props);
this.state = {
products: []
}
}
render() {
return this.state.products.map(product => {
const {
avatar = {}
} = product.manufacturer;
return (
<img src={avatar.url} />
);
});
}

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. :)

Type Error when Accessing and returning objects arrays inside objects

I'm trying to return the data inside each array in an object.
This is my Json:
"student_clubs": { // brought in as object
"art": { // object
"illustration": ["Done"], // Array
"design": ["Done"] // Array
},
"sports": { // object
"Basketball": ["Incomplete"], // Array
"Hockey": ["Done"] // Array
}
},
I'm trying to display it like this:
art:
illustration: done design: done
sports:
Basketball: Incomplete Hockey: Done
my jsx:
import React, { PropTypes } from 'react';
const ClubsPropTypes = {
student_clubs: PropTypes.object.isRequired,
student_grades: PropTypes.object.isRequired,
};
class Clubs extends React.Component {
render() {
const StudentClubs = this.props.student_clubs;
const Grades = this.props.grades;
return (
<div className="">
{(StudentClubs.art || []).map(art => (
<div className="row">
<ul>
<p>art</p>
<li>illustration: {art.illustration}</li>
<li>design: {art.design}</li>
</ul>
</div>
))}
... same code for sports (seems repetitive)
</div>
);
}
}
Clubs.propTypes = ClubsPropTypes;
module.exports = Clubs;
Right now I'm getting Uncaught TypeError: (StudentClubs.art || []).map is not a function. Why is that? How can I fix this? Also is there a better way to loop through these objects? :\
StudentClubs.art is an object so you cant use it with map.
You can loop over an object using Object.keys() which returns an array of the object keys, see: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
You can loop over an object keys like:
Object.keys(myobject).map((key) => {
// do stuff with myobject[key]
})
Or using for in, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
eg:
for (let key in myobject) {
if (myobject.hasOwnProperty(key)) {
// do stuff with myobject[key]
}
}
So then you can loop over the different clubs and for each of those clubs loop over their keys - display the key name and the list of values (Eg. "done").
If you can, adjust your JSON so that student_clubs is an array of objects and each of those is an array of objects containing the sub category string and a status string.
You dont need an array for Done / Incomplete.
In your json art property is not an array, is an object, and objects has not the map function.
I don't understand why you might want to have an array to specify the state of the subproperty (sport or art).
I would do as follows:
"student_clubs": [{
"art":[
{
"discipline":"illustration",
"status: "Done"
},
{
"discipline:"Design",
"status":"Done"
}
],
"sports":[
{
"discipline":"Basketball",
"status":"Incomplete"
},
{
"discipline":"Hockey",
"status":"whatever"
}
]
]}
So now you can map througth each club, and inside each club you can map througth each "discipline" and do whatever you want in an easier way I think.

Categories