将ui相关属性存储在组件状态之外

时间:2018-06-12 12:21:37

标签: reactjs

我已经从使用纯jsons转变为实例。 意思是,我将实例存储在组件状态中。 问题是实例无法更新自己的属性,因为这意味着要改变状态,所以我需要克隆实例,从实例外部更新,然后设置状态。

我想将实例存储在组件上并存储一个虚拟状态属性,一旦实例属性发生更改,它就会更新将触发重新渲染的虚拟状态。

由于实例与UI有关,我知道它们应该位于该状态。

我知道这不是最佳做法,但我试图弄清楚它有多糟糕。

例如:

class Team {
  constructor(data) {
    this._teamMates = null;
    this.id = data.id;
  }

  get teamMates() {
    if (!this._teamMates) {
      this.fetchTeamMates();
      return null;
    }
    else 
      return this._teamMates;
  }

  fetchTeamMates() {
    fetch('/teamMates/' + this.id).then(teamMates => this._teamMates = teamMates);
  }
}

当我们第一次尝试获得teamMated时,它们无效,因此我们获取它们,并且在下一次状态更改时,我希望它们作为teamMates有效。

我可以返回一个promise而不是null,这是可能的,但我想以这种方式处理它,因为如果我们有teamMates,我想要条件渲染。

我知道有多种方法可以解决这个问题,但我宁愿尝试找到一种方法让它在不返回承诺的情况下发挥作用。

2 个答案:

答案 0 :(得分:0)

我不完全确定你的意思:

"问题是实例无法更新自己的属性,因为这意味着要改变状态,所以我需要克隆实例,从实例外部更新,然后设置状态。&# 34;

听起来最简单的事情是在父HOC中创建状态和方法,将它们传递给子组件并在事件处理程序中触发操作,然后它应该更新父状态并导致正常渲染孩子理想情况下,您的孩子将成为一个愚蠢的组件",字面上只是传递值和动作以及渲染。

答案 1 :(得分:0)

我认为你正在寻找一些州长,例如redux(你没有从你的问题中排除它)

如果你真的想要保留自己的实例(我不认为这是必要的,因为它没有必要,你正在处理状态),那么你应该创建一个不可变版本的类,也就是说,如果您更新团队中的玩家,则应返回团队实例的新版本,并更改​​播放器数据。

以下示例不使用实例,但它将团队配合移动到商店,并让redux管理状态(因此将其从组件状态中删除)。

要关注的主要关键点是:

connect语句,将组件与其props中注入的(部分)状态连接起来,并添加一个dispatcher来处理您发送给它的操作,如下所示:

const ConnectedTeamEditor = connect( teamStateToProps, playerDispatcher )( TeamEditor );

这需要一个状态映射器和调度程序,看起来非常像这样:

// dispatcher for the actions
const playerDispatcher = dispatch => ({
  fetch() {
    return getTeamMates().then( response => dispatch( { type: 'loaded', payload: response } ) );
  },
  update( player ) {
    dispatch( { type: 'update', payload: player } );
  }
});

// state mapper, sharing teamMates state over the connected components props
const teamStateToProps = state => ({ teamMates: state.teamMates });

要处理调度调用,我们需要一个还原器,然后注册到商店,我们使用react-redux提供的createStore方法。 reducer可以有一个默认状态

// reducer for player actions
const playerReducer = ( state = { teamMates: null }, action ) => {
  switch ( action.type ) {
    case 'loaded':
      return { teamMates: action.payload };
    case 'update':
      return { teamMates: state.teamMates.map( player => player.id === action.payload.id ? action.payload : player ) };
    default:
      return state;
  }
};

// and registration to store
const appStore = createStore( playerReducer );

React-redux会在状态更新后自动更新受影响的组件。需要注意的重要一点是,状态不应该发生变异,应该在需要时进行替换。如果找不到匹配的action.type,则返回状态也很重要。

最后,应用程序应该包含一个Provider,它通过其道具接收store,然后看起来就像那样。

ReactDOM.render( <Provider store={appStore}><ConnectedTeamEditor /></Provider>, target );

我确实保留了组件状态,但仅用于更新当前选定的播放器。

由于我们大多数人都处于WK心情,我有点自由设计,只需运行代码看看我的意思^ _ ^

&#13;
&#13;
const { createStore } = Redux;
const { Provider, connect } = ReactRedux;

// data provider
function getTeamMates() {
  return Promise.resolve([
    { id: 1, firstName: 'Romelu', lastName: 'Lukaku', position: 'forward' },
    { id: 2, firstName: 'Dries', lastName: 'Mertens', position: 'forward' },
    { id: 3, firstName: 'Eden', lastName: 'Hazard', position: 'forward' },
    { id: 4, firstName: 'Radja', lastName: 'Naingolan', position: 'midfield' },
    { id: 5, firstName: 'Kevin', lastName: 'De Bruyne', position: 'midfield' },
    { id: 6, firstName: 'Jordan', lastName: 'Lukaku', position: 'defender' },
    { id: 7, firstName: 'Axel', lastName: 'Witsel', position: 'midfield' },
    { id: 8, firstName: 'Vincent', lastName: 'Kompany', position: 'defender' },
    { id: 9, firstName: 'Thomas', lastName: 'Meunier', position: 'defender' },
    { id: 10, firstName: 'Marouane', lastName: 'Fellaini', position: 'midfield' },
    { id: 11, firstName: 'Thibaut', lastName: 'Courtois', position: 'Goalie' }
  ]);
}

