反应组件调用(history.replace(),history.push())-竞争条件?

时间:2019-07-15 07:38:19

标签: reactjs redux state

我有一个可以添加产品的应用程序。 这些产品将显示在摘要页面中,如下所示:

Summary page with toggle all and single toggle of products

出什么问题了? 默认情况下,应检查“摘要”页面上的产品。那是应该实现的,但并非在每种情况下都行得通。

基本上,我有一些反应路由器路线的导航。

这就是我所使用的:

"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-session": "^1.0.5",
"redux-thunk": "^2.3.0",

现在我正在使用history.push()和history.replace()重定向到摘要组件。

如果我是

  • 具有模态对话框(因此会稍有延迟),组件运行正常(将检查摘要中的所有产品)
  • 直接单击摘要路线,该组件将正常运行(将检查摘要中的所有产品)
  • 在我的history.push()或history.replace()重定向后重新加载页面(通过按f5或单击刷新)-组件运行正常(将检查摘要中的所有产品)
  • 单击另一条路线,然后单击摘要路线-该组件将正常运行(将检查摘要中的所有产品)

所以我认为必须有一个竞争条件,但我找不到它。

有人可以在这里帮忙吗?切换项目的相关功能是toggleAll()

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link, Redirect, withRouter } from 'react-router-dom';

// eslint-disable-next-line
import { hot } from 'react-hot-loader';

import Checkbox from '@material-ui/core/Checkbox';

import SnackBar from '@material-ui/core/Snackbar';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import Tooltip from '@material-ui/core/Tooltip';

import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';

import SaveConfigDialog from './SaveConfigDialog';
import ShareConfigDialog from './ShareConfigDialog';
import SaveConfigResultDialog from './SaveConfigResultDialog';

import EtailerLightbox from '../shared/etailer-lightbox';

import DeleteConfirmation from '../delete-confirmation';

import { removeProduct } from '../../store/product-selection/actions';

import Notification from '../notification';
import ProductLine from './product-line';

// import { getCookies, setCookie } from '../../utils/cookie';
import { sessionId, fireEvent } from '../../utils/gtm-helper';
import { getProductTitle } from '../../utils/product-helper';
import { addProductIds } from '../../data/service/product-cart-cookie';
import { translate } from '../../utils/translations';
import { icon } from '../../utils/icons';

import Selection from '../../data/model/Selection';
import ProductCollection from '../../data/model/product-collection';

import Configuration from '../../data/model/configuration';
import ConfigurationDataService from '../../data/service/ConfigurationDataService';
import { withConfigurationData } from '../../context/ConfigurationDataContext';

const propTypes = {
  // the selection of products to be rendered
  globalProductSelection: PropTypes.instanceOf(Selection).isRequired,
  // a function to be called when removing a product from the selection
  onRemoveProduct: PropTypes.func,
  // currently loaded Configuration can be null
  loadedConfiguration: PropTypes.instanceOf(Configuration),
  // the data service to use for saving the configuration
  configurationDataService: PropTypes.instanceOf(ConfigurationDataService)
    .isRequired,
};

const defaultProps = {
  // by default, do nothing
  onRemoveProduct: Function.prototype,
  loadedConfiguration: null,
};

/**
 * renders a summary of the current selection
 * @warning some mild refactoring might be in order
 * @todo find better names for the internal and global product selection
 * @todo document what the internal selection is being used for
 */
class Summary extends React.Component {
  constructor(props) {
    super(props);

    const { globalProductSelection } = this.props;

    const products = globalProductSelection.getProducts();

    this.state = {
      // indicates which products are selected inside this summary
      // do not confuse this with the selection that should actually be shown
      selected: new ProductCollection(...products.items.concat()),
      // true if the etailer lightbox should be shown
      showEtailerLightbox: false,
      showFeedback: false,
      // true if the "save configuration" dialog should be shown
      saveConfigDialog: false,
      // true if the "configuration saved" dialog should be shown
      saveConfigResultDialog: false,
      isRequestByShareCode: false,
      shareCodeNotAvailable: false,
      showCurrentSelectionGetsOverwrittenDialog: false,
    };

    this.shareCode = null;

    this.trackingSessions = {
      addToCartSession: null,
      compareSession: null,
      shareSession: null,
    };
  }

