How to manage state in a tree component in reactjs - javascript

I've been struggling this for a couple of days, trying to figure out the "react" way to do it.
Basically, I have a tree, a list of lists (of lists ...) that can be arbitrarily nested, and I want a component that will display this and also enable rearrangement.
Here's my data:
var data = [{
id: 1
}, {
id: 2, children: [
{
id: 3, children: [{id: 6}]
}, {
id: 4
}, {
id: 5
}]
}]
My first pass was to just have a single "tree" component that builds the nested lists of DOM elements in its render function (look at the code here). That actually worked pretty well for small numbers of elements, but I want to be able to support hundreds of elements, and there was a very high re-render cost when an element was moved within the tree (~600ms when there were a few hundred elements).
So I think I'll have each "node" of the tree be it's own instance of this component. But here's my question (sorry for the long intro):
Should each node dynamically query for the list it's children's IDs from a central "database" and store that in state? Or should the top-most node load the whole tree and pass everything down through props?
I'm still trying to wrap my mind around how state & props should be handled & divvied up.
Thanks

I wanted to try out the tree structure with React and came up with a simple component that hides subtrees when you click on <h5>. Everything is a TreeNode. Is this similar to what you were thinking?
You can see it in action in this JSFiddle: http://jsfiddle.net/ssorallen/XX8mw/
TreeNode.jsx:
var TreeNode = React.createClass({
getInitialState: function() {
return {
visible: true
};
},
render: function() {
var childNodes;
if (this.props.node.childNodes != null) {
childNodes = this.props.node.childNodes.map(function(node, index) {
return <li key={index}><TreeNode node={node} /></li>
});
}
var style = {};
if (!this.state.visible) {
style.display = "none";
}
return (
<div>
<h5 onClick={this.toggle}>
{this.props.node.title}
</h5>
<ul style={style}>
{childNodes}
</ul>
</div>
);
},
toggle: function() {
this.setState({visible: !this.state.visible});
}
});
bootstrap.jsx:
var tree = {
title: "howdy",
childNodes: [
{title: "bobby"},
{title: "suzie", childNodes: [
{title: "puppy", childNodes: [
{title: "dog house"}
]},
{title: "cherry tree"}
]}
]
};
React.render(
<TreeNode node={tree} />,
document.getElementById("tree")
);

