0

I've been battling with this for too long, can someone point me to the right direction? The issue: When i create a list I am able to update/delete it. I am also able to add items to it and update/delete these items. When I add another list, the items from the former gets carried over to the latter then I am unable to edit the items. If I delete the List and don't refresh the browser the items are still in the list. I need a way to tie the two together in a way that a list only knows about its items Thanks in advance for the help.

/actions/lists.js

export const CREATE_LIST = 'CREATE_LIST'
export function createList(list) {
  return {
    type: CREATE_LIST,
    id: uuid.v4(),
    items: list.items || [],
    ...list
  }
}

export const CONNECT_TO_LIST = 'CONNECT_TO_LIST'
export function connectToList(listId, itemId) {
  return {
    type: CONNECT_TO_LIST,
    listId,
    itemId
  }
}

export const DISCONNECT_FROM_LIST = 'DISCONNECT_FROM_LIST'
export function disconnectFromList(listId, itemId) {
  return {
    type: DISCONNECT_FROM_LIST,
    listId,
    itemId
  }
}

/actions/items.js

export const CREATE_ITEM = 'CREATE_ITEM'
export function createItem(item) {
  return {
    type: CREATE_ITEM,
    item: {
      id: uuid.v4(),
      ...item
    }
  }
}

export const UPDATE_ITEM = 'UPDATE_ITEM'
export function updateItem(updatedItem) {
  return {
    type: UPDATE_ITEM,
    ...updatedItem
  }
}

/reducers/lists.js

import * as types from '../actions/lists'

const initialState = []

export default function lists(state = initialState, action) {
  switch (action.type) {
    case types.CREATE_LIST:
      return [
        ...state,
        {
          id: action.id,
          title: action.title,
          items: action.items || []
        }
      ]

    case types.UPDATE_LIST:
      return state.map((list) => {
        if(list.id === action.id) {
          return Object.assign({}, list, action)
        }

        return list
      })

    case types.CONNECT_TO_LIST:
      const listId = action.listId
      const itemId = action.itemId

      return state.map((list) => {
        const index = list.items.indexOf(itemId)

        if(index >= 0) {
          return Object.assign({}, list, {
            items: list.items.length > 1 ? list.items.slice(0, index).concat(
              list.items.slice(index + 1)): []
          })
        }
        if(list.id === listId) {
          return Object.assign({}, list, {
            items: [...list.items, itemId]
          })
        }

        return list
      })

    case types.DISCONNECT_FROM_LIST:
      return state.map((list) => {
        if(list.id === action.listId) {
          return Object.assign({}, list, {
            items: list.items.filter((id) => id !== action.itemId)
          })
        }

        return list
      })

    default:
      return state
  }
}

/reducers/items.js

import * as types from '../actions/items'

const initialState = []

export default function items(state = initialState, action) {
  switch (action.type) {
    case types.CREATE_ITEM:
      return [ ...state, action.item ]

    case types.UPDATE_ITEM:
      return state.map((item) => {
        if(item.id === action.id) {
          return Object.assign({}, item, action)
        }

        return item
      })

    case types.DELETE_ITEM:
      return state.filter((item) => item.id !== action.id )

    default:
      return state
  }
}

/components/List.jsx

import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import Items from './Items'
import Editor from './Editor'
import * as listActionCreators from '../actions/lists'
import * as itemActionCreators from '../actions/items'

export default class List extends React.Component {
  render() {
    const { list, updateList, ...props } = this.props
    const listId = list.id

    return (
      <div {...props}>
        <div className="list-header"
          onClick={() => props.listActions.updateList({id: listId, isEditing: true})}
        >
          <div className="list-add-item">
            <button onClick={this.addItem.bind(this, listId)}>+</button>
          </div>
          <Editor 
            className="list-title"
            isEditing={list.isEditing}
            value={list.title}
            onEdit={title => props.listActions.updateList({id: listId, title, isEditing: false})} 
          />
          <div className="list-delete">
            <button onClick={this.deleteList.bind(this, listId)}>x</button>
          </div>
        </div>
        <Items
          items={this.listItems}
          onValueClick={id => props.itemActions.updateItem({id, isEditing: true})}
          onEdit={(id, text) => props.itemActions.updateItem({id, text, isEditing: false})}
          onDelete={itemId => this.deleteItem(listId, itemId)}
        />
      </div>
    )
  }