  componentDidMount() {
    const { isRequestByShareCode } = this.state;
    const { shareCode } = this.props;

    if (shareCode && !isRequestByShareCode) {
      this.setState({ isRequestByShareCode: true });
      this.shareCode = shareCode;
      this.checkForUnsavedCurrentSeletion();
    }
  }

  checkForUnsavedCurrentSeletion() {
    const { configurationDataService } = this.props;
    const currentSelectedProductAmount = parseInt(
      localStorage.getItem('product-advisor-productsamount') || '0',
      10
    );
    return configurationDataService
      .isThisShareCodeValid(this.shareCode)
      .then((isConfigValid) => {
        if (isConfigValid) {
          if (currentSelectedProductAmount > 0) {
            return this.showOverwriteDialog();
          }
          return this.handleCurrentSelectionIsEmpty();
        }
        return this.handleConfigIsNotValid();
      });
  }

  handleCurrentSelectionIsEmpty() {
    this.loadConfigIntoWizard(this.shareCode);
    const { history } = this.props;
    if (history.location.pathname.includes('share')) {
      history.replace('/selection/summary');
    }
  }

  handleConfigIsNotValid() {
    this.setState({ shareCodeNotAvailable: true });
  }

  showOverwriteDialog() {
    this.setState({ showCurrentSelectionGetsOverwrittenDialog: true });
  }

  loadConfigIntoWizard(shareCode) {
    const { configurationDataService } = this.props;

    return configurationDataService
      .getConfigurationByShareCode(shareCode)
      .then(configuration =>
        configurationDataService.loadIntoWizard(configuration));
  }

  /**
   * removes a product from the given selection and the internal selection as well
   * @param  {Product} product
   */
  removeProduct(product) {
    if (!product) return;

    this.setState(
      ({ selected }) => {
        selected.removeProduct(product);

        return {
          selected: new ProductCollection(...selected.items),
        };
      },
      () => this.props.onRemoveProduct(product)
    );
  }

  /**
   * empties the current selection (and the internal selection as well)
   */
  removeAllSelectedProducts() {
    this.state.selected.forEach(product => this.removeProduct(product));
    // deselect everything - everything was removed
    this.setState({ selected: new ProductCollection(...[]) });
  }

  /**
   * toggles the selection of all products
   * - if not all products are selected, select all
   * - if all products are selected, select none
   */
  toggleAll(forceShow) {
    const { globalProductSelection } = this.props;
    if (
      this.state.selected.length() <
        globalProductSelection.getProducts().length() ||
      forceShow
    ) {
      this.setState({
        selected: new ProductCollection(...globalProductSelection.getProducts().items),
      });
    } else {
      this.setState({
        selected: new ProductCollection(...[]),
      });
    }
  }

  /**
   * toggles the selection of the given product
   * @param  {Product} product
   */
  toggleSelection(product) {
    const currentSelection = this.state.selected;

    if (currentSelection.hasProduct(product)) {
      currentSelection.removeProduct(product);
    } else {
      currentSelection.addProduct(product);
    }

    this.setState({
      selected: new ProductCollection(...currentSelection.items),
    });
  }

  storeProductsToCookie() {
    /*
    to create a unique id within a cart for each
    configuration we use a sessionId for temp configs
    */
    addProductIds(
      this.state.selected.map(product => `${product.id}`),
      `Cart_${sessionId}`
    ).then(() => {
      /* Tracking */
      if (this.trackingSessions.addToCartSession !== `${sessionId}`) {
        this.trackingSessions.addToCartSession = `${sessionId}`;
        this.state.selected.forEach((product) => {
          fireEvent({
            category: 'Product Advisor_beta_Configurations',
            label: `Cart_${sessionId}`,
            action: `${product.name} - ${product.category.name ||
              product.productCategory}`,
          });
        });
      }
    });
  }

  handleCancelClick() {
    this.shareCode = null;
    this.setState({ showCurrentSelectionGetsOverwrittenDialog: false });
    this.props.history.goBack();
  }

  handleOkClick() {
    const { history } = this.props;
    this.loadConfigIntoWizard(this.shareCode).then(() => {
      const { globalProductSelection } = this.props;

      this.setState({
        selected: new ProductCollection(...globalProductSelection.getProducts().items),
        showCurrentSelectionGetsOverwrittenDialog: false,
      });
      if (history.location.pathname.includes('share')) {
        history.replace('/selection/summary');
      }
    });
  }

