Use React state to manage list of already instanced objects? - javascript

I want to check each collection I instance to see if another instance of the same type already exists so I can use the stored version instead of creating a new one, how can I use the react state to keep a list to check against?
Sample code:
export default React.createBackboneClass({
getInitialState() {
return {
data: [],
groups: {}
}
},
componentWillMount() {
this.setState({
data: this.props.collection
});
},
render() {
const gridGroups = this.state.data.map((model) => {
let gridGroupsCollection = null;
if(this.state.groups[model.get('id')]) {
gridGroupsCollection = this.state.groups[model.get('id')];
} else {
gridGroupsCollection = this.state.groups[model.get('id')] = new GridGroupCollection([], {
groupId: model.get('id')
});
this.setState((previous) => {
groups: _.extend({}, previous, gridGroupsCollection)
});
}
return <GridGroupComponent
key={model.get('id')}
name={model.get('n')}
collection={gridGroupsCollection} />
});
return (
<div>
{gridGroups}
</div>
);
}
});

Few points. First of all, I would use the componentWillReviewProps to do the heavy work. Doing so on the render method can be more costly. I have yet to save instances on the state object. Is it needed for caching performance-wise or to solve a sorting issue?
Moreover, as noted already, calling setState from the render method will generate an infinite loop. as render is called when the state changes.
On a side note I would consider Redux for this, which offers several holistic ways to separate components from data manipulation.
update
This TodoMVC example might point you to the direction of how to combine react and backbone:
componentDidMount: function () {
// Whenever there may be a change in the Backbone data, trigger a
// reconcile.
this.getBackboneCollections().forEach(function (collection) {
// explicitly bind `null` to `forceUpdate`, as it demands a callback and
// React validates that it's a function. `collection` events passes
// additional arguments that are not functions
collection.on('add remove change', this.forceUpdate.bind(this, null));
}, this);
},
componentWillUnmount: function () {
// Ensure that we clean up any dangling references when the component is
// destroyed.
this.getBackboneCollections().forEach(function (collection) {
collection.off(null, null, this);
}, this);
}

Related

Vue 2: global event listener receives only once

To sumarize what I'm trying to achieve, I'm creating a calendar, where each day is a component instance, and inside each day there can be zero to multiple instances of a smaller component, named Task, kind of like this.
<Root>
<CalendarDay>
<Task></Task>
<Task></Task>
...
</CalendarDay>
<CalendarDay></CalendarDay>
...
</Root>
The number of smaller components to draw is dependent of a data array placed on the root component, which is expected to change multiple times via API calls.
The flow I had in mind is, first draw the CalendarDay components, then make an API call to obtain the Tasks that must be drawn on all days drawn, and finally fire a global event so that each CalendarDay retrieves the Tasks assigned to it. To achieve it, I added an eventBus to Vue following the steps of an answer of another question here, which I can't find, but basically is setting an extra property on the Vue prototype.
Vue.prototype.$eventHub = new Vue(); // Global event bus
Everything is set up, and on the first execution the API retrieves the Tasks, a global event is fired, and all CalendarDay components retrieve and draw the Tasks; but when a second global event is fired, the listeners either don't receive anything or they are not responding.
Here are the root and components
// ROOT Component
var app = new Vue({
el: '#appVue',
data: function() {
return {
...
tasks: [],
calendar_days: [] // Each element in this array is an object with 2 properties: numday and weekday
...
}
},
methods: {
db_get_tasks_month: function(){
REMOTE_WS.get(`<API route>`)
.then(response => {
this.tasks= response.data.tasks;
this.$eventHub.$emit('sort-tasks-in-days');
})
.catch(err => {
console.error('err');
});
},
...
}
})
// CalendarDay Component
Vue.component('calendar-day', {
props: {'day': Object}, //{numday, weekday}
data: function(){
return {
tasks: []
};
},
methods: {
sortTasksInDays: function(){
this.tasks= this.$root.$data.tasks.filter(t => {
return t.hasOwnProperty("data_ts") && new Date(t.data_ts).getDate() === this.day.numday;
});
}
},
created: function(){ this.$eventHub.$on('sort-tasks-in-days', this.sortTasksInDays) },
beforeDestroy: function(){ this.$eventHub.$off('sort-tasks-in-days'); }
});
Could someone point out what am I doing wrong?
Thanks,
EDIT: To add a bit more info, I checked if the global event is even firing, and using the Vue plugin on Chrome I can confirm that it is indeed firing, so what I'm missing must be on the listeners.
EDIT 2: Adding the property in ROOT where the calendar days are stored to display on a v-for
SOLVED: as Niels pointed out, passing the Tasks array as a prop was a solution. For future reference, the changes made to make it work are as follows:
// ROOT Component: The calendar_days data in root is this
data: function() {
return {
...
tasks: [],
calendar_days: [] // Each element in this array is an object with 3 properties: numday, weekday and tasks
...
}
},
methods: {
// The method db_get_tasks_month no longer does the global event emit
// The tasks are passed as a prop to each component
}
// CalendarDay Component
Vue.component('calendar-day', {
props: {
numday: { type: Number, default: 0},
weekday: { type: Number, default: 0},
tasks: { type: Array, default: []}
}
});
First of all, I was wondering why you are using a global event bus for this. This issue seems rather trivial and can be solved using component properties. This would create a more readable structure.
As it appears, every calendar day needs an array of Tasks. This can be defined in the 'props'-property of your CalendarDay. Your CalendarDay holds too much inforamtion now. It is using root-information whch is later on filtered out.
There's plenty of ways to get the data in the format you need but I think mapping whenever you fetch the data is the easiest. You map the response.data to the format:
days: {
'1': [],
'2': ['task A', 'Task B'],
...
}
Then it is a matter of looping over this object in your root-template and passing the tasks along as props (these are reactive).
<calendar-day v-for="(tasks, day) in days" :key="day" :tasks="tasks" :day-number="day"></calendar-day>

