Share processed state across components - redux

I have three components. My state has a property named state.selected.
Currently in my mapStateToProps I am doing this in all three components:
function mapStateToProps(state, ownProps) {
return { selected:state.selected }
}
In each presentational component I then do the same processing called getSelectedDisplays. This function does some processing based on what is selected.
var PresentaionalComponent_1 = React.createClass({
render: function() {
var displays = getSelectedDisplays();
// custom processing on `displays` for coponent 1
}
})
var PresentaionalComponent_2 = React.createClass({
render: function() {
var displays = getSelectedDisplays();
// custom processing on `displays` for coponent 2
}
})
var PresentaionalComponent_3 = React.createClass({
render: function() {
var displays = getSelectedDisplays();
// custom processing on `displays` for coponent 3
}
})
No control over parent component
I was hoping to avoid wrapping the three components in an extra div as my only need was to pass to them the result of getSelectedDisplays. I was hoping to avoid this:
React.createElement(OverContainer)
and OverContainer would be the only one receiving state.selected and it would then do getSelectedDisplays then it will render the three components with it as a prop:
var OverPresentaional = React.createClass({
render: function() {
var { selected } = this.props;
var display = getSelectedDisplays(selected);
return React.createElement('div', {},
React.createElement(PresentaionalComponent_1, { display });
React.createElement(PresentaionalComponent_2, { display });
React.createElement(PresentaionalComponent_3, { display });
);
}
}});
Is this possible without wrapping them in a parent div?

You can create a selector, that will encapsulate getting the data from the state, and computing derived properties:
export const getSelectedDisplays = (state) => {
const selected = state.selected;
const selectedDisplays = // whatever logic you need to get selectedDisplays from selected
return {
selectedDisplays;
};
};
Now for each component:
import { getSelectedDisplays } from 'selectorFile';
function mapStateToProps(state, ownProps) {
return getSelectedDisplays(state);
}
var PresentaionalComponent_1 = React.createClass({
render: function() {
var displays = this.props.selectedDisplays;
// custom processing on `displays` for coponent 1
}
})
etc...
The only problem is, that getting the data, and the logic will be performed 3 times, instead of ones. To solve that, you can create a memoized selector, that will cache and return the same result, if the supplied params (state in this case) haven't changed. Reselect is a library the creates memoized selectors for you.

Related

Vue doesn't update when computed data change