Seems like it'd be nicer to pass everything down as props, as this will prevent you from the trouble of managing individual insertion/deletion. Also, like the comments said, the key attributes prevents a huge chunk of unnecessary re-rendering.
You might want to check this link: http://facebook.github.io/react/blog/2013/11/05/thinking-in-react.html. It describes the kind of dilemma you're having and how to approach it.
(Coincidentally, I've made a react tree view a while ago: https://github.com/chenglou/react-treeview. Take whatever you want from it!)

Here is a quick example of how to create a treeview using React and Flux.
http://www.syntaxsuccess.com/viewarticle/5510d81be1ce52d00e93da55
The React component is recursive and state is managed using Flux.

Related

Is it possible to send Vue/Javascript code within an object to another component?

I have a parent and child Vue components. The child component supplies the parent component with data required to render the page using a simple object that is emitted using emit.
Child component data:
const Steps [
{
sequence: 1,
name: "Personal",
description: `<p>Enter your name and phone number</p>`,
},
{
sequence: 2,
name: "OTP",
description: `<p>An OTP code has been sent to you. Resend code</p>`,
},
]
const SelectedStep = ref( 0 ); // Which step did you select?
const ActiveStep = ref( {} ); // What step is the form currently on?
SelectedStep.value += 1; // e.g. SelectedStep.value === 2
// Get the object in Steps array where the sequence === 2
ActiveStep.value = Steps.find(step => {
return step.sequence === SelectedStep.value
})
// Send this to the parent to render the description and title
emit('SelectedStep', ActiveStep.value);
Depending on which sequence is selected, the object within Steps matching that sequence value will get loaded into ActiveStep. This is then emitted/supplied to the parent component.
However if you look at the object with sequence: 2 above, within the description is a resend code text. I need that to be a link with a binding so that when it is clicked a function is run to resend the code. I imagined something like this:
{
sequence: 2,
name: "OTP",
description: `<p>An OTP code has been sent to you. <a v-on:click="resendOTP">Resend code</a></p>`,
},
When that is rendered on the page, the v-on:click is not being interpreted and is rendered as-is in the HTML.
The parent component is just a view that uses this component:
<header>
<h1>{{ActiveStep.title}}</h1>
<div v-html="`${ActiveStep.description}`">{{ActiveStep.description}}</div>
</header>
<div>
<div class="content">
<Component-Signup v-on:SelectedStep="updateActiveStep"/>
</div>
</div>
<script>
import ComponentSignup from "../../components/Signup.vue"
export default {
components: {
"Component-Signup": ComponentSignup
},
setup() {
const ActiveStep = ref({});
function updateActiveStep(SelectedStep) {
ActiveStep.value = SelectedStep // SelectedStep is the object emitted from child component
}
return {
updateActiveStep,
ActiveStep
}
}
}
</script>
How could this be achieved?
First, your description contains HTML, so interpolation ({{ }}) will not display it as you expect ...it will be displayed encoded
v-html directive can be used to render raw HTML
BUT v-html is useful ONLY for HTML. Any Vue related functionality (as v-on) will not work. Docs:
Note that you cannot use v-html to compose template partials, because Vue is not a string-based templating engine. Instead, components are preferred as the fundamental unit for UI reuse and composition.
Your only option is to create separate component for each step, and use is to display the right component for the current step...

Can I modify Vue.js VNodes?

I want to assign some attributes and classes to the children VNode through data object. That just works. But during my Vue.js investigation, I have not seen such pattern in use, that's why I don't think it's good idea to modify children VNode's.
But that approach sometimes comes in handy – for example I want to assign to all the buttons in default slot the aria-label attribute.
See example below, using default stateful components:
Vue.component('child', {
template: '<div>My role is {{ $attrs.role }}</div>',
})
Vue.component('parent', {
render(h) {
const {
default: defaultSlot
} = this.$slots
if (defaultSlot) {
defaultSlot.forEach((child, index) => {
if (!child.data) child.data = {}
if (!child.data.attrs) child.data.attrs = {}
const {
data
} = child
data.attrs.role = 'button'
data.class = 'bar'
data.style = `color: #` + index + index + index
})
}
return h(
'div', {
class: 'parent',
},
defaultSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent>
<child></child>
<child></child>
<child></child>
<child></child>
<child></child>
</parent>
</div>
And here is examples using stateless functional components:
Vue.component('child', {
functional: true,
render(h, {
children
}) {
return h('div', {
class: 'bar'
}, children)
},
})
Vue.component('parent', {
functional: true,
render(h, {
scopedSlots
}) {
const defaultScopedSlot = scopedSlots.default({
foo: 'bar'
})
if (defaultScopedSlot) {
defaultScopedSlot.forEach((child, index) => {
child.data = {
style: `color: #` + index + index + index
}
child.data.attrs = {
role: 'whatever'
}
})
}
return h(
'div', {
class: 'parent',
},
defaultScopedSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<parent>
<template v-slot:default="{ foo }">
<child>{{ foo }}</child>
<child>{{ foo }}</child>
<child>{{ foo }}</child>
</template>
</parent>
</div>
I am waiting for the following answers:
Yes, you can use it, there are no potential problems with this approach.
Yes, but these problem(s) can happen.
No, there are a lot of problem(s).
UPDATE:
That another good approach I have found it's to wrap child VNode into the another created VNode with appropriate data object, like this:
const wrappedChildren = children.map(child => {
return h("div", { class: "foo" }, [child]);
});
Using this approach I have no fear modifying children VNode's.
Thank you in advance.
There are potential problems with doing this. Used very sparingly it can be a useful technique and personally I would be happy to use it if no simple alternative were available. However, you're in undocumented territory and if something goes wrong you'll likely have to debug by stepping through Vue internals. It is not for the faint-hearted.
First, some of examples of something similar being used by others.
Patching key:
https://medium.com/dailyjs/patching-the-vue-js-virtual-dom-the-need-the-explanation-and-the-solution-ba18e4ae385b
An example where Vuetify patches a VNode from a mixin:
https://github.com/vuetifyjs/vuetify/blob/5329514763e7fab11994c4303aa601346e17104c/packages/vuetify/src/components/VImg/VImg.ts#L219
An example where Vuetify patches a VNode from a scoped slot: https://github.com/vuetifyjs/vuetify/blob/7f7391d76dc44f7f7d64f30ad7e0e429c85597c8/packages/vuetify/src/components/VItemGroup/VItem.ts#L58
I think only the third example is really comparable to the patching in this question. A key feature there is that it uses a scoped slot rather than a normal slot, so the VNodes are created within the same render function.
It gets more complicated with normal slots. The problem is that the VNodes for the slot are created in the parent's render function. If the child's render function runs multiple times it'll just keep getting passed the same VNodes for the slot. Modifying those VNodes won't necessarily do what you'd expect as the diffing algorithm just sees the same VNodes and doesn't perform any DOM updates.
Here's an example to illustrate:
const MyRenderComponent = {
data () {
return {
blueChildren: true
}
},
render (h) {
// Add a button before the slot children
const children = [h('button', {
on: {
click: () => {
this.blueChildren = !this.blueChildren
}
}
}, 'Blue children: ' + this.blueChildren)]
const slotContent = this.$slots.default
for (const child of slotContent) {
if (child.data && child.data.class) {
// Add/remove the CSS class 'blue'
child.data.class.blue = this.blueChildren
// Log it out to confirm this really is happening
console.log(child.data.class)
}
children.push(child)
}
return h('div', null, children)
}
}
new Vue({
el: '#app',
components: {
MyRenderComponent
},
data () {
return {
count: 0
}
}
})
.red {
border: 1px solid red;
margin: 10px;
padding: 5px;
}
.blue {
background: #009;
color: white;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-render-component>
<div :class="{red: true}">This is a slot content</div>
</my-render-component>
<button #click="count++">
Update outer: {{ count }}
</button>
</div>
There are two buttons. The first button toggles a data property called blueChildren. It's used to decide whether or not to add a CSS class to the children. Changing the value of blueChildren will successfully trigger a re-render of the child component and the VNode does get updated, but the DOM is unchanged.
The other button forces the outer component to re-render. That regenerates the VNodes in the slot. These then get passed to the child and the DOM will get updated.
Vue is making some assumptions about what can and can't cause a VNode to change and optimising accordingly. In Vue 3 this is only going to get worse (by which I mean better) because there are a lot more of these optimisations coming along. There's a very interesting presentation Evan You gave about Vue 3 that covers the kinds of optimisations that are coming and they all fall into this category of Vue assuming that certain things can't change.
There are ways to fix this example. When the component is performing an update the VNode will contain a reference to the DOM node, so it can be updated directly. It's not great, but it can be done.
My own feeling is that you're only really safe if the patching you're doing is fixed, such that updates aren't a problem. Adding some attributes or CSS classes should work, so long as you don't want to change them later.
There is another class of problems to overcome. Tweaking VNodes can be really fiddly. The examples in the question allude to it. What if data is missing? What if attrs is missing?
In the scoped slots example in the question the child component has class="bar" on its <div>. That gets blown away in the parent. Perhaps that's intentional, perhaps not, but trying to merge together all the different objects is quite tricky. For example, class could be a string, object or array. The Vuetify example uses _b, which is an alias for Vue's internal bindObjectProps, to avoid having to cover all the different cases itself.
Along with the different formats are the different node types. Nodes don't necessarily represent components or elements. There are also text nodes and comments, where comment nodes are a consequence of v-if rather than actual comments in the template.
Handling all the different edge cases correctly is pretty difficult. Then again, it may be that none of these edge cases cause any real problems for the use cases you actually have in mind.
As a final note, all of the above only applies to modifying a VNode. Wrapping VNodes from a slot or inserting other children between them in a render function is perfectly normal.

Vue fails to render data object which has a parent

I need a JS tree object in which child nodes know who their parents are. Vue seems OK with this, at least as far as displaying the correct infinitely expandable object in Vue Devtools goes:
... and displaying the correct parent in the console:
The problem is that Vue errors when trying to render that data on a page:
HTML
<ol>
<li>
<b>family.name</b> {{ family.name }}
</li>
<li>
<b>family.children[0].name</b> {{ family.children[0].name }}
</li>
<li>
<b>family.children[1].name</b> {{ family.children[1].name }}
</li>
<li>
<!-- Add the extra brackets to this to see the "object" error -->
<b>family.children[0].parent.name</b> { family.children[0].parent.name }
</li>
</ol>
JS
var app = new Vue({
el: '.container',
data: {
family: {
name: "Dad",
children: [
{
name: "Marcus",
children: [
{
name: "Bob"
}
]
},
{
name: "Anna",
children: [
{
name: "Ringo"
}
]
}
]
}
},
mounted() {
this.family.children[0].parent = this.family;
this.family.children[0].children[0].parent = this.family.children[0];
this.family.children[1].parent = this.family;
this.family.children[1].children[0].parent = this.family.children[1];
}
});
There is a live example if you prefer at https://s3-ap-southeast-2.amazonaws.com/mscau/vue-parenting.htm.
Does anyone know how to overcome this obstacle?
You are setting up that relationship in mounted() lifecycle hook which occurs after the first render, so the render will fail before you even set this up.
If you do this work inside created() hook instead, it will set it up before the first render to avoid the failure.
See lifecycle diagram for more detailed info about these hooks.

Vue - Render components depending on state of parent data

I'm trying to render components depending on the state of an array in the parent (App.vue). I'm not sure at all that this is the correct approach for this use case (new to Vue and not experienced programmer) so I will gladly take advice if you think this is not the right way to think about this.
I'm trying to build a troubleshooter that consists of a bunch of questions. Each question is a component with data that look something like this:
data: function() {
return {
id: 2,
question: "Has it worked before?",
answer: undefined,
requires: [
{
id: 1,
answer: "Yes"
}
]
}
}
This question is suppose to be displayed if the answer to question 1 was yes.
My problem is I'm not sure on how to render my components conditionally. Current approach is to send an event from the component when it was answered, and to listen to that event in the parent. When the event triggers, the parent updates an array that holds the "state" of all answered questions. Now I need to check this array from each component to see if there are questions there that have been answered and if the right conditions are met, show the question.
My question is: How can I check for data in the parent and show/hide my component depending on it? And also - is this a good idea or should I do something different?
Here is some more code for reference:
App.vue
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: []
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
}
}
}
</script>
two.vue
<template>
<div v-if="show">
<h3>{{ question }}</h3>
<div class="c-troubleshooter__section">
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer">
<label for="question-2-a">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-b" name="question-2" value="nej" v-model="answer">
<label for="question-2-b">Nej</label>
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
id: 2,
question: "Bla bla bla?",
answer: undefined,
requires: [
{
id: 1,
answer: "Ja"
}
]
}
},
computed: {
show: function() {
// Check in parent to see if requirements are there, if so return true
return true;
}
},
watch: {
answer: function() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
}
}
}
</script>
 Rendering questions conditionally
as #Roy J suggests in comments, questions data probably belongs to the parent. It is the parent who handles all the data and who decides which questions should be rendered. However, there are plenty of strategies for this:
Display questions conditionally with v-if or v-show directly in the parent template:
Maybe the logic to display some questions is not at all generic. It can depend upon more things, user settings... I don't know. If that's the case, just render the questions conditionally directly in the parent, so you don't need to access the whole questions data in any question. Code should be something like the following:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions" v-if="displayQuestion(1)"/>
<two #changeAnswer="updateActiveQuestions" v-if="displayQuestion(2)"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: [],
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
},
displayQuestion(index){
// logic...
}
},
}
</script>
Pass a reference to the previous question to every question:
If any question should be visible only when the previous question has been answered or viewed or something like that, you can pass that as a prop to every question, so they know wether they must render or not:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions" prev="activeQuestions[0]"/>
</div>
</div>
</template>
And in two.vue:
props: ['prev'],
computed: {
show: function() {
return this.prev && this.prev.status === 'ANSWERED';
// or some logic related to this, idk
}
},
just pass the whole data to the children:
As you coded it, you can just pass the whole questions data as a prop to every question component, then use it in a computed property. This is not what I would do, but just works, and since objects are references this is not necessarily unperformant.
Using a generic component:
It seems weird to have a one.vue, two.vue for every question, and sure does not scale well.
I'm not really sure how modular I can do them since the template for each question can be a bit different. Some have images or custom elements in them for example, while others don't.
If template are really different from each question to another, this can get complicated. However, if, as I suspect, they share common HTML structure, with a defined header or a common 'ask' button at the bottom and stuff like that, then you should be able to address this using Vue slots.
Apart from template issues, I suppose that every question in your app can get an arbitrary number of 'sub-questions' (as two.vue having question-2-a and question-2-b). This will require a complex and flexible data structure for the questions data (which will get more complex when you start to add multiple choices, multiple possible answers etc. etc.). This can get very complex but you should probably work on this until you can use a single question.vue component, this will surely pay out.
tip: avoid watchers
You're using v-model to answer in the two.vue template, then using a watcher to track changes in the answer variable and emit the event. This is convoluted and difficult to read, you can use #input or #change events on the <input> element instead:
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer" #input="emitAnswer">
And then instead of the watcher, have a method:
emitAnswer() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
This is a pretty broad question, but I'll try to give some useful guidance.
First data should be used for internal state. Very often, a component should use props for things you might think would be data it owns. That is the case here: the questions need to be coordinated by the parent, so the parent should own the data. That allows you to make a sensible function to control whether a question component displays.
Having the parent own the data also allows you to make one question component that configures itself according to its props. Or you might have a few different question component types (you can use :is to select the right one), but almost certainly some of them are reusable if you pass their question/answer/other info in.
To update answers, you will emit changes from the question components and let the parent actually change the value. I use a settable computed to allow the use of v-model in the component.
new Vue({
el: '#app',
data() {
return {
questions: [{
id: 1,
question: 'blah 1?',
answer: null
},
{
id: 2,
question: 'blah 2?',
answer: null,
// this is bound because data is a function
show: () => {
const q1 = this.questions.find((q) => q.id === 1);
return Boolean(q1.answer);
}
},
{
id: 3,
question: 'Shows anyway?',
answer: null
}
]
};
},
components: {
questionComponent: {
template: '#question-template',
props: ['props'],
computed: {
answerProxy: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('change', newValue);
}
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div class="c-troubleshooter">
<question-component v-for="q in questions" v-if="!q.show || q.show()" :props="q" #change="(v) => q.answer = v" :key="q.id">
</question-component>
</div>
<h2>State</h2>
<div v-for="q in questions" :key="q.id">
{{q.question}} {{q.answer}}
</div>
</div>
<template id="question-template">
<div>
{{props.question}}
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-a`" :name="`question-${props.id}`" value="ja" v-model="answerProxy">
<label :for="`question-${props.id}-a`">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-b`" :name="`question-${props.id}`" value="nej" v-model="answerProxy">
<label :for="`question-${props.id}-b`">Nej</label>
</div>
</div>
</template>

How to propagate a Vue.js event up the components chain?

I do have three components.
I don't have any influence on what the Datatable component does cause I have it from npm.
Now I want to send an event from EditButton to my Zonelist.
Zonelist component:
<template>
<datatable :columns="table_columns" :data="table_rows" filterable paginate v-on:remove="removeItem"></datatable>
</template>
<script>
import datatable from 'vuejs-datatable';
import moment from 'moment';
export default {
data() {
return {
table_columns: [
{label: "Zone", component: 'ZoneLink'},
{label: "Last updated", callback (row) {
let locale = $('html').closest('[lang]').attr('lang') || 'en';
moment.locale(locale);
return moment(row.last_updated).format('D. MMM YYYY');
}},
{label: '', component: 'EditButton'}
],
table_rows: [
{
"name": "xyz.de",
"last_updated": "2017-10-21 17:29:50"
}
],
form: {
name: '',
errors: []
}
};
},
components: {
datatable
},
methods: {
removeItem (item) {
this.table_rows.forEach((value, index, array) => {
if (value.name === item) {
Vue.delete(array, index);
}
});
}
}
}
</script>
Now my EditButton component $emit()'s the remove event with a parameter.
But nothing happens. So I think vue is not able to locate the listener.
(I'm using method shorthands from ES6 here)
How could I do this properly without mutating Zonelist's state via this.$parent.$parent from the EditButton?
Non parent-child communication in Vue is typically handled via either an event bus, or a state management system.
In this case, unless your application is more complex, the event bus is probably all you need. Since you are using single file components, you may need to declare the bus on the window, probably in your main script.
window.bus = new Vue()
Then in your EditButton, you can emit the event
bus.$emit('some-event', someData)
And in your ZoneList you can listen for it.
bus.$on('some-event', someData => this.doSomething(someData))
Another option is to ask DataTable to pass any and all events up by adding a v:on="$listeners" attribute to it.
See https://stackoverflow.com/a/61329264/578318 for a more detailed explanation.
Edit:
A much safer option would be to simply listen to the event in the parent class and pass it on...
<ancestor #message="console.log($event)"> <!-- cute trick see * -->
...
<parent #message="$emit('message', $event)"> <!-- passes it on -->
...
<child #click="$emit('Hello World')"> <!-- creates the event -->
* computed: {'console' : () => console}
By default, if a Vue component only has one node in it (i.e. no child nodes), then all events that you attach to it from the parent will fire just like if it were a regular HTML element.
But what if you have a child node that has children? In that case, you want to use v-on="$listeners"
Example:
<template>
<div v-on="$listeners">
<button>Hello world</button>
</div>
</template>
Docs: https://v2.vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components

Categories