  listItems() {
    props.list.items.map(id => state.items[
      state.items.findIndex(item => item.id === id)
    ]).filter(item => item)
  }

  deleteList(listId, e) {
    e.stopPropagation()

    this.props.listActions.deleteList(listId)
  } 

  addItem(listId, event) {
    event.stopPropagation()

    const item = this.props.itemActions.createItem({
      text: 'New Shopping Item'
    })

    this.props.listActions.connectToList(listId, item.id)
  }

  deleteItem(listId, itemId) {
    this.props.listActions.disconnectFromList(listId, itemId)
    this.props.itemActions.deleteItem(itemId)
  }
}

function mapStateToProps(state) {
  return {
    lists: state.lists,
    items: state.items
  }
}

function mapDispatchToProps(dispatch) {
  return {
    listActions: bindActionCreators(listActionCreators, dispatch),
    itemActions: bindActionCreators(itemActionCreators, dispatch)
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(List)

/components/List.jsx

import React from 'react'
import List from './List.jsx'

export default ({lists}) => {
  return (
    <div className="lists">{lists.map((list) =>
      <List className="list" key={list.id} list={list} id={list.id} />
    )}</div>
  )
}

/components/Items.jsx

import React from 'react'
import { connect } from 'react-redux'
import Editor from './Editor'
import Item from './Item'

export default class Items extends React.Component {
  render () {
    const {items, onEdit, onDelete, onValueClick, isEditing} = this.props

    return (
      <ul className="items">{items.map(item =>
        <Item 
          className="item"
          key={item.id}
          id={item.id}
          isEditing={item.isEditing}>
          <Editor
            isEditing={item.isEditing}
            value={item.text}
            onValueClick={onValueClick.bind(null, item.id)}
            onEdit={onEdit.bind(null, item.id)}
            onDelete={onDelete.bind(null, item.id)}
          />
        </Item>
      )}</ul>
    )
  }
}

export default connect(
  state => ({
    items: state.items
  })
)(Items)

/components/Item.jsx

import React from 'react'

export default class Item extends React.Component {
  render() {
    const { id, isEditing, ...props } = this.props

    return (
      <li {...props}>{props.children}</li>
    )
  }
}

/components/App.jsx

class App extends React.Component {
  handleClick = () => {
    this.props.dispatch(createList({title: "New Shopping List"}))
  }

  render() {
    const lists = this.props.lists

    return (
      <div>
        <button 
          className="add-list"
          onClick={this.handleClick}>Add Shopping List</button>
        <Lists lists={lists}/>
      </div>
    )
  } 
}

export default connect(state => ({ lists: state.lists }))(App)
Diego
  • 594
  • 2
  • 10
  • 29

2 Answers2

1

Assuming this is all meant to operate on a single array at a time, this part looks pretty suspicious:

case types.CREATE_LIST:
  return [
    ...state,
    {
      id: action.id,
      title: action.title,
      items: action.items || []
    }
  ]

That ...state is expanding whatever existing array there is into the new array that you're returning, and it doesn't sound like that's the behavior you actually want. My first guess is that when you create a new list, you probably want to just return the one new item inside, not the entire old list contents plus that new item.

Some of your other immutable-style update code also looks sort of complex. I know that "update a thing in the middle of an array" isn't always easy to deal with. You might want to take a look at this SO post on updating immutable data, which lists several ways to approach it. I also have a links repo that catalogs Redux-related libraries, and it has a list of immutable data management libraries that might make things easier for you.

Community
  • 1
  • 1
markerikson
  • 63,178
  • 10
  • 141
  • 157
0

Though @markerikson 's tip was part of the issue i was having, it didn't solve it completely. I had to fix my mapStateToProps to this

function mapStateToProps(state, props) {
  return {
    lists: state.lists,
    listItems: props.list.items.map(id => state.items[
        state.items.findIndex(item => item.id === id)
      ]).filter(item => item)
  }
}

and remove the connect from previous implementation for the items alone in my Items.jsx

Diego
  • 594
  • 2
  • 10
  • 29