3

Working on a small project using React and Redux in which I'm making what is essentially a Trello clone, or kanban system. But I'm having trouble figuring out how to construct my state for Redux so that things don't get weird.

Essentially, a user needs to be able to create multiple instances of a <Board/> component via React. Each <Board/> instance has a title. Continuing to drill down, each </Board> instance can also have an array of its own <List/> component instances. Each <List/> instance can in turn have its own <Card/> component instances.

Which is where I get confused. In React, it's simple--each instance of <Board/> and each instance of <List/> just manage their own state. But I can't figure out how to refactor everything so that Redux manages all state, but each component instance receives the correct slice of state.

So far, I've constructed my Redux state to look as follows. Any help is appreciated!

{
  boards: [
    {
      title: 'Home',
      lists: [
        {
          title: 'To Do',
          cards: [
            { title: 'Finish this project.'},
            { title: 'Start that project.'}
          ]
        },
        {
          title: 'Doing',
          cards: []
        },
        {
          title: 'Done',
          cards: []
        }
      ]
    }
  ]
}
B--rian
  • 5,578
  • 10
  • 38
  • 89
joehdodd
  • 371
  • 5
  • 15
  • what about creating some id's to your objects? and i would consider better names. i mean `title: 'To Do'` and `title: 'Doing'` seems like a `status` to me – Sagiv b.g Feb 25 '18 at 21:12
  • @Sagivb.g I like the ID idea. Right now I'm using each board title as a sort of ID. The list title keys are named as such because a user can give a list any title they want, not just the three in my example. – joehdodd Feb 25 '18 at 23:31

1 Answers1

5

redux is basically a one global store object. so theoretically this is no different than using react without redux but keeping the store at the most top level component's state.

Of course with redux we gain a lot of other goodies that makes it a great state manager. But for the sake of simplicity lets focus on the state structure and data flow between the react components.

Lets agree that if we have one global store to hold our single source of truth then we don't need to keep any local state inside our child components.
But we do need to break and assemble our data within our react flow, so a nice pattern is to create little bits of components that just get the relevant data an id and handlers so they can send back data to the parents with the corresponding id. This way the parent can tell which instance was the one invoking the handler.

So we can have a <Board /> that renders a <List /> that renders some <Cards /> and each instance will have it's own id and will get the data it needs.
Lets say we want to support addCard and toggleCard actions, We will need to update our store in couple level for this.

For toggling a card we will need to know:

  1. What is the Card id that we just clicked on
  2. What is the List id that this card belongs to
  3. What is the Board id that this list is belong to

For adding a card we will need to know:

  1. What is the List id that we clicked on
  2. What is the Board id that this list is belong to

Seems like the same pattern but with different levels.

To do that we will need to pass onClick events to each component and this component will invoke it while passing it's own id to the parent, in turn the parrent will invoke it's onClick event while passing the child's id and it's own id so the next parent will know which child instances were being clicked.

For example:
Card will invoke:

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

List will listen and will invoke:

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

Board wil llisten and will invoke:

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

Now our App can listen as well and by this time it will have all the necessary data it needs to perform the update:

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

From here its up to the reducers to do their job.

See how the components transfer data upwards, each component gather the data sent from its child and passing it upwards while adding another piece of data of itself. Small bits of data are assembled up until the top most component get all the data it needs to perform the updates to the state.

I've made a small example with your scenario, note that i'm not using redux due to the limitations of stack-snippets. I did wrote however the reducers and the entire logic of the updates and data flow, but i skipped the action creators part and connecting to an actual redux store.

I think it can give you some idea on how to structure your store, reducers and components.

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 StackOverflow.', 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>
Sagiv b.g
  • 30,379
  • 9
  • 68
  • 99
  • 1
    Wow! This is the most helpful and impressive answer I've ever received on Stack Overflow. Thank you so much for the time and effort you put into it. I was seeing things here and there about using UUIDs, but correctly applying it to my implementation wasn't sinking in. This is basically what I was trying to figure out. And also thanks for not being condescending! That seems to happen a lot and it bums me out. – joehdodd Feb 26 '18 at 02:59
  • Also, since I need the capability to add _multiple_ `` instances and multiple `` instances within those ``s, I would need similar functions/actions/reducers to your `onAddCard()` and `onCardClick()` methods, correct? – joehdodd Feb 26 '18 at 03:29
  • @joehdodd exactly. my code isn't complete, it's only half way written. It lacks the `boardReducer` and `boardsReducer`. i just wrote enough code to get you started on the right path. as you can see there is a return pattern here and all you need to do is assemble the pieces :) – Sagiv b.g Feb 26 '18 at 07:45
  • Got it! That makes sense. So am I correct that every action will be using the `uuidv4` function when generating a new instance of my components? Every ``, `` and `` instance will need a UUID, I'm assuming. – joehdodd Feb 26 '18 at 15:26
  • You should have a unique `id` for each object in my opinion and you should get it from somewhere, i just used the `uuidv4` function as an example (taken from [here](https://stackoverflow.com/a/2117523/3148807) by the way) you can use whatever method the fits your needs. – Sagiv b.g Feb 26 '18 at 15:40
  • Okay, I've got that figured out. But now I'm trying to implement it correctly with React Router so that You can be on a "Boards Home" route, but when you click a particular board, say ``, it loads the correct `` components. I think I'm overcomplicating. – joehdodd Feb 26 '18 at 17:22
  • To have a `` component identify itself I added this code to the component `const [thisBoard] = this.props.boards.filter(board => board.id === boardId).map(board => board)`. Now I can allow React Router to render the correct `Board` instance, and the component also renders the correct `` components. – joehdodd Feb 26 '18 at 18:50
  • Will do! This one is solved. And I have a new question about dragging and dropping cards between lists, like if you finish a task (card) and want to move it from the "Doing" list to the "Done" list. – joehdodd Feb 28 '18 at 14:11