React-Redux重构容器逻辑

时间:2017-10-30 08:34:50

标签: javascript reactjs redux react-redux

我有一个容器连接到一个组件。它是一个选择建议组件。问题是我的容器和组件都得到了太多的重复逻辑,我想解决这个问题,可能是创建一个配置文件或从一个配置接收道具。

这是代码:

import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { goToPageRequest as goToPageRequestCompetitions  } from '../ducks/competitions/index';
import { getSearchParam as getSearchCompetitionsParam, getCompetitionsList } from '../ducks/competitions/selectors';
import { goToPageRequest as goToPageRequestIntermediaries } from '../ducks/intermediaries/index';
import { getSearchParam as getSearchIntermediariesParam, getIntermediariesList } from '../ducks/intermediaries/selectors';
import SelectBox2 from '../components/SelectBox2';


export const COMPETITIONS_CONFIGURATION = {
    goToPageRequest: goToPageRequestCompetitions(),
    getSearchParam: getSearchCompetitionsParam(),
    suggestions: getCompetitionsList()
};

export const INTERMEDIARIES_CONFIGURATION = {
    goToPageRequest: goToPageRequestIntermediaries(),
    getSearchParam: getSearchIntermediariesParam(),
    suggestions: getIntermediariesList()
};

const mapStateToProps = (state, ownProps) => ({
    searchString: ownProps.reduxConfiguration.getSearchParam(state),
});

const mapDispatchToProps = (dispatch, ownProps) => ({
    dispatchGoToPage: goToPageRequestObj =>
        dispatch(ownProps.reduxConfiguration.goToPageRequest(goToPageRequestObj)),
});

const mergeProps = (stateProps, dispatchProps, ownProps) => ({
    ...ownProps,
    search: searchParam => dispatchProps.dispatchGoToPage({
        searchParam
    }),
    ...stateProps
});

export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(SelectBox2));






import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Flex, Box } from 'reflexbox';
import classname from 'classnames';
import styles from './index.scss';
import Input from '../Input';
import { AppButtonRoundSquareGray } from '../AppButton';
import RemovableList from '../RemovableList';

const MIN_VALUE_TO_SEARCH = 5;
const NO_SUGGESTIONS_RESULTS = 'No results found';


