反应中的肿状状态



我有一个带有一些相似之处的组件的React应用程序:

  1. 大多数组件在施工中加载来自Firebase的数据
  2. 大多数组件都有一个输入表格,用户可以与
  3. 进行交互
  4. 大多数组件都有简单的视图

我的问题是,当我试图将所有状态保持在最高级别的组件中时,状态就很难在早期进行管理。例如,我的组件下方是用户创建一个新产品,添加一些图像并将自定义标记放在其中一个图像上。

我当前对所有组件的设置是有一个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,或者至少阅读文档。

最新更新