Circular dependency in React JSX element - javascript

I am having an issue with React creating a circular structure in an object I am passing as a prop. Basically, I am using react-bootstrap-table2 to render a table of data. This component requires two arrays of objects as props: columns and data. I am creating and passing these props, but for some reason they contain a circular structure. At some point, JSON.stringify is called and TypeError: Converting circular structure to JSON is thrown. The example below shows the problem occurring in the columns prop. I create a hard-coded array and one dynamically to showcase the difference. this.props.cols = ["2017","2018","2019"]
const columns1 = [
{
dataField: 'dataTopic',
text: null
},
{
dataField: '2017',
text:
<div>
<div>2016 – 2017</div>
</div>
},
{
dataField: '2018',
text:
<div>
<div>2017 – 2018</div>
</div>
},
{
dataField: '2019',
text:
<div>
<div>2018 – 2019</div>
</div>
}
];
class MyTable extends React.Component {
static propTypes = {
cols: PropTypes.array
};
constructor(props){
super(props);
this.getColumns = this.getColumns.bind(this);
this.getRows = this.getRows.bind(this);
}
getColumns() {
const columns = [
{
dataField: 'dataTopic',
text: null
}
];
this.props.cols.forEach((year) => {
columns.push(
{
dataField: year,
text:
<div>
<div>{year - 1} – {year}</div>
</div>
}
);
});
return columns;
}
getRows(){
//Do stuff
}
render() {
console.log(columns1);
console.log(this.getColumns());
return (
<div>
<BootstrapTable keyField='dataTopic' columns={this.getColumns()} data={this.getRows()} />
</div>
);
}
}
export default MyTable;
I get the following output to the console:
For some reason, everything is the same except for the _owner attribute in the JSX element. If I expand _owner, I find where my recursion is occurring:
I understand that _owner is used to track the parent of a React component, but I don't understand why it is null in columns1 and not in the object returned by getColumns(). Could someone please explain to me why this is?

The _owner is null for columns1 because this array has been declared outside of the MyTable component.

Related

Vue 3 passing array warning: Extraneous non-props attributes were passed to component but could not be automatically inherited