class SelectBox extends Component {
    /**
     * Component setup
     * -------------------------------------------------------------------------*/
    constructor(props) {
        super(props);
        this.state = {
            displayBox: false,
            selection: null,
            value: '',
            items: [],
            suggestions: [],
        };
    }
    /**
     * Component lifecycle
     * -------------------------------------------------------------------------*/
    componentWillMount() {
        console.log(this.props);
        document.addEventListener('mousedown', this.onClickOutside, false);
        if (this.props.suggestionsType){
            if (this.props.suggestionsType === 'competition'){
                this.state.suggestions = this.props.competitionsSuggestions;
            }
            if (this.props.suggestionsType === 'intermediaries'){
                this.state.suggestions = this.props.intermediariesSuggestions;
            }
        }
    }
    componentWillUnmount() {
        console.log(this.props);
        document.removeEventListener('mousedown', this.onClickOutside, false);
    }
    componentWillReceiveProps(nextProps){
        console.log(this.props);
        if (this.props.suggestionsType === 'competition') {
             this.state.suggestions = nextProps.competitionsSuggestions;
         }
        if (this.props.suggestionsType === 'intermediaries') {
            this.state.suggestions = nextProps.intermediariesSuggestions;
        }
     }
    /**
     * DOM event handlers
     * -------------------------------------------------------------------------*/
    onButtonClick = (ev) => {
        ev.preventDefault();
        const itemIncluded = this.state.items.find(item => item.id === this.state.selection);
        if (this.state.selection && !itemIncluded) {
            const item =
                this.state.suggestions.find(suggestion => suggestion.id === this.state.selection);
            this.setState({ items: [...this.state.items, item] });
        }
    };
    onChangeList = (items) => {
        const adaptedItems = items
            .map(item => ({ label: item.name, id: item.itemName }));
        this.setState({ items: adaptedItems });
    };
    onClickOutside = (ev) => {
        if (this.wrapperRef && !this.wrapperRef.contains(ev.target)) {
            this.setState({ displayBox: false });
        }
    };
    onSuggestionSelected = (ev) => {
        this.setState({
            displayBox: false,
            value: ev.target.textContent,
            selection: ev.target.id });
    };
    onInputChange = (ev) => {
        this.generateSuggestions(ev.target.value);
    };
    onInputFocus = () => {
        this.generateSuggestions(this.state.value);
    };
    /**
     * Helper functions
     * -------------------------------------------------------------------------*/
    setWrapperRef = (node) => {
        this.wrapperRef = node;
    };
    executeSearch = (value) => {
        if (this.props.suggestionsType === 'competition'){
            this.props.searchCompetitions(value);
        }
        if (this.props.suggestionsType === 'intermediaries'){
            this.props.searchIntermediaries(value);
        }
    };
    generateSuggestions = (value) => {
        if (value.length > MIN_VALUE_TO_SEARCH) {
            this.executeSearch(value);
            this.setState({ displayBox: true, value, selection: '' });
        } else {
            this.setState({ displayBox: false, value, selection: '' });
        }
    };
    renderDataSuggestions = () => {
        const { listId } = this.props;
        const displayClass = this.state.displayBox ? 'suggestions-enabled' : 'suggestions-disabled';
        return (
            <ul
                id={listId}
                className={classname(styles['custom-box'], styles[displayClass], styles['select-search-box__select'])}
            >
                { this.state.suggestions.length !== 0 ?
                    this.state.suggestions.map(suggestion => (<li
                        className={classname(styles['select-search-box__suggestion'])}
                        onClick={this.onSuggestionSelected}
                        id={suggestion.get(this.props.suggestionsOptions.id)}
                        key={suggestion.get(this.props.suggestionsOptions.id)}
                    >
                        <span>{suggestion.get(this.props.suggestionsOptions.label)}</span>
                    </li>))
                    :
                    <li className={(styles['select-search-box__no-result'])}>
                        <span>{NO_SUGGESTIONS_RESULTS}</span>
                    </li>
                }
            </ul>
        );
    };

    renderRemovableList = () => {
        if (this.state.items.length > 0) {
            const adaptedList = this.state.items
                .map(item => ({ name: item.name, itemName: item.id }));
            return (<RemovableList
                value={adaptedList}
                className={classname(styles['list-box'])}
                onChange={this.onChangeList}
                uniqueIdentifier="itemName"
            />);
        }
        return '';
    };

    render() {
        const input = {
            onChange: this.onInputChange,
            onFocus: this.onInputFocus,
            value: this.state.value
        };
        return (
            <Flex className={styles['form-selectBox']}>
                <Box w={1}>
                    <div
                        ref={this.setWrapperRef}
                        className={styles['div-container']}
                    >
                        <Input
                            {...this.props}
                            input={input}
                            list={this.props.listId}
                            inputStyle={classname('form-input--bordered', 'form-input--rounded', styles.placeholder)}
                        />
                        { this.renderDataSuggestions() }
                    </div>
                </Box>
                <Box>
                    <AppButtonRoundSquareGray type="submit" className={styles['add-button']} onClick={this.onButtonClick}>
                        Add
                    </AppButtonRoundSquareGray>
                </Box>
                <Box>
                    { this.renderRemovableList() }
                </Box>
            </Flex>
        );
    }
}


SelectBox.propTypes = {
    items: PropTypes.instanceOf(Array),
    placeholder: PropTypes.string,
    listId: PropTypes.string,
    className: PropTypes.string
};

SelectBox.defaultProps = {
    items: [],
    placeholder: 'Choose an option...',
    listId: null,
    className: ''
};

export default SelectBox;

