用于管理多个 React 组件实例的 Redux 状态形状?

Redux state shape for managing multiple React component instances?

我正在使用 React 和 Redux 开发一个小项目,其中我正在制作一个本质上是 Trello 克隆或看板系统的东西。但是我无法弄清楚如何为 Redux 构建我的状态,这样事情就不会变得奇怪了。

本质上,用户需要能够通过 React 创建 <Board/> 组件的多个实例。每个 <Board/> 实例都有一个标题。继续向下钻取,每个 </Board> 个实例还可以有一个包含其自己的 <List/> 个组件实例的数组。每个 <List/> 个实例又可以有自己的 <Card/> 个组件实例。

这是我感到困惑的地方。在 React 中,这很简单——<Board/> 的每个实例和 <List/> 的每个实例只管理它们自己的状态。但我无法弄清楚如何重构所有内容,以便 Redux 管理 all 状态,但每个组件实例都会收到正确的 slice 状态。

到目前为止,我构建的 Redux 状态如下所示。感谢您的帮助!

{
  boards: [
    {
      title: 'Home',
      lists: [
        {
          title: 'To Do',
          cards: [
            { title: 'Finish this project.'},
            { title: 'Start that project.'}
          ]
        },
        {
          title: 'Doing',
          cards: []
        },
        {
          title: 'Done',
          cards: []
        }
      ]
    }
  ]
}

redux 基本上是一个全球商店 object。所以从理论上讲,这与使用 react 而不使用 redux 没有什么不同,但将商店保持在最顶级组件的状态。

当然,在 redux 中,我们获得了许多其他好东西,使它成为一个伟大的状态管理器。但是为了简单起见,让我们关注 React 组件之间的状态结构和数据流。

让我们同意,如果我们有一个全局存储来保存我们的单一事实来源,那么我们不需要在我们的 child 组件中保留任何本地状态。
但是我们确实需要在我们的反应流中打破和 assemble 我们的数据,所以一个很好的模式是创建一些组件,这些组件只获取相关数据的 id 和处理程序,以便它们可以将数据发送回 parents对应的id。这样 parent 就可以知道调用处理程序的是哪个实例。

所以我们可以有一个 <Board /> 渲染一个 <List /> 渲染一些 <Cards /> 并且每个实例都会有它自己的 id 并且会得到它需要的数据.
假设我们想要支持 addCardtoggleCard 操作,我们需要为此更新我们的商店。

要切换卡片,我们需要知道:

  1. 我们刚刚点击的 Card id 是什么
  2. 这张卡所属的 List id 是什么
  3. 此列表所属的 Board id 是什么

要添加卡片,我们需要知道:

  1. 我们点击的 List id 是什么
  2. 此列表所属的 Board id 是什么

似乎是相同的模式,但级别不同。

为此,我们需要将 onClick 事件传递给每个组件,该组件将调用它,同时将它自己的 id 传递给 parent,反过来,父项将在传递 child 的 id 它自己的 id 时调用它的 onClick 事件,所以下一个 parent 将知道哪些 child 个实例被点击了。

例如:
Card 将调用:

this.props.onClick(this.props.id)

List 将监听并调用:

onCardClick = cardId => this.props.onClick(this.props.id,cardId);

Board 将监听并调用:

onListClick = (listId, cardId) => this.props.onClick(this.props.id, listId, cardId)

现在我们的 App 也可以收听了,此时它将拥有执行更新所需的所有必要数据:

onCardToggle(boardId, listId, cardId) => dispatchToggleCard({boardId, listId, cardId})

从这里开始由减速器来完成他们的工作。

看看组件是如何向上传输数据的,每个组件收集来自它child的数据并向上传递,同时添加另一个自己的数据。少量数据 assembled 直到最顶层的组件获得执行状态更新所需的所有数据。

我已经用你的场景做了一个小例子,请注意,由于 stack-snippets 的限制,我没有使用 redux。然而,我确实编写了 reducer 以及更新和数据流的整个逻辑,但我跳过了 action creators 部分并连接到实际的 redux 商店。

我认为它可以让您了解如何构建存储、reducer 和组件。

function uuidv4() {
  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  )
}

class Card extends React.Component {
  onClick = () => {
    const { onClick, id } = this.props;
    onClick(id);
  }
  render() {
    const { title, active = false } = this.props;
    const activeCss = active ? 'active' : '';
    return (
      <div className={`card ${activeCss}`} onClick={this.onClick}>
        <h5>{title}</h5>
      </div>
    );
  }
}

class List extends React.Component {
  handleClick = () => {
    const { onClick, id } = this.props;
    onClick(id);
  }