// some container to translate the properties to readable names
const labels = {
  'id': '#',
  'firstName': 'First Name',
  'lastName': 'Last Name',
  'position': 'Position'
};

// small function to return the correct translation or default value
const translateProperty = ( property ) => labels[property] || property;

// a field that is either editable or just shows a span with a value
const Field = ({ isEditable, value, onChange }) => {
  if ( isEditable ) {
    return <span className="value">
      <input type="text" value={ value } onChange={ e => onChange( e.target.value ) }  />
    </span>;
  }
  return <span className="value">{ value }</span>;
};

// a single player, delegating changes to its data and selection style
const Player = ({ player, isSelected, onChange, onSelect }) => {
  return (
    <div className={classNames('row', { isSelected })} onClick={ () => !isSelected && onSelect && onSelect( player ) }>
    { Object.keys( player ).map( property => (
      <div className="cell" key={property}>
        <span className="label">{ translateProperty( property ) }</span>
        <Field 
          isEditable={isSelected && property !== 'id' }
          value={ player[property] }
          onChange={ newValue => onChange( {...player, [property]: newValue } ) }
        />
      </div>
    ) ) }
    </div>
  );
};

// reducer for player actions
const playerReducer = ( state = { teamMates: null }, action ) => {
  switch ( action.type ) {
    case 'loaded':
      return { teamMates: action.payload };
    case 'update':
      return { teamMates: state.teamMates.map( player => player.id === action.payload.id ? action.payload : player ) };
    case 'add':
      return { teamMates: state.teamMates.concat( [ {
        id: state.teamMates.reduce( (c, i) => c > i.id ? c : i.id, 0 ) + 1,
        ...action.payload } ] ) };
    default:
      return state;
  }
};

// dispatcher for the actions
const playerDispatcher = dispatch => ({
  fetch() {
    return getTeamMates().then( response => dispatch( { type: 'loaded', payload: response } ) );
  },
  update( player ) {
    dispatch( { type: 'update', payload: player } );
  },
  add( player ) {
    dispatch( { type: 'add', payload: player } );
  }
});

const teamStateToProps = state => ({ teamMates: state.teamMates });

// the team editor that works with props
class TeamEditor extends React.Component {
  constructor() {
    super();
    this.state = {
      selectedPlayer: null,
      newPlayer: null
    };
  }
  componentWillMount() {
    // load when mounting
    this.props.fetch();
  }
  selectPlayer( player ) {
    // component state keeps the selected player
    this.setState( { selectedPlayer: player } );
  }
  updatePlayer( player ) {
    // update player through dispatcher
    this.props.update( player );
  }
  
  onAddClicked() {
    this.setState( { 
      isAdding: true, 
      newPlayer: { 
        firstName: '', 
        lastName: '', 
        position: '' 
      } 
    } );
  }
  
  updateNewPlayer( player ) {
    this.setState( { newPlayer: player } );
  }
  
  savePlayer() {
    this.setState( { isAdding: false } , () => this.props.add( this.state.newPlayer ) );
  }
  
  cancelChanges() {
    this.setState( { isAdding: false } );
  }
  
  render() {
    const { teamMates} = this.props;
    const { selectedPlayer, isAdding, newPlayer } = this.state;
    return (
      <div className="team">
        <div className="row">
          { !isAdding && <button 
              type="button" 
              onClick={ () => this.onAddClicked() }>Add team member</button> 
          }
          { isAdding && <span>
              <button 
                type="button"
                onClick={ () => this.savePlayer() }>Save</button>
              <button 
                type="button"
                onClick={ () => this.cancelChanges() }>Cancel</button>
            </span> }
        </div>
        { isAdding && <div className="row">
          <Player 
            isSelected
            player={newPlayer}
            onChange={ (...args) => this.updateNewPlayer( ...args ) } />
        </div> }
        { teamMates && teamMates.map( player => (
          <Player 
            key={player.id} 
            isSelected={ !isAdding && selectedPlayer && selectedPlayer.id === player.id } 
            player={player} 
            onChange={ (...args) => this.updatePlayer( ...args ) } 
            onSelect={ (...args) => this.selectPlayer( ...args ) }
          /> ) ) }
      </div>
    );
  }
}

// connect the teameditor with state and dispatcher
const ConnectedTeamEditor = connect( teamStateToProps, playerDispatcher )( TeamEditor );

// create a simple store, no middleware
const appStore = createStore( playerReducer );

const target = document.querySelector('#container');
ReactDOM.render( <Provider store={appStore}><ConnectedTeamEditor /></Provider>, target );
&#13;
* { box-sizing: border-box; }
body { margin: 0; padding: 0; }
.row {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: space-evenly;
  align-content: center;
}
.row:hover {
  cursor: pointer;
}
.row.isSelected > .cell > * {
  background-image: linear-gradient( rgba( 225, 225, 225, 0.7 ), rgba( 255, 255, 255, 0.9 ) );
}
.cell {
  display: flex;
  flex-direction: column;
  flex-basis: 25%;
  flex-grow: 0;
  flex-shrink: 0;
  align-self: flex-start;
  background-color: yellow;
}
.cell:first-child {
  background-color: red;
}
.cell:last-child {
  background-color: black;
  color: #fff;
}
.cell > span {
  padding: 5px;
}
.cell > span > input {
  width: 100%;
}
.label {
  text-transform: capitalize;
}
&#13;
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
<script id="redux" src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<script id="react-redux" src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js"></script>
<div id="container"></div>
&#13;
&#13;
&#13;