React.js: react on changes in external object

After reading official react.js documentation I understand how it should work in a good way, like
I have list of items in initial component state
adding new item through setState will update state and trigger update of UI
What should I do if I use external object as model like some global array which should be available for some not react.js parts of code OR could be modified with web sockets somewhere in future? Is calling ReactDOM.render after each action a good way? AFAIK it should work ok from performance point of view.
You still use setState:
let React = require('React');
let externalThing = require('tools/vendor/whoever/external-lib');
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = this.getInitialState();
}
getInitialState() {
// This assumes your external thing is written by someone who was
// smart enough to not allow direct manipulation (because JS has
// no way to monitor primitives for changes), and made sure
// to offer API functions that allow for event handling etc.
externalThing.registerChangeListener(() => this.updateBasedOnChanges(externalThing));
return { data: externalThing.data }
}
updateBasedOnChanges(externalThing) {
// note that setState does NOT automatically trigger render(),
// because React is smarter than that. It will only trigger
// render() if it sees that this new 'data' is different
// (either by being a different thing entirely, or having
// different content)
this.setState({
data: externalThing.data
});
}
render() {
// ...
}
}
If the external thing you're using is terribly written and you have to manipulate its data directly, your first step is to write an API for it so you don't directly manipulate that data.
let externalData = require('externaldata') // example: this is a shared array
let ExternalDataAPI = new ExternalDataAPI(externalData);
...
And then you make sure that API has all the update and event hooks:
class ExternalDataAPI {
constructor(data) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
update(...) {
// do something with data
this.listeners.forEach(fn => fn());
}
...
}
Alternatively, there are frameworks that already do this for you (flux, etc) but they also somewhat dictate how many more things "should be done" so that might be overkill for your need.
Since your question is about organizing your code in a manageable way, I would first of all suggest pairing ReactJS with a Flux-type framework, like Redux or Relay.
If you want to skip that for now, then you can organize your project using some react components at the top of the hierarchy for storing and retrieving data. For example, in such a component, in its componentWillMount method, you can start a setTimeout that periodically checks your global array and calls setState when appropriate. The render method should then contain child components that receive this state as props.
Below is an example. Obviously, the timers can be replaced by whichever method you use to subscribe to your data changes.
// your global object
var globalState = {name: "Sherlock Holmes"}
function onData(callback) {
setInterval(function(){
callback(globalState)
}, 1500)
}
var Child = React.createClass({
render: function() {
return <h1>Hello, {this.props.name}</h1>;
}
});
var Root = React.createClass({
getInitialState: function() {
return {}
},
componentWillMount: function() {
var that = this;
this.props.onData(function(data){
that.setState({external: data})
})
},
render: function() {
if (this.state.external)
return <Child name={this.state.external.name}/>
else
return <div>loading...</div>;
}
});
ReactDOM
.render(<Root onData={onData} />, document.getElementById('container'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>

What's the correct way to pass props as initial data in Vue.js 2?

So I want to pass props to an Vue component, but I expect these props to change in future from inside that component e.g. when I update that Vue component from inside using AJAX. So they are only for initialization of component.
My cars-list Vue component element where I pass props with initial properties to single-car:
// cars-list.vue
<script>
export default {
data: function() {
return {
cars: [
{
color: 'red',
maxSpeed: 200,
},
{
color: 'blue',
maxSpeed: 195,
},
]
}
},
}
</script>
<template>
<div>
<template v-for="car in cars">
<single-car :initial-properties="car"></single-car>
</template>
</div>
</template>
The way I do it right now it that inside my single-car component I'm assigning this.initialProperties to my this.data.properties on created() initialization hook. And it works and is reactive.
// single-car.vue
<script>
export default {
data: function() {
return {
properties: {},
}
},
created: function(){
this.data.properties = this.initialProperties;
},
}
</script>
<template>
<div>Car is in {{properties.color}} and has a max speed of {{properties.maxSpeed}}</div>
</template>
But my problem with that is that I don't know if that's a correct way to do it? Won't it cause me some troubles along the road? Or is there a better way to do it?
Thanks to this https://github.com/vuejs/vuejs.org/pull/567 I know the answer now.
Method 1
Pass initial prop directly to the data. Like the example in updated docs:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
But have in mind if the passed prop is an object or array that is used in the parent component state any modification to that prop will result in the change in that parent component state.
Warning: this method is not recommended. It will make your components unpredictable. If you need to set parent data from child components either use state management like Vuex or use "v-model". https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
Method 2
If your initial prop is an object or array and if you don't want changes in children state propagate to parent state then just use e.g. Vue.util.extend [1] to make a copy of the props instead pointing it directly to children data, like this:
props: ['initialCounter'],
data: function () {
return {
counter: Vue.util.extend({}, this.initialCounter)
}
}
Method 3
You can also use spread operator to clone the props. More details in the Igor answer: https://stackoverflow.com/a/51911118/3143704
But have in mind that spread operators are not supported in older browsers and for better compatibility you'll need to transpile the code e.g. using babel.
Footnotes
[1] Have in mind this is an internal Vue utility and it may change with new versions. You might want to use other methods to copy that prop, see How do I correctly clone a JavaScript object?.
My fiddle where I was testing it:
https://jsfiddle.net/sm4kx7p9/3/
In companion to #dominik-serafin's answer:
In case you are passing an object, you can easily clone it using spread operator(ES6 Syntax):
props: {
record: {
type: Object,
required: true
}
},
data () { // opt. 1
return {
recordLocal: {...this.record}
}
},
computed: { // opt. 2
recordLocal () {
return {...this.record}
}
},
But the most important is to remember to use opt. 2 in case you are passing a computed value, or more than that an asynchronous value. Otherwise the local value will not update.
Demo:
Vue.component('card', {
template: '#app2',
props: {
test1: null,
test2: null
},
data () { // opt. 1
return {
test1AsData: {...this.test1}
}
},
computed: { // opt. 2
test2AsComputed () {
return {...this.test2}
}
}
})
new Vue({
el: "#app1",
data () {
return {
test1: {1: 'will not update'},
test2: {2: 'will update after 1 second'}
}
},
mounted () {
setTimeout(() => {
this.test1 = {1: 'updated!'}
this.test2 = {2: 'updated!'}
}, 1000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app1">
<card :test1="test1" :test2="test2"></card>
</div>
<template id="app2">
<div>
test1 as data: {{test1AsData}}
<hr />
test2 as computed: {{test2AsComputed}}
</div>
</template>
https://jsfiddle.net/nomikos3/eywraw8t/281070/
I believe you are doing it right because it is what's stated in the docs.
Define a local data property that uses the prop’s initial value as its initial value
https://vuejs.org/guide/components.html#One-Way-Data-Flow
Second or third time I run into that problem coming back to an old vue project.
Not sure why it is so complicated in vue, but it can we done via watch:
export default {
props: ["username"],
data () {
return {
usernameForLabel: "",
}
},
watch: {
username: {
immediate: true,
handler (newVal, oldVal) {
this.usernameForLabel = newVal;
}
},
},
Just as another approach, I did it through watchers in the child component.
This way is useful, specially when you're passing an asynchronous value, and in your child component you want to bind the passed value to v-model.
Also, to make it reactive, I emit the local value to the parent in another watcher.
Example:
data() {
return {
properties: {},
};
},
props: {
initial-properties: {
type: Object,
default: {},
},
},
watch: {
initial-properties: function(newVal) {
this.properties = {...newVal};
},
properties: function(newVal) {
this.$emit('propertiesUpdated', newVal);
},
},
This way I have more control and also less unexpected behaviour. For example, when props that passed by the parent is asynchronous, it may not be available at the time of created or mounted lifecycle. So you can use computed property as #Igor-Parra mentioned, or watch the prop and then emit it.
Following up on Cindy's comment on another answer:
Be carful. The spread operator only shallow clones, so for objects
that contain objects or arrays you will still copy pointers instead of
getting a new copy.
Indeed this is the case. Changes within objects inside arrays will still propagate to your components even when a spread operator is employed.
Here was my solution (using Composition API):
setup() {
properties = ref([])
onMounted(() => {
properties.value = props.initialProperties.map((obj) => ({ ...obj }));
})
}
This worked to set the values and prevent them from getting changed, even if the data was changed in the parent component.

Aurelia binding hook on "nested" data update in custom element

I'm want to get notified, when a update happened in an binded object.
This plunk http://plnkr.co/edit/7thr0V demonstrates my problem.
In more detail:
I pass an object "data" via bind [data.bind] into a custom element. if i now
update a property in data, i would expect, that the "dataChanged" hook in the
custom element is called.
If i show property from the binded data object in the custom elements template, it gets updated, so the binding itself works properly.
My second reproach is using the ObserverLocator, but it doesn't fire on nested updates, too.
The object in app.js:
this.data = {
nested: {
content: "Hello nested world!"
}
};
The binding to custom element ce:
<require from="ce"></require>
<ce data.bind="data"></ce>
The ce.js part:
#bindable data;
constructor(observerLocator) {
this.observerLocator = observerLocator;
var subscription = this.observerLocator
.getObserver(this, 'data')
//.getObserver(this, 'data["nested"]["content"]') //Doesn't work
//.getObserver(this, 'data.nested.content') //Doesn't work
.subscribe(this.onChangeData);
}
onChangeData(newData, oldData) {
console.log('data changed from ', oldData, newData);
}
dataChanged(d) {
console.log("Changed", d);
}
The ce template part:
${data.nested.content}
In app.js I update the data object in 2 intervals.
The first interval every second edit a "nested" property.
The second interval every five seconds sets the data object new.
On the second interval, the hooks and the observer get called,
but I want a way to know, when the first intervals did any change.
setInterval(() => {
this.data.nested.content += "!";
}, 1000);
setInterval(() => {
this.data = {
nested: {
content: "Hello nested world No. " + this.counter++ + "!"
}
};
}, 5000);
The ObserverLocator is Aurelia's bare metal API for observing simple property changes and array/map/set mutation.
There's a new, higher level API called the BindingEngine that you can use to observe complex expressions.
Here's an example: https://gist.run?id=868a7611952b2e40f350
ce.html
<template>
${data.nested.content}
<!-- debug logging -->
<h4>Observed Changes:</h4>
<div repeat.for="change of changes"><pre><code>${change}</code></pre></div>
</template>
ce.js
import {
bindable,
BindingEngine,
inject
} from "aurelia-framework";
#inject(BindingEngine)
export class Ce {
#bindable data;
changes = []; // debug logging
constructor(bindingEngine) {
this.bindingEngine = bindingEngine;
}
expressionChanged(newValue, oldValue) {
// debug logging:
this.changes.splice(0, 0, `expressionChanged: "${newValue}"`);
}
syncSubscription(subscribe) {
if (this.subscription) {
this.subscription.dispose();
this.subscription = null;
}
if (subscribe && this.data) {
let observer = this.bindingEngine.expressionObserver(this.data, 'nested.content');
this.subscription = observer.subscribe(::this.expressionChanged);
}
}
dataChanged(newValue, oldValue) {
// subscribe to new data instance
this.syncSubscription(true);
// debug logging:
this.changes.splice(0, 0, `dataChanged: ${JSON.stringify(newValue, null, 2)}`);
}
attached() {
// subscribe
this.syncSubscription(true);
}
detached() {
// unsubscribe (avoid memory leaks)
this.syncSubscription(false);
}
}
Why doesn't aurelia observe whole objects for changes by default?
It's too expensive in terms of speed and memory to eagerly observe everything. Not all browsers support object.observe.

ParseReact.Mixin how do I listen for changes to this.data?

I have a React component that observes data on a Parse server.
mixins: [ParseReact.Mixin],
observe: function() {
var query = new Parse.Query('Item');
return {
items: query
};
}
In the render method I do receive my items and that work well. But, I want to be able to listen for when this.data.items will change it's value.
I'm aware of the regular lifecycle methods, but in them I have no way of checking if this.data.items is the same as before the update.
componentWillUpdate: function(nextProps, nextState) {},
componentDidUpdate: function(prevProps, prevState) {},
How do I do that?
You mean you need a "receiving data" callback?
Now there are no such a callback.
See this issue.
You can override "_receiveData" function in class extended ParseComponent by yourself though I'm not sure it's the right way for your situation.
_receiveData(name, value) {
this.data[name] = value;
delete this._pendingQueries[name];
delete this._queryErrors[name];
this.forceUpdate();
}

Categories

Resources