  buildDialog() {
    return (
      <Dialog open className="productAdvisor-shareConfig" maxWidth={false}>
        <DialogTitle>
          {translate('product.summary.shareConfigByUrl.selectionGetsOverwritten')}
        </DialogTitle>

        <DialogActions className="myDisplayActions">
          <Button
            color="primary"
            className="performConfigInsertOrReplaceOK"
            onClick={() => {
              this.handleOkClick();
            }}
          >
            {translate('product.summary.shareConfigByUrl.btn.overwrite')}
          </Button>
          <Button
            color="primary"
            className="button-gray btn performConfigInsertOrReplaceCancel"
            onClick={() => {
              this.handleCancelClick();
            }}
          >
            {translate('product.summary.shareConfigByUrl.btn.cancel')}
          </Button>
        </DialogActions>
      </Dialog>
    );
  }

  render() {
    const {
      globalProductSelection,
      loadedConfiguration,
      isDisabled,
    } = this.props;
    const {
      shareCodeNotAvailable,
      isRequestByShareCode,
      showCurrentSelectionGetsOverwrittenDialog,
    } = this.state;

    if (isDisabled) return <Redirect to="/selection/summary/" />;
    // if (isDisabled) return <Redirect to="/selection/camera" />;
    const products = globalProductSelection.getProductsOrderedByCategoryAndSubCategory();
    if (!globalProductSelection.hasProducts() && !isRequestByShareCode) {
      return <Notification message={translate('summary.noselection')} />;
    }

    if (shareCodeNotAvailable) {
      return <Notification message="ShareCode not available" />;
    }

    if (showCurrentSelectionGetsOverwrittenDialog && !!this.shareCode) {
      return this.buildDialog();
    }

    const firstSelectedProduct = this.state.selected.at(0);

    const { showEtailerLightbox } = this.state;

    const resultTableCSS = { position: 'relative' };
    return (
      <div className="type1 summary_content">
        <EtailerLightbox
          show={showEtailerLightbox}
          product={firstSelectedProduct}
          onClose={() => {
            this.setState({
              showEtailerLightbox: false,
            });
          }}
          onAddToCart={() => {
            this.storeProductsToCookie();
            this.setState({
              showEtailerLightbox: false,
            });
            // Website-Function to toggle cart
            window.addToCartTimeout();
          }}
        />

        <Table
          className="result-table product-advisor-table"
          style={resultTableCSS}
        >
          <TableHead>
            <TableRow>
              <TableCell className="summary-checkbox-column">
                <Checkbox
                  checked={this.state.selected.length() > 0}
                  disableRipple
                  color={
                    this.state.selected.length() === products.length()
                      ? 'secondary'
                      : 'default'
                  }
                  onChange={() => this.toggleAll()}
                />
              </TableCell>
              <TableCell colSpan={4}>
                <div className="table-header-row">
                  {this.state.selected.length > 0 ? (
                    <div className="selected-products">
                      <div className="selected-products-line-one">
                        {translate('summary.items.you_have')}
                      </div>
                      <div className="selected-products-line-two">
                        {this.state.selected.length()}
                        {this.state.selected.length() > 0
                          ? translate('summary.items.selected')
                          : translate('summary.item.selected')}
                      </div>
                    </div>
                  ) : (
                    ''
                  )}

                  <div className="table-header-buttons">
                    <Tooltip
                      title={
                        this.state.selected.length === 0
                          ? translate('summary.tooltips.add-to-cart')
                          : ''
                      }
                    >
                      <span>
                        <a
                          className="button btn-cta btn-white btn-flush-summary"
                          disabled={this.state.selected.length === 0}
                          onClick={() => {
                            this.setState({
                              openDeletionAlert: true,
                            });
                          }}
                        >
                          &#8203;
                          <div className="icn-external icn-alone">
                            <img
                              src={icon('general.details.trashorange')}
                              alt={translate('product.summary.flush')}
                            />
                          </div>
                        </a>
                      </span>
                    </Tooltip>

                    <Link
                      className="button btn-white"
                      to="/saved/configs/overview"
                    >
                      {translate('overview.myconfigs')}
                    </Link>

                    <a
                      className="button btn-white shareConfigInSummary"
                      size="small"
                      onClick={() => {
                        this.setState({ showShareConfigDialog: true });
                      }}
                    >
                      {translate('product.summary.shareConfig')}
                    </a>

                    <a
                      className="button btn-white compareConfigsInSummary"
                      disabled={this.state.selected.length === 0}
                      size="small"
                      onClick={() => {
                        const doTrack = () => {
                          if (
                            this.trackingSessions.compareSession !==
                            `${sessionId}`
                          ) {
                            this.trackingSessions.compareSession = `${sessionId}`;
                            this.state.selected.forEach((product) => {
                              fireEvent({
                                category: 'Product Advisor_beta_Configurations',
                                label: `WatchList_${sessionId}`,
                                // I'm not sure if we need the old getProductTitle call,
                                // so I'm leaving it in
                                // @TODO investigate if we actually need this
                                action: `${getProductTitle(product) ||
                                  product.name} - ${product.productCategory ||
                                  product.category.name}`,
                              });
                            });
                          }
                        };

                        this.setState({ saveConfigDialog: true });
                        doTrack();
                      }}
                    >
                      {translate('product.summary.compare')}
                    </a>
                    <Tooltip
                      title={
                        this.state.selected.length === 0
                          ? translate('summary.tooltips.add-to-cart')
                          : ''
                      }
                    >
                      <span>
                        <a
                          className="button add-to-cart"
                          disabled={this.state.selected.length === 0}
                          onClick={() => {
                            if (
                              this.state.selected.length() === 1 &&
                              this.state.selected.at(0).etailer
                            ) {
                              this.setState({
                                showEtailerLightbox: true,
                              });
                            } else {
                              this.storeProductsToCookie();
                              // Website-Function to toggle cart
                              window.addToCartTimeout();
                            }
                          }}
                        >
                          {translate('product.cart.addtocart')}
                          <div className="icn-external icn-add-to-cart">
                            <img
                              src={icon('general.details.addtocartwhite')}
                              alt={translate('product.cart.addtocart')}
                            />
                          </div>
                        </a>
                      </span>
                    </Tooltip>
                  </div>
                </div>
              </TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {products.map(product => (
              <ProductLine
                product={product}
                key={product.id}
                productIsSelected={this.state.selected.hasProduct(product)}
                onSelect={p => this.toggleSelection(p)}
                onRemoveProduct={removedProduct =>
                  this.removeProduct(removedProduct)
                }
              />
            ))}
          </TableBody>
        </Table>
        {this.state.saveConfigDialog && (
          <SaveConfigDialog
            configuration={loadedConfiguration}
            onClose={() => this.setState({ saveConfigDialog: false })}
            onSave={() => this.setState({ saveConfigResultDialog: true })}
          />
        )}
        {this.state.saveConfigResultDialog && (
          <SaveConfigResultDialog
            onClose={() =>
              this.setState({
                saveConfigResultDialog: false,
                saveConfigDialog: false,
              })
            }
          />
        )}
        <SnackBar
          autoHideDuration={5000}
          message={translate('summary.success-feedback')}
          open={this.state.showFeedback}
          disableWindowBlurListener
          anchorOrigin={{
            horizontal: 'center',
            vertical: 'center',
          }}
          onClose={() =>
            this.setState({
              showFeedback: false,
            })
          }
        />
        <DeleteConfirmation
          open={this.state.openDeletionAlert}
          onClose={() =>
            this.setState({
              openDeletionAlert: false,
            })
          }
          onConfirm={() =>
            this.setState(
              {
                openDeletionAlert: false,
              },
              () => this.removeAllSelectedProducts()
            )
          }
        />
        {this.state.showShareConfigDialog && (
          <ShareConfigDialog
            open={this.state.showShareConfigDialog}
            configuration={loadedConfiguration}
            saveOnComponentMount
            onClose={() =>
              this.setState({
                showShareConfigDialog: false,
              })
            }
          />
        )}
      </div>
    );
  }
}

Summary.propTypes = propTypes;
Summary.defaultProps = defaultProps;

const stateMapper = ({
  productSelection: { selected, loadedConfiguration },
}) => ({
  globalProductSelection: selected,
  loadedConfiguration,
});

export default hot(module)(connect(
  stateMapper,
  dispatch => ({
    onRemoveProduct: product => dispatch(removeProduct(product)),
  })
)(withRouter(withConfigurationData(Summary))));

0 个答案:

没有答案
相关问题