  onCardClick = cardId => {
    const { onCardClick, id: listId } = this.props;
    onCardClick({ listId, cardId });
  }

  render() {
    const { title, cards } = this.props;
    return (
      <div className="list">
        <button className="add-card" onClick={this.handleClick}>+</button>
        <h4>{title}</h4>
        <div>
          {
            cards.map((card, idx) => {
              return (
                <Card key={idx} {...card} onClick={this.onCardClick} />
              )
            })
          }
        </div>
      </div>
    );
  }
}

class Board extends React.Component {
  onAddCard = listId => {
    const { onAddCard, id: boardId } = this.props;
    const action = {
      boardId,
      listId
    }
    onAddCard(action)
  }
  onCardClick = ({ listId, cardId }) => {
    const { onCardClick, id: boardId } = this.props;
    const action = {
      boardId,
      listId,
      cardId
    }
    onCardClick(action)
  }
  render() {
    const { title, list } = this.props;
    return (
      <div className="board">
        <h3>{title}</h3>
        {
          list.map((items, idx) => {
            return (
              <List onClick={this.onAddCard} onCardClick={this.onCardClick} key={idx} {...items} />
            )
          })
        }
      </div>
    );
  }
}


const cardRedcer = (state = {}, action) => {
  switch (action.type) {
    case 'ADD_CARD': {
      const { cardId } = action;
      return { title: 'new card...', id: cardId }
    }
    case 'TOGGLE_CARD': {
      return {
        ...state,
        active: !state.active
      }
    }
    default:
      return state;
  }
}

const cardsRedcer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_CARD':
      return [...state, cardRedcer(null, action)];
    case 'TOGGLE_CARD': {
      return state.map(card => {
        if (card.id !== action.cardId) return card;
        return cardRedcer(card, action);
      });
    }
    default:
      return state;
  }
}

const listReducer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_CARD': {
      const { listId } = action;
      return state.map(item => {
        if (item.id !== listId) return item;
        return {
          ...item,
          cards: cardsRedcer(item.cards, action)
        }
      });
    }
    case 'TOGGLE_CARD': {
      const { listId, cardId } = action;
      return state.map(item => {
        if (item.id !== listId) return item;
        return {
          ...item,
          cards: cardsRedcer(item.cards,action)
        }
      });
    }
    default:
      return state;
  }
}

class App extends React.Component {
  state = {
    boards: [
      {
        id: 1,
        title: 'Home',
        list: [
          {
            id: 111,
            title: 'To Do',
            cards: [
              { title: 'Finish this project.', id: 1 },
              { title: 'Start that project.', id: 2 }
            ]
          },
          {
            id: 222,
            title: 'Doing',
            cards: [
              { title: 'Finish Another project.', id: 1 },
              { title: 'Ask on Whosebug.', id: 2 }]
          },
          {
            id: 333,
            title: 'Done',
            cards: []
          }
        ]
      }
    ]
  }

  onAddCard = ({ boardId, listId }) => {
    const cardId = uuidv4();
    this.setState(prev => {
      const nextState = prev.boards.map(board => {
        if (board.id !== boardId) return board;
        return {
          ...board,
          list: listReducer(board.list, { type: 'ADD_CARD', listId, cardId })
        }
      })
      return {
        ...prev,
        boards: nextState
      }
    });
  }

  onCardClick = ({ boardId, listId, cardId }) => {
    this.setState(prev => {
      const nextState = prev.boards.map(board => {
        if (board.id !== boardId) return board;
        return {
          ...board,
          list: listReducer(board.list, { type: 'TOGGLE_CARD', listId, cardId })
        }
      })
      return {
        ...prev,
        boards: nextState
      }
    });
  }

  render() {
    const { boards } = this.state;
    return (
      <div className="board-sheet">
        {
          boards.map((board, idx) => (
            <Board
              id={board.id}
              key={idx}
              list={board.list}
              title={board.title}
              onAddCard={this.onAddCard}
              onCardClick={this.onCardClick}
            />
          ))
        }
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));
.board-sheet{
  padding: 5px;
}

.board{
  padding: 10px;
  margin: 10px;
  border: 1px solid #333;
}

.list{
  border: 1px solid #333;
  padding: 10px;
  margin: 5px;
}

.card{
    cursor: pointer;
    display: inline-block;
    overflow: hidden;
    padding: 5px;
    margin: 5px;
    width: 100px;
    height: 100px;
    box-shadow: 0 0 2px 1px #333;
}

.card.active{
  background-color: green;
  color: #fff;
}

.add-card{
  cursor: pointer;
  float: right;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>