Context: I have a list of posts with tags, categories from wordpress api. I display these posts with Vue and using computed with a search box to filter the result based on titre, description, tags, and categories
Problem: I am trying to update a computed list when user click on a list of tag available. I add the get and set for computed data like this:
var vm = new Vue({
el: '#blogs',
data: {
search: '',
posts: [],
filterPosts: []
},
beforeMount: function() {
// It should call the data and update
callData();
},
computed: {
filterPosts: {
get: function() {
var self = this;
return self.posts.filter(function(post){
var query = self.search.toLowerCase();
var title = post.title.toLowerCase();
var content = post.content.toLowerCase();
var date = post.date.toLowerCase();
var categories = '';
post.categories.forEach(function(category) {
categories += category.name.toLowerCase();
});
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
return title.indexOf(query) !== -1 ||content.indexOf(query) !== -1 || date.indexOf(query) !== -1 || categories.indexOf(query) !== -1 || tags.indexOf(query) !== -1;
});
},
set: function (newValue) {
console.log(newValue);
this.filterPosts = Object.assign({}, newValue);
}
}
},
methods: {
filterByTag: function(tag, event) {
event.preventDefault();
var self = this;
self.filterPosts = self.posts.filter(function(post){
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
return tags.indexOf(tag.toLowerCase()) !== -1;
});
}
}
}); // Vue instance
The console.log always output new data based on the function I wrote on methods but Vue didn't re-render the view. I think I didn't do the right way or thought like Vue. Could you please give some insight?
Edit 1
Add full code.
I tried to add filterPosts in data but I received this error from Vue: The computed property "filterPosts" is already defined in data.
Your setter is actually not setting anything, it only logs the new value. You need to store it somewhere.
For example you can store it in the component's data:
data: {
value: 'foo',
},
computed: {
otherValue: {
get() { /*...*/ },
set(newVal) { this.value = newVal },
},
},
But this is definitely not the only possibility, if you use Vuex, the setter can dispatch an action that will then make the computed value get updated. The component will eventually catch the update and show the new value.
computed: {
value: {
get() {
return this.$store.getters.externalData;
},
set(newVal) {
return this.$store.dispatch('modifyingAction', newVal);
},
},
},
The bottomline is you have to trigger a data change in the setter, otherwise your component will not be updated nor will it trigger any rerender.
EDIT (The original answer was updated with full code):
The answer is that unless you want to manually change the list filteredPosts without altering posts, you don't need a get and set function for your computed variable. The behaviour you want can be acheived with this:
const vm = new Vue({
data() {
return {
search: '',
posts: [],
// these should probably be props, or you won't be able to edit the list easily. The result is the same anyway.
};
},
computed: {
filteredPosts() {
return this.posts.filter(function(post) {
... // do the filtering
});
},
},
template: "<ul><li v-for='post in filteredPosts'>{{ post.content }}</li></ul>",
});
This way, if you change the posts or the search variable in data, filteredPosts will get recomputed, and a re-render will be triggered.
After going around and around, I found a solution, I think it may be the right way with Vue now: Update the computed data through its dependencies properties or data.
The set method didn't work for this case so I add an activeTag in data, when I click on a tag, it will change the activeTag and notify the computed filterPost recheck and re-render. Please tell me if we have another way to update the computed data.
var vm = new Vue({
el: '#blogs',
data: {
search: '',
posts: [],
tags: [],
activeTag: ''
},
beforeMount: function() {
// It should call the data and update
callData();
},
computed: {
filterPosts: {
get: function() {
var self = this;
return self.posts.filter(function(post){
var query = self.search.toLowerCase();
var title = post.title.toLowerCase();
var content = post.content.toLowerCase();
var date = post.date.toLowerCase();
var categories = '';
post.categories.forEach(function(category) {
categories += category.name.toLowerCase();
});
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
var activeTag = self.activeTag;
if (activeTag !== '') {
return tags.indexOf(activeTag.toLowerCase()) !== -1;
}else{
return title.indexOf(query) !== -1 ||content.indexOf(query) !== -1 || date.indexOf(query) !== -1 || categories.indexOf(query) !== -1 || tags.indexOf(query) !== -1;
}
});
},
set: function (newValue) {
console.log(newValue);
}
}
},
methods: {
filterByTag: function(tag, event) {
event.preventDefault();
var self = this;
self.activeTag = tag;
}
}
}); // Vue instance
Try something like:
data: {
myValue: 'OK'
},
computed: {
filterPosts: {
get: function () {
return this.myValue + ' is OK'
}
set: function (newValue) {
this.myValue = newValue
}
}
}
More:
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter

Vuefire get Firebase Image Url

