Well.. I created a upload component, when user upload a image the component show a image preview using FileReader API.
But, if I've been using 3 components in another component, when I upload a image, this image is also repeated in 3 components.
Example:
... in render method
<UploadImage />
<UploadImage />
<UploadImage />
....
My component:
var React = require('react');
var Reflux = require('reflux');
// Actions
var actions = require('../../actions/Actions');
// Stores
var UploadStore = require('../../stores/ui/UploadStore');
var UI = require('material-ui');
var FlatButton = UI.FlatButton;
var Snackbar = UI.Snackbar;
var UploadImage = React.createClass({
mixins: [Reflux.connect(UploadStore, 'upload')],
propTypes: {
filename: React.PropTypes.string,
filesrc: React.PropTypes.string,
extensions: React.PropTypes.array.isRequired
},
getDefaultProps: function() {
return {
extensions: ['jpg', 'png', 'jpeg', 'gif']
};
},
_uploadImage: function () {
var file = {
file: this.refs.upload.getDOMNode().files[0] || false,
extensions: this.props.extensions
};
try {
actions.upload(file);
}
catch (e) {
console.log(e);
}
},
_uploadedImage: function() {
if (this.state.upload.filename) {
return (
<div className="upload-image">
<img src={this.state.upload.filesrc} />
<p>{this.state.upload.filename}</p>
</div>
);
}
},
render: function() {
return (
<div className="upload-image-container component-container">
<div className="upload-fields component-fields">
<h3>Imagem</h3>
<p>Arquivos PNG ou SVG no tamanho de XXXxYYYpx de até 50kb.</p>
<FlatButton label="Selecionar Imagem" className="upload-button">
<input
type="file"
id="imageButton"
className="upload-input"
ref="upload"
onChange={this._uploadImage} />
</FlatButton>
</div>
{this._uploadedImage()}
</div>
);
}
});
module.exports = UploadImage;
My store:
var Reflux = require('reflux');
var actions = require('../../actions/Actions');
var UploadStore = Reflux.createStore({
listenables: [actions],
data: {
filename: '',
filesrc: ''
},
getInitialState: function() {
return this.data;
},
onUpload: function (f) {
if (f) {
// Check extension
var extsAllowed = f.extensions;
if (this.checkExtension(extsAllowed, f.file.name)) {
// Crate the FileReader for upload
var reader = new FileReader();
reader.readAsDataURL(f.file);
reader.addEventListener('loadend', function() {
this.setData({
uploaded: true,
filename: f.file.name,
filesrc: reader.result
});
}.bind(this));
reader.addEventListener('error', function () {
actions.error('Não foi possível ler o seu arquivo. Por favor, verifique se enviou o arquivo corretamente.');
}.bind(this));
}
else {
actions.error('O arquivo que você está tentando enviar não é válido. Envie um arquivo nas seguintes extensões: ' + extsAllowed.join(', ') + '.');
}
}
else {
actions.error('File object not found.');
}
},
checkExtension: function (extensions, filename) {
var fileExt = filename.split('.').pop().toLowerCase();
var isSuccess = extensions.indexOf(fileExt) > -1;
if (isSuccess) return true;
return false;
},
setData: function(data) {
this.data = data;
this.trigger(data);
}
});
module.exports = UploadStore;
The result:
Any idea?
Thanks!
unfortunately the store behaves like a singleton, i.e there is only one UploadStore instance.
What you can do is introduce an extra parameter to keep the uploads apart. Your store will now take an array of uploads but each upload is tagged with the category and your component will also have a category and takes only the images from the store which belong to the same category. This is done using the Reflux.connectFilter
mixin.
First, I'd separate the uploaded image into its own component like this:
var UploadedImage = React.createClass({
propTypes: {
upload: React.PropTypes.object.isRequired
},
render: function() {
return (
<div className="upload-image">
<img src={this.props.upload.filesrc} />
<p>{this.props.upload.filename}</p>
</div>
);
}
});
Then we have to change some stuff inside your UploadImage
component so that it will filter by category:
var UploadImage = React.createClass({
// only select those uploads which belong to us
mixins: [
Reflux.connectFilter(UploadStore, "uploads", function(uploads) {
return uploads.filter(function(upload) {
return upload.category === this.props.category;
}.bind(this))[0];
})
],
propTypes: {
filename: React.PropTypes.string,
filesrc: React.PropTypes.string,
extensions: React.PropTypes.array.isRequired,
// an additional prop for the category
category: React.PropTypes.string.isRequired
},
_uploadImage: function () {
var file = {
file: this.refs.upload.getDOMNode().files[0] || false,
extensions: this.props.extensions
};
try {
// pass in additional parameter!
actions.upload(file, this.props.category);
}
catch (e) {
console.log(e);
}
},
render: function() {
return (
<div className="upload-image-container component-container">
<div className="upload-fields component-fields">
<h3>Imagem</h3>
<p>Arquivos PNG ou SVG no tamanho de XXXxYYYpx de até 50kb.</p>
<FlatButton label="Selecionar Imagem" className="upload-button">
<input
type="file"
id="imageButton"
className="upload-input"
ref="upload"
onChange={this._uploadImage} />
</FlatButton>
</div>
{this.state.uploads.map(function(upload, index) {
return <UploadedImage key={index} upload={upload}/>;
})}
</div>
);
}
});
And your store now holds an array of "file" objects, each tagged with a category:
var UploadStore = Reflux.createStore({
listenables: [actions],
// data is now an array of objects
data: [],
getInitialState: function() {
return this.data;
},
// here we get the file + category
onUpload: function (f, category) {
if (f) {
// Check extension
var extsAllowed = f.extensions;
if (this.checkExtension(extsAllowed, f.file.name)) {
// Crate the FileReader for upload
var reader = new FileReader();
reader.readAsDataURL(f.file);
reader.addEventListener('loadend', function() {
this.setData(this.data.concat([{
uploaded: true,
filename: f.file.name,
filesrc: reader.result,
category: category /* adding category here */
}]));
}.bind(this));
reader.addEventListener('error', function () {
actions.error('Não foi possível ler o seu arquivo. Por favor, verifique se enviou o arquivo corretamente.');
}.bind(this));
}
else {
actions.error('O arquivo que você está tentando enviar não é válido. Envie um arquivo nas seguintes extensões: ' + extsAllowed.join(', ') + '.');
}
}
else {
actions.error('File object not found.');
}
},
checkExtension: function (extensions, filename) {
var fileExt = filename.split('.').pop().toLowerCase();
var isSuccess = extensions.indexOf(fileExt) > -1;
if (isSuccess) return true;
return false;
},
setData: function(data) {
this.data = data;
this.trigger(data);
}
});
And finally in your view you can use the UploadImage
component like this:
I wrote the code on the fly, so there might be some issues - but it's more about the concept. Also it's possible to upload more than one image per category now, if this is not desired, then think about replacing the array in the store with a hash map, so that the keys correspond to the category - then only one image can be uploaded in each category.
Answer to your comment
Maybe you could get away with a factory method for the store, i.e something like this:
var UploadStoreFactory = function() {
return Reflux.createStore({
/* your existing code as it was originally */
});
};
var UploadImage = React.createClass({
mixins: [Reflux.connect(UploadStoreFactory(), 'upload')],
/* your existing code as it was originally */
});
but I suspect that your action will trigger all instances of your upload stores, but it's worth a try. But this comes with a lot of drawback such as other components can not listen to this store easily.
In this stackoverflow a similar question is asked, and also the conceptual right way to do it is to use one bucket/store for all and keep the items in the store tagged so that you can keep them apart.
Keep in mind that stores also get cleared an refilled, as an example if you create a web-shop with products and different categories, you clear and refill the ProductStore
everytime the user switches to another category. If you additionally have a sidebar that maybe shows "Products you might like" then I'd model this as a separate store, i.e the ProductSuggestionStore
but both contains objects of the type "Product".
If the stores behave semantically different but share a lot of the upload logic you could also try to build a base prototype/class for your stores and then extend the specific stores or outsource the upload logic into a service class.
If you are worried about performance, i.e one upload causes all components to re-render, then you can add a check within shouldComponentUpdate
.
A good example why to use only one store might be the case where the user wants to close the window but somewhere on your website an upload is still pending, then your main application view just has to check one store. Also uploads could be queued easily so that the bandwidth is not exhausted, since all uploads go through one store.
Also keep in mind you can have stores that listen to other stores, as an example the UploadHistoryStore
keeps a timestamped record of the last 10 uploads. All uploads go into the same bucket but if you have a "Last 10 Uploads" component it just has to listen to the "UploadHistoryStore"
var UploadStore = Reflux.createStore({
/* ... upload stuff and trigger as usual ... */
});
var UploadHistoryStore = Reflux.createStore({
// keep the last ten uploads
historyLength: 10,
init: function() {
// Register statusStore's changes
this.listenTo(UploadStore, this.output);
this.history = [];
},
// Callback
output: function(upload) {
this.history.push({
date: new Date(), // add a date when it was uploaded
upload: upload // the upload object
}).slice(1, this.historyLength);
// Pass the data on to listeners
this.trigger(this.history);
}
});