如您所见,在许多地方,我正在验证建议的类型并对此做些什么。它假设是一个可重用的组件,这个组件可以接受任何类型的建议。如果这种情况增长,将会有非常大的验证,我不希望如此。所以我认为我想要类似的东西:

const mapStateToProps = (state, ownProps) => ({
    searchString: ownProps.reduxConfiguration.getSearchParam(state),
});

const mapDispatchToProps = (dispatch, ownProps) => ({
    dispatchGoToPage: goToPageRequestObj =>
        dispatch(ownProps.reduxConfiguration.goToPageRequest(goToPageRequestObj)),
});

const mergeProps = (stateProps, dispatchProps, ownProps) => ({
    ...ownProps,
    search: searchParam => dispatchProps.dispatchGoToPage({
        searchParam
    }),
    ...stateProps
});

我怎样才能做出类似的东西?

1 个答案:

答案 0 :(得分:0)

以下是需要考虑的一些事项:

使用Redux的目的是从组件中删除状态逻辑。

你现在得到的东西Redux提供了一些状态你的组件提供一些状态。 这是反模式(不好)

// State from Redux: (line 22 - 24)
const mapStateToProps = (state, ownProps) => ({
  searchString: ownProps.reduxConfiguration.getSearchParam(state),
});


// State from your component: (line 65 - 71)
this.state = {
  displayBox: false,
  selection: null,
  value: '',
  items: [],
  suggestions: [],
};

如果你再看一下你的SelectBox组件 - 它正在做的很多事情是选择状态

// The component is parsing the state and choosing what to render (line 79 - 86)
if (this.props.suggestionsType){
  if (this.props.suggestionsType === 'competition'){
    this.state.suggestions = this.props.competitionsSuggestions;
  }
  if (this.props.suggestionsType === 'intermediaries'){
    this.state.suggestions = this.props.intermediariesSuggestions;
  }
}

原来,这正是mapStateToProps()。您应该将此选择逻辑移至mapStateToProps()。像这样:

const mapStateToProps = (state) => {
  let suggestions = null;
  switch (state.suggestionType) {
    case 'competition':
      suggestions = state.suggestions.competition;
      break;
    case 'intermediaries':
      suggestions = state.suggestions.intermediaries;
      break;
    default:
      break;
  }

  return {
    suggestions
  };
};

每次状态更新(在Redux中)它都会将新道具传递给您的组件。您的组件应该只关心如何呈现其状态的一部分。这引出了我的下一点:当你的应用程序状态全部由Redux管理而你的组件中没有状态逻辑时,你的组件可以只是函数(功能组件)。 / p>

const SelectBox3 = ({ suggestions }) => {
  const onClick = evt => { console.log('CLICK!'); };
  const list = suggestions.map((suggestion, index) => {
    return (
      <li key={index} onClick={onClick}>suggestion</li>
    );
  });

  return (
    <ul>
      {list}
    </ul>
  );
};

应用这些模式,您可以获得非常容易推理的组件,如果您希望将来维护此代码,这将是一件大事。

另外,顺便说一句,您不需要在示例中使用mergeProps()。 mapDispatchToProps可以只返回你的搜索功能,因为connect()将自动为你组装最终的道具对象。:

const mapDispatchToProps = (dispatch, ownProps) => ({
  // 'search' will be a key on the props object passed to the component!
  search: searchParam => {
    dispatch(ownProps.reduxConfiguration.goToPageRequest({ searchParam });
    // (also, your 'reduxConfiguration' is probably something that belongs in
    // the Redux state.) 
  }
}); 

我强烈建议给Redux文档一个很好的阅读。 Dan Abramov(和工作人员)做了很好的工作,并在那里解释了这些模式的原因。

以下是链接:Redux

另外,请查看async actions and redux-thunk处理异步调用(例如,在服务器上执行搜索)。

最后让我说:你走在正确的轨道上。继续努力,很快您就会知道为您的网络应用程序编写优雅功能组件的乐趣。祝你好运!