I am storing relative paths to images in my firebase database for each item I wish to display. I am having trouble getting the images to appear on the screen, as I need to get the images asynchronously. The firebase schema is currently as follows:
{
items: {
<id#1>: {
image_loc: ...,
},
<id#2>: {
image_loc: ...,
},
}
}
I would like to display each of these images on my page with code such as:
<div v-for="item in items">
<img v-bind:src="item.image_loc">
</div>
This does not work, as my relative location points to a place in firebase storage. The relavent code to get the true url from this relative url is:
firebase.storage().ref('items').child(<the_image_loc>).getDownloadURL()
which returns a promise with the true url. Here is my current vue.js code:
var vue = new Vue({
el: '.barba-container',
data: {
items: []
},
firebase: function() {
return {
items: firebase.database().ref().child('items'),
};
}
});
I have tried using computed properties, including the use of vue-async-computed, but these solutions do not seem to work as I cannot pass in parameters.
Basically, how do I display a list of elements where each element needs the result of a promise?
I was able to solve this by using the asyncComputed library for vue.js and by making a promise to download all images at once, instead of trying to do so individually.
/**
* Returns a promise that resolves when an item has all async properties set
*/
function VotingItem(item) {
var promise = new Promise(function(resolve, reject) {
item.short_description = item.description.slice(0, 140).concat('...');
if (item.image_loc === undefined) {
resolve(item);
}
firebase.storage().ref("items").child(item.image_loc).getDownloadURL()
.then(function(url) {
item.image_url = url;
resolve(item);
})
.catch(function(error) {
item.image_url = "https://placeholdit.imgix.net/~text?txtsize=33&txt=350%C3%97150&w=350&h=150";
resolve(item);
});
});
return promise;
}
var vue = new Vue({
el: '.barba-container',
data: {
items: [],
is_loading: false
},
firebase: function() {
return {
items: firebase.database().ref().child('items'),
};
},
asyncComputed: {
processedItems: {
get: function() {
var promises = this.items.map(VotingItem);
return Promise.all(promises);
},
default: []
}
}
});
Lastly, I needed to use: v-for="item in processedItems" in my template to render the items with image urls attached
I was able to solve it without any extra dependencies not adding elements to the array until the url is resolved:
in my template:
<div v-for="foo in foos" :key="foo.bar">
<img :src="foo.src" :alt="foo.anotherbar">
...
</div>
in my component (for example inside mounted())
const db = firebase.firestore()
const storage = firebase.storage().ref()
const _this = this
db.collection('foos').get().then((querySnapshot) => {
const foos = []
querySnapshot.forEach((doc) => {
foos.push(doc.data())
})
return Promise.all(foos.map(foo => {
return storage.child(foo.imagePath).getDownloadURL().then(url => {
foo.src = url
_this.foos.push(foo)
})
}))
}).then(() => {
console.log('all loaded')
})

React rendering recursion stops without error

I've encountered a problem with rendering some elements in React.
(I use ImmutableJS)
renderComponents: function(components) {
if(components.isEmpty()) return [];
var table = [];
components.map(function(component) {
table.push(<ComponentTableElement key={ component.get('id') } data={ component } />);
if(component.has('children')) {
var children = component.get('children');
table.concat(this.renderComponents(children));
}
});
return table;
},
As I looked for error, I found that this.renderComponents(children) doesn't return anything at all and the code somehow stops.
I mean before that line everything works ok, but then after this line, when i try to console.log something, it doesn't show up. And it doesn't even reach return table.
So what is wrong with that code?
In the context of the function you pass to map, this refers to the window object, not to the current component instance, so this.renderComponents is undefined when you try to call it.
components.map(function(component) {
this === window;
});
You can pass a value to use as this in the body of your function as the second parameter of Array::map.
components.map(function(component) {
table.push(<ComponentTableElement key={ component.get('id') } data={ component } />);
if(component.has('children')) {
var children = component.get('children');
// here, `this` refers to the component instance
table.concat(this.renderComponents(children));
}
}, this);
If you're using ES6, you can also use fat-arrow functions, which are automatically bound to this.
components.map((component) => {
table.push(<ComponentTableElement key={ component.get('id') } data={ component } />);
if(component.has('children')) {
var children = component.get('children');
// here, `this` refers to the component instance
table.concat(this.renderComponents(children));
}
});

data binding in react + signalR

I have a strategy question.
I want to change data in my website using signalR and display changed data using react. My question would be: How to perform data binding between signalR and react?
My first clue is the following:
signalR:
chat.client.addMessage = function (name, message) {
chatHistory.push({ Author: name, Text: message }); //here I change global variable chatHistory
};
react:
var CommentList = React.createClass({some class here});
var CommentBox = React.createClass({
componentRefresh: function () {
this.setState({ data: chatHistory });
},
getInitialState: function () {
return { data: chatHistory };
},
componentDidMount: function () {
this.componentRefresh();
setInterval(this.componentRefresh, this.props.interval);
},
render: function () {
return (
React.DOM.div(null,
CommentList({ data: this.state.data })
)
);
}
});
React.renderComponent(
CommentBox({ interval: 2000 }),
document.getElementById('content')
);
in react commentBox component I feed global chatHistory and ask for a new value every 2 seconds.
Is there more elegant way of doing it?
and how to avoid redrawing of CommentBox if chatHistory variable wasn't changed?
Your approach of maintaining state in CommentBox is fine. As your component base grows, it might become complicated to maintain self-updating components though. I recommend investigating the Flux architecture the React team designed and their Todo MVC Flux example in particular.
You could implement shouldComponentUpdate to prevent React from re-rendering the CommentBox if you know state hasn't changed. Also, you should keep a reference to the interval so you can clear it when the CommentBox is unmounted otherwise it will go on polling after the component is removed.
var CommentBox = React.createClass({
...
componentDidMount: function() {
this.componentRefresh();
this._interval = setInterval(this.componentRefresh, this.props.interval);
},
componentWillUnmount: function() {
clearInterval(this._interval);
this._interval = null;
},
shouldComponentUpdate: function(nextProps, nextState) {
// Do a deep comparison of `chatHistory`. For example, use
// Underscore's `isEqual` function.
return !_.isEqual(this.state.chatHistory, nextState.chatHistory);
},
...
});

Is it possible to display a UI transition to reflect changes in a collection with meteorjs?

I would like to display a pulse transition when my collection change.
In my html file, I have that:
<template name="menuItemTag">
<div class="app-menu-item-tag ui label">{{ count }}</div>
</template>
In my js file, I expose the count variable for my template like that:
Template.menuItemTag.count = function() {
return MyCollection.find().count();
};
With that the count change in the ui when the collection is updated.
Now, I would like to display a pulse transition on my div label each time the count value change.
I tried to use the cursor.observe
Template.menuItemTag.rendered = function() {
MyCollection.find().observe({
added: function (id, user) {
$('.app-menu-item-tag:nth(0)').transition('pulse');
},
removed: function () {
$('.app-menu-item-tag:nth(0)').transition('pulse');
}
});
};
Unfortunately, it is call too many times when the template is rendered the first time.
If initialy I have 40 items in my collection, the transition is played 40 times...
Is there a clean way for playing a ui transition on changes and avoid the collection initialisation?
Try this:
Template.menuItemTag.count = function() {
return Session.get('count');
};
Template.menuItemTag.rendered = function() {
this.countComputation = Deps.autorun(function() {
Session.set('count', MyCollection.find().count());
$('.app-menu-item-tag:nth(0)').transition('pulse');
});
};
Template.menuItemTag.destroyed = function() {
this.countComputation.stop();
};
Thanks sbking for your answer, I still have a problem on initialization of the collection.
I propose below to defer the first animation util the collection will be completely filled:
Template.menuItemTag.count = function() {
return Session.get('count');
};
Template.menuItemTag.rendered = function() {
var that = this;
this.countComputation = Deps.autorun(function() {
Session.set('count', MyCollection.find().count());
// Cancel playing UI transition. The collection is not completely initialized
if (that.handleTimeout) {
Meteor.clearTimeout(that.handleTimeout);
}
// Play the UI transition without the timeout if the collection is initialized
if (that.noNeedTimeoutAnymore) {
$('.app-menu-item-tag:nth(0)').transition('pulse');
}
// Tentative to play the UI transition during the collection feeding
else {
that.handleTimeout = Meteor.setTimeout(function() {
$('.app-menu-item-tag:nth(0)').transition('pulse');
// At this point we know that the collection is totaly initialized
// then we can remove the timeout on animation for the future update
that.noNeedTimeoutAnymore = true;
}, 1500); // You can adjust the delay if needed
}
});
};
Template.menuItemTag.destroyed = function() {
this.countComputation.stop();
};

Resources