please, I'm learning a VueJS 3 and I have probably begineer problem. I have warn in browser developer console like this one:
The Message is:
[Vue warn]: Extraneous non-props attributes (class) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.
I'm passing array of objects to the child Component. In my parent views/Home.vue compoment I have this implemenation:
<template>
<div class="wrapper">
<section v-for="(item, index) in items" :key="index" class="box">
<ItemProperties class="infobox-item-properties" :info="item.properties" />
</section>
</div>
</template>
<script>
import { ref } from 'vue'
import { data } from '#/data.js'
import ItemProperties from '#/components/ItemProperties.vue'
export default {
components: {
ItemDescription,
},
setup() {
const items = ref(data)
return {
items,
}
},
</script>
In child compoment components/ItemProperties.vue I have this code:
<template>
<div class="infobox-item-property" v-for="(object, index) in info" :key="index">
<span class="infobox-item-title">{{ object.name }}:</span>
<span v-if="object.type === 'rating'">
<span v-for="(v, k) in object.value" :key="k">{{ object.icon }}</span>
</span>
<span v-else>
<span>{{ object.value }}</span>
</span>
</div>
</template>
<script>
export default {
props: {
info: {
type: Array,
required: false,
default: () => [
{
name: '',
value: '',
type: 'string',
icon: '',
},
],
},
},
}
</script>
It doesn't matter if I have default() function or not. Also doesn't matter if I have v-if condition or not. If I have cycle in the Array, I got this warning
Data are in data.js file. The part of file is here:
export const data = [
{
title: 'White shirt',
properties: [
{ name: 'Material', value: 'Cotton', type: 'string', icon: '' },
{ name: 'Size', value: 'M', type: 'string', icon: '' },
{ name: 'Count', value: 4, type: 'number', icon: '' },
{ name: 'Absorption', value: 4, type: 'rating', icon: '💧' },
{ name: 'Rating', value: 2, type: 'rating', icon: '⭐️' },
{ name: 'Confort', value: 2, type: 'rating', icon: '🛏' },
{ name: 'Sleeves', value: 'Short', type: 'string', icon: '' },
{ name: 'Color', value: 'White', type: 'string', icon: '' },
],
},
]
PS: Application works but I'm afraid about that warning. What can I do please like right way?
I will be glad for any advice. Thank you very much.
Well I think the error message is pretty clear.
Your ItemProperties.vue component is rendering fragments - because it is rendering multiple <div> elements using v-for. Which means there is no single root element.
At the same time, you are passing a class to the component with <ItemProperties class="infobox-item-properties" - class can be placed on HTML elements only. If you place it on Vue component, Vue tries to place it on the root element of the content the component is rendering. But because the content your component is rendering has no root element, Vue does not know where to put it...
To remove the warning either remove the class="infobox-item-properties" or wrap the content of ItemProperties to a single <div>.
The mechanism described above is called Fallthrough Attributes ("Non-prop attributes" Vue 2 docs). It is good to know that this automatic inheritance can be switched off which allows you to apply those attributes by yourself on the element (or component) you choose besides the root element. This can be very useful. Most notably when designing specialized wrappers around standard HTML elements (like input or button) or some library component...
The ItemProperties component has multiple root nodes because it renders a list in the root with v-for.
Based on the class name (infobox-item-properties), I think you want the class to be applied to a container element, so a simple solution is to just add that element (e.g., a div) in your component at the root:
// ItemProperties.vue
<template>
<div>
<section v-for="(item, index) in items" :key="index" class="box">
...
</section>
</div>
</template>
demo
You could also prevent passing down attributes in child components by doing this:
export default defineComponent({
name: "ChildComponentName",
inheritAttrs: false // This..
})
Source: https://vuejs.org/guide/components/attrs.html
This could also be triggered from parent components that have props: true in their route definition. Make sure that you add props: true only in the components that you actually need it and have some route params as props.
You are passing a class attribute to ItemProperties without declaring it.
Declare class in props options api should solve this issue.
ItemProperties.vue
...
export default {
props:["class"],
...
}

Babel expects "," where there should be a "."?

So, the following implementation works just fine to read JSON data and turn it into rendered components - until I try to add the children. Then, it spits out an error.
function:
const catalogRenderer = (config) => {
if (typeof KeysToComponentMap[config.component] !== "undefined") {
return React.createElement(
KeysToComponentMap[config.component],
{
key: config.key,
title: config.title
},
{
config.children && config.children.map(c => catalogRenderer(c))
}
);
}
}
error:
app.js:134 Uncaught Error: Module build failed (from ./node_modules/babel-loader/lib/index.js)
"...Scripts/CatalogRenderer.js: Unexpected token, expected "," (25:14)"
console:
},
24 | {
> 25 | config.children && config.children.map(c => catalogRenderer(c))
| ^
26 | }
27 | );
28 | }
I'm using react as part of an electron application, it's a long story about all the moving parts, but everything else so far has worked just fine. In the editor, if I move to the preceding { from that mysteriously disliked . on line 25, it's highlighting the period as if this should somehow close the bracket.
Is there something I'm not understanding about the syntax here? The same thing happens if I attempt to just map and render the children like so:
{
config.children.map(c => catalogRenderer(c))
}
I've tried enclosing the whole statement in brackets, curly braces, parentheses--no matter what I do, babel seems to expect a comma, but giving it a comma obviously doesn't help. Any idea what I'm doing wrong?
eta: This is the JSON object I'm attempting to render from:
const catConfig = {
catalog: [
{
component: 'pen',
title: `B.C. Palmer`,
key: `B.C.PalmerPen`,
children: `A child string`
},
{
component: 'content',
key: `B.C.PalmerWorldList`,
children: [
{
component: 'world',
title: `Rismere`,
key: `RismereWorld`
},
{
component: 'content',
key: `RismereSeries`,
children: [
{
component: 'series',
title: `The Eidolon War`,
key: `TheEidolonWarSeries`
},
{
component: 'content',
key: `TheEidolonWarBooks`,
children: [
{
component: 'book',
title: `Magic's Heart`,
key: `MagicsHeartBook`
},
{
component: 'book',
title: `Magic's Fury`,
key: `MagicsFuryBook`
},
{
component: 'book',
title: `Magic's Grace`,
key: `MagicsGraceBook`
}
]
}
]
}
]
},
{
component: 'pen',
title: `Simon Strange`,
key: `SimonStrangePen`
}
]
}
This JSON will be generated via a database call, and written each time the database is updated, and update the state of the 'catalog' component.
So, for example, the second object in the catalog array above is a container which, when the first 'pen' component is clicked, becomes visible and shows a list of 'world' components (in this case, just the one.) However, the function only successfully renders any 'parent' components--if I take out the curly braces at lines 24 and 26, it simply skips them but doesn't error.
The components are composed of button elements and a div (content). The buttons will likely become Link element when I get this working, but the original version was written in vanilla javascript, I haven't implemented routing with the catalog yet. So, the pen component for example:
import React from 'react'
export default penButton => {
return(
<button className="catalogItem pen">
<img src="src/icons/catPenName.png" className="catalogIcon"/>
<p className="contentLabel">{penButton.title}</p>
</button>
)
}
Is a top level component, and gets rendered just fine. It's next sibling (and the next sibling of any button except a book) is content:
import React from 'react'
export default contentList => {
return(
<div className="contentList">
</div>
)
}
contentList is just a div with the contentList class, which handles visibility and animation. Should I have a place for the "children" key in JSON to populate the children of content?
When you want to render multipile children elemen'ts into your react component, you need to pass each child as a seperate parameter.
See this answer as an example:
how to render multiple children without JSX
so your solution should be to use spread syntax.
here is an example:
const catalogRenderer = (config) => {
if (typeof KeysToComponentMap[config.component] !== "undefined") {
let childs = [];
if (config.children) {
childs = config.children.map(c => catalogRenderer(c));
}
return React.createElement(
KeysToComponentMap[config.component],
{
key: config.key,
title: config.title
},
...childs
);
}
}
Well, that was simple and a little silly. I updated the content component to:
import React from 'react'
export default contentList => {
return(
<div className="contentList">
{contentList.children} <---- added
</div>
)
}
Content didn't have a place to put children. Obviously.

Can not use #johmun/vue-tags-input with VueJS and Laravel/Spark

I currently set up a new playground with VueJS/Laravel/Spark and want to implement a tags input component.
I don't understand how to register those components correctly. I'm following the how-to-guides and official documentation, but the implementation just works so-so.
I want to implement the library from #johmun -> http://www.vue-tags-input.com which I installed via npm (npm install #johmun/vue-tags-input).
I created a single file component named VueTagsInput.vue that looks like this:
<template>
<div>
<vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="newTags => tags = newTags"
:autocomplete-items="filteredItems"
/>
</div>
</template>
<script>
import VueTagsInput from '#johmun/vue-tags-input';
export default {
components: {
VueTagsInput,
},
data() {
return {
tag: '',
tags: [],
autocompleteItems: [{
text: 'Spain',
}, {
text: 'France',
}, {
text: 'USA',
}, {
text: 'Germany',
}, {
text: 'China',
}],
};
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
};
</script>
I imported this single file component at resources/js/bootstrap.js like so:
import VueTagsInput from './VueTagsInput.vue'
And I'm using this component in the home.blade.php view like this:
<vue-tags-input v-model="tag"
autocomplete-always-open
add-from-paste
allow-edit-tags>
</vue-tags-input>
This renders an input with which I can interact as desired, but I can not use the autocomplete function with the countries entered above, and the console also throws the following error:
[Vue warn]: Property or method "tag" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
I don't know what I'm doing wrong.
So I stumbled across the solution by trial & error.
First I had to register the component the right way in resources/js/bootstrap.js like so:
import VueTagsInput from './VueTagsInput.vue'
Vue.component('vue-tags-input', VueTagsInput);
But this caused another error because I called the component within the component registration itself. I used the name option in the single file component in order to overcome this error. I gave my newly created component a different name like this:
<template>
<div>
<johmun-vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="newTags => tags = newTags"
:autocomplete-items="filteredItems"
/>
</div>
</template>
<script>
import JohmunVueTagsInput from '#johmun/vue-tags-input';
export default {
name: "VueTagsInput",
components: {
JohmunVueTagsInput,
},
data() {
return {
tag: '',
tags: [],
autocompleteItems: [{
text: 'Spain',
}, {
text: 'France',
}, {
text: 'USA',
}, {
text: 'Germany',
}, {
text: 'China',
}],
};
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
};
</script>

How can I pass an object from container to class in Meteor and React?

I'm trying to get a value in an object of javascript but it fails somehow. I managed to get an intended data from mongoDB by findOne method. Here is my code and console log.
const title = Questions.findOne({_id: props.match.params.id});
console.log(title);
Then console says:
Object {_id: "bpMgRnZxh5L4rQjP9", text: "Do you like apple?"}
What I wanna get is only the text in the object. I have already tried these.
console.log(title.text);
console.log(title[text]);
console.log(title["text"]);
console.log(title[0].text);
But I couldn't access to it... The error message is below.
Uncaught TypeError: Cannot read property 'text' of undefined
It sounds super easy but I couldn't solve by my self. Could anyone help me out?
Additional Context
I'm using Meteor and React. I would like to pass the text inside of the object from the container to the class. I would like to render the text in render(). But it doesn't receive any data from the container... The console.log in the container works well and shows the object.
import React, { Component } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import { Questions } from '../../api/questions.js';
import PropTypes from 'prop-types';
import { Answers } from '../../api/answers.js';
import ReactDOM from 'react-dom';
import { Chart } from 'react-google-charts';
class MapClass extends React.Component{
handleAlternate(event){
event.preventDefault();
const country = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
Answers.insert({
country,
yes: false,
question_id:this.props.match._id,
createdAt: new Date(), // current time
});
ReactDOM.findDOMNode(this.refs.textInput).value = '';
}
handleSubmit(event) {
event.preventDefault();
const country = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
Answers.insert({
country,
yes: true,
question_id: this.props.match.params.id,
createdAt: new Date(), // current time
});
ReactDOM.findDOMNode(this.refs.textInput).value = '';
}
constructor(props) {
super(props);
this.state = {
options: {
title: 'Age vs. Weight comparison',
},
data: [
['Country', 'Popularity'],
['South America', 12],
['Canada', 5.5],
['France', 14],
['Russia', 5],
['Australia', 3.5],
],
};
this.state.data.push(['China', 40]);
}
render() {
return (
<div>
<h1>{this.props.title.text}</h1>
<Chart
chartType="GeoChart"
data={this.state.data}
options={this.state.options}
graph_id="ScatterChart"
width="900px"
height="400px"
legend_toggle
/>
<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
<input
type="text"
ref="textInput"
placeholder="Type to add new tasks"
/>
<button type="submit">Yes</button>
<button onClick={this.handleAlternate.bind(this)}>No</button>
</form>
</div>
);
}
}
export default MapContainer = createContainer(props => {
console.log(Questions.findOne({_id: props.match.params.id}));
return {
title: Questions.findOne({_id: props.match.params.id})
};
}, MapClass);
The problem here is that at the moment when the container is mounted, the data is not yet available. Since I do not see any subscriptions in your container I assume that you handle that elsewhere and thus there is no way of knowing when the data is ready. You have 2 options.
1) move the subscription into the container and use the subscription handle ready() function to assess if the data is ready. Show a spinner or something while it is not. Read this.
2) use lodash/get function (docs) to handle empty props. You would need to
npm install --save lodash
and then
import get from 'lodash/get';
and then in your class render method:
render() {
const text = get(this.props, 'title.text', 'you-can-even-define-a-default-value-here');
// then use `text` in your h1.
return (...);
}
Does this work for you?

What would be the most efficient way to structure a VueJS app with this logic?

I have an app that will display data fetched from a series of sources, depending on a condition. The problem is the way I fetch, organize, and return the data is different depending on the original source (I even have to import other libraries just for one method).
I currently have it set up like the example below, but what happens when my list of sources grows to, say, 100? How should I be structuring the app? Thank you!
<template>
<div>
<h1>{{data.title}}</h1>
<h2>{{data.subtitle}}</h2>
<p>{{data.description}}</p>
</div>
</template>
<script>
export default {
data() {
return {
data: {}
}
},
methods: {
getFetchMethod() {
var i = Math.floor(Math.random() * 3);
if (i == 0) {
this.getData();
} else if (i == 1) {
this.getDataThisWay();
} else if (i == 2) {
this.getDataAnotherWay();
} else {
this.getDataEtc();
};
},
getData() {
this.data = {
'title': 'Foo',
'subtitle': 'Bar',
'description': 'Blah'
};
},
getDataThisWay() {
this.data = {
'title': 'Foo',
'subtitle': 'Bar',
'description': 'Blah'
};
},
getDataAnotherWay() {
this.data = {
'title': 'Foo',
'subtitle': 'Bar',
'description': 'Blah'
};
},
getDataEtc() {
this.data = {
'title': 'Foo',
'subtitle': 'Bar',
'description': 'Blah'
};
}
},
mounted() {
this.getFetchMethod();
}
}
</script>
<style lang="css">
</style>
This has nothing to do with VueJS. You should create an array of objects, each with their own data set. You can then use your random number as an index.
// store all your data in an array (you could make this part of the vuejs data object
var datasets = [
{
title: 'Foo',
subtitle: 'Bar',
description: 'Blah'
},
{
title: 'Foo2',
subtitle: 'Bar2',
description: 'Blah2'
}
// etc...
];
// get a random number based on the length of your array
var i = Math.floor(Math.random() * datasets.length);
// use the random number to index your array
this.data = datasets[i];
UPDATE: You say that you've got multiple functions that all get data differently, you could do the same approach by putting the functions into an array an indexing them.
// put all the method references (no calling parens) into an array
var methods = [
this.getData,
this.getDataThisWay,
this.getDataEtc
];
var i = Math.floor(Math.random() * datasets.length);
// index the array then call the particular method
this.data = datasets[i]();
Additionally, if your methods rely on a particular context, you can use call() to supply a specific context that's different from this.
this.data = datasets[i].call(this); // where this is the current context
I'd probably make the template into its own component that takes props for the title, subtitle, and description. The parent component will then be responsible for for passing the data into this child component based on however it got the data.
Child.vue
<template>
<div>
<h1>{{title}}</h1>
<h2>{{subtitle}}</h2>
<p>{{description}}</p>
</div>
</template>
<script>
export default {
props: ["title", "subtitle", "description"]
}
</script>
Parent.vue
<template>
<div>
<button #click="way1">Way 1</button>
<button #click="way2">Way 2</button>
<child :title="title" :subtitle="subtitle" :description="description"></child>
</div>
</template>
<script>
import Child from "./Child.vue"
export default {
components:{
Child
},
data(){
return {
title: "",
subtitle: "",
description: ""
};
},
methods: {
way1(){
this.title="way 1 title";
this.subtitle="way 1 subtitle"
this.description="way 1 description"
},
way2(){
this.title="way 2 title";
this.subtitle="way 2 subtitle"
this.description="way 2 description"
}
}
}
</script>
EDIT:
I'd also recommend importing a "data provider" into the Parent.vue who can have any logic for getting the data, but the expectation would be that it returns it in a known shape which can then easily be passed into the child component
Parent2.vue
<template>
<div>
<button #click="get">Get</button>
<child :title="title" :subtitle="subtitle" :description="description"></child>
</div>
</template>
<script>
import dataProvider from "./dataProvider"
import Child from "./Child.vue"
export default {
components:{
Child
},
data(){
return {
title: "",
subtitle: "",
description: ""
};
},
methods: {
get(){
var data = dataProvider.get();
this.title=data.title;
this.subtitle=data.subtitle;
this.description=data.description;
}
}
}
</script>

Categories