我有一个带有一些相似之处的组件的React应用程序:
- 大多数组件在施工中加载来自Firebase的数据
- 大多数组件都有一个输入表格,用户可以与 进行交互
- 大多数组件都有简单的视图
我的问题是,当我试图将所有状态保持在最高级别的组件中时,状态就很难在早期进行管理。例如,我的组件下方是用户创建一个新产品,添加一些图像并将自定义标记放在其中一个图像上。
我当前对所有组件的设置是有一个CurrentEntry代表用户当前编辑的条目,我用空白状态初始化。
将所有状态保留在这样的顶部组件中是最好的做法,还是我应该重新考虑我的结构?
import React, { Component } from 'react';
import CreateEntryForm from "../../components/entries/createEntryForm";
import { withStyles } from 'material-ui/styles';
import ViewImageDialog from "../../components/entries/viewImageDialog";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import {generateFilename, removeItem, snapshotToArray} from "../../utils/utils";
import {
Redirect
} from 'react-router-dom';
import AppBar from "../../components/appBar";
import Spinner from "../../components/shared/spinner";
import firebase from 'firebase';
const styles = theme => ({
root: {
margin: theme.spacing.unit*2,
}
});
const initialFormState = {
currentProduct: null,
selectedProducts: [],
selectedUploads: [],
selectedMarkedImage: null,
productQuantity: '',
locationDescription: '',
comments: '',
currentUpload: null,
username: 'username'
};
const initialFormErrorState = {
selectProductError: '',
};
class CreateEntry extends Component {
constructor() {
super();
this.state = {
products: [],
job: null,
currentEntry: {...initialFormState},
formErrors: initialFormErrorState,
uploadLoading: false,
markedImageLoaded: false,
attachmentDialogOpen: false,
openAttachment: null,
markerPosition: null,
availableAttachments: [],
entries: [],
redirect: false,
loading: true,
isEditing: false
};
this.firebase = new FirebaseList('entries');
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.setMarker = this.setMarker.bind(this);
this.handleAttachmentDialogOpen = this.handleAttachmentDialogOpen.bind(this);
this.saveMarkedImage = this.saveMarkedImage.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleProgress = this.handleProgress.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.handleUploadSuccess = this.handleUploadSuccess.bind(this);
}
componentDidMount() {
this.firebase.path = `entries/${this.props.match.params.id}`;
this.jobId = this.props.match.params.id;
this.entryId = this.props.match.params.entry || null;
this.firebase.db().ref(`jobs/${this.props.match.params.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
job: job,
loading: false,
})
});
this.firebase.databaseSnapshot(`attachments/${this.jobId}`).then((snap) => {
const attachments = snapshotToArray(snap);
this.setState({availableAttachments: attachments})
});
this.firebase.databaseSnapshot(`entries/${this.jobId}`).then((snap) => {
const entries = snapshotToArray(snap);
const otherMarkedEntries = entries.filter(entry => entry.id !== this.entryId);
this.setState({otherMarkedEntries: otherMarkedEntries})
});
if (this.entryId) {
this.firebase.databaseSnapshot(`entries/${this.jobId}/${this.entryId}`).then((entry) => {
const updatedEntry = Object.assign({...initialFormState}, entry.val());
this.setState({
currentEntry: updatedEntry,
isEditing: !!this.entryId
})
});
}
}
validate() {
const errors = {...initialFormErrorState};
let isError = false;
if(this.state.currentEntry.selectedProducts.length === 0) {
errors.selectProductError = "You must select at least one product";
isError = true;
}
this.setState({formErrors: errors});
return isError
}
handleSubmit() {
const err = this.validate();
if(!err) {
if(this.state.job && this.state.currentEntry) {
if(!this.state.isEditing) {
const newEntry = {
...this.state.currentEntry,
'creationDate': Date.now()
};
let newEntryRef = this.firebase.db().ref(`entries/${this.jobId}`).push();
newEntryRef.set(newEntry);
if (this.state.currentEntry.selectedMarkedImage !== null) {
this.firebase.db().ref(`attachments/${this.jobId}/${newEntry.currentUpload.id}/markings/${newEntryRef.key}`)
.set(this.state.currentEntry.selectedMarkedImage)
}
this.setState({redirect: 'create'});
} else {
const updatedEntry = {
...this.state.currentEntry
};
const newLogEntry = {
'lastUpdated': Date.now(),
'updatedBy': 'username'
};
this.firebase.db().ref(`log/${this.jobId}/${this.entryId}`).push(newLogEntry);
this.firebase.update(this.entryId, updatedEntry)
.then(() => this.setState({redirect: 'edit'}));
}
}
}
};
handleInputChange = name => e => {
e.preventDefault();
const target = e.target;
const value = target.value;
if (name === 'currentUpload') {
this.handleAttachmentDialogOpen(this.state.job.selectedUploads);
}
this.setState({ currentEntry: { ...this.state.currentEntry, [name]: value } });
};
addSelectedChip = () => {
if (this.state.currentEntry.currentProduct) {
const updatedCurrentProduct = {
...this.state.currentEntry.currentProduct,
'productQuantity': this.state.currentEntry.productQuantity
};
const updatedSelectedProducts = [...this.state.currentEntry.selectedProducts, updatedCurrentProduct];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedProducts: updatedSelectedProducts,
currentProduct: null,
productQuantity: ''
};
this.setState({currentEntry: updatedEntryStatus});
}
};
handleRequestDeleteChip = (data, group) => {
const itemToChange = new Map([['product', 'selectedProducts'], ['upload', 'selectedUploads']]);
const selected = itemToChange.get(group);
const updatedSelectedItems = removeItem(this.state.currentEntry[selected], data.id);
const updatedEntryStatus = {
...this.state.currentEntry,
[selected]: updatedSelectedItems
};
this.setState({currentEntry: updatedEntryStatus});
};
handleAttachmentDialogOpen = (attachment) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: attachment
});
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
saveMarkedImage() {
const markedImage = {
'attachment': this.state.openAttachment[0],
'position': this.state.markerPosition
};
const updatedCurrentEntry = {
...this.state.currentEntry,
'selectedMarkedImage': markedImage
};
this.setState({
currentEntry: updatedCurrentEntry
});
this.handleAttachmentDialogClose()
}
setMarker(e) {
const dim = e.target.getBoundingClientRect();
const position = {
'pageX': e.pageX - dim.left -25,
'pageY': e.pageY - dim.top - 50
};
this.setState({markerPosition: position});
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
filterProducts(selected, available) {
if(this.state.job) {
const selectedProductNames = [];
selected.forEach(product => selectedProductNames.push(product.name));
return available.filter(product => !selectedProductNames.includes(product.name))
}
}
handleUploadStart = () => this.setState({uploadLoading: true, progress: 0});
handleProgress = (progress) => this.setState({progress});
handleUploadError = (error) => {
this.setState({uploadLoading: false});
console.error(error);
};
handleUploadSuccess = (filename) => {
firebase.storage().ref('images').child(filename).getDownloadURL().then(url => {
const getNameString = (f) => f.substring(0,f.lastIndexOf("_"))+f.substring(f.lastIndexOf("."));
const uploadItem = {"name": getNameString(filename), "url": url, "id": this.generateRandom()};
const updatedSelectedUploads = [...this.state.currentEntry.selectedUploads, uploadItem];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedUploads: updatedSelectedUploads
};
this.setState({
uploadLoading: false,
currentEntry: updatedEntryStatus
});
});
};
generateRandom() {
return parseInt(Math.random());
}
render() {
const {classes} = this.props;
const filteredProducts = this.filterProducts(this.state.currentEntry.selectedProducts, this.state.job && this.state.job.selectedProducts);
const title = this.state.isEditing ? "Edit entry for" : "Add entry for";
const redirectRoute = this.state.redirect
? `/entries/${this.props.match.params.id}/${this.state.redirect}`
: `/entries/${this.props.match.params.id}`;
return (
<section>
<AppBar title={`${title} ${this.state.job && this.state.job.jobId}`} route={`/entries/${this.props.match.params.id}`}/>
{this.state.loading
? <Spinner />
: <div className={classes.root}>
<ViewImageDialog open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.currentEntry.currentUpload}
setMarker={this.setMarker}
markerPosition={this.state.markerPosition || this.state.selectedMarkedImage && this.state.selectedMarkedImage.position}
saveMarkedImage={this.saveMarkedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
otherMarkedEntries={this.state.otherMarkedEntries}
/>
<CreateEntryForm handleInputChange={this.handleInputChange}
handleSubmit={this.handleSubmit}
availableProducts={filteredProducts}
addSelectedChip={this.addSelectedChip}
handleRequestDeleteChip={this.handleRequestDeleteChip}
job={this.state.job}
availableAttachments={this.state.availableAttachments}
uploadLoading={this.state.uploadLoading}
handleAttachmentDialogOpen={this.handleAttachmentDialogOpen}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
isEditing={this.state.isEditing}
handleProgress={this.handleProgress}
handleUploadError={this.handleUploadError}
handleUploadSuccess={this.handleUploadSuccess}
firebaseStorage={firebase.storage().ref('images')}
filename={file => generateFilename(file)}
otherMarkedEntries={this.state.otherMarkedEntries}
{...this.state.currentEntry}
{...this.state.formErrors}
/>
{this.state.redirect && <Redirect to={redirectRoute} push />}
</div>}
</section>
);
}
}
export default withStyles(styles)(CreateEntry);
集中式全球状态是需要全球全球应用程序的良好模式。对我来说,https://redux.js.org/是React应用程序的最佳状态引擎。
当我构建React/redux应用程序时,我倾向于以最低的组件级别存储状态,然后在需要时将其移至组件树,最后将其移至Global Redux状态。
例如,存储是否徘徊在盘旋上的状态可以存储在组件级别,因为它不会影响其他组件,但是存储是否打开模态的状态可能需要是在全球redux状态下,由于应用程序的其他部分需要知道这一点。
我真的建议尝试redux,或者至少阅读文档。