24

I'm a bit stuck thinking on how to implement a reducer where its entities can have children of the same type.

Let's take reddit comments as an example: each comment can have child comments that can have comments themselves etc. For simplification reason, a comment is a record of type {id, pageId, value, children}, with pageId being the reddit page.

How would one model the reducer around that? I was thinking of having the reducer be a map -> id of the comments where you can filter by page using the pageId.

The issue is that for example when we want to add a comment to a nested one: we need to create the record on the root of the map and then add its id in the parent children property. To display all the comments we'd need to get all of them, filter those that we have at the top (that would be kept in the page reducers as an orderedList for example) and then iterate on them, fetching from the comments objects when we encounter children using recursion.

Is there a better approach than that or is it flawed?

dhilt
  • 18,707
  • 8
  • 70
  • 85
Vincent P
  • 699
  • 1
  • 7
  • 18
  • I think you could try normalizr: https://github.com/gaearon/normalizr Haven't used it myself, so I'm not sure if it will help you in your case. – Simon Sep 27 '15 at 11:21
  • I know about normalizr, I'm more wondering if there is an "accepted" solution on how to deal with it in the components. Unless you connect() every comment you will need to do the opposite of normalizr on each change and even if you do connect that looks a bit like a mess – Vincent P Sep 27 '15 at 18:18

2 Answers2

40

The official solution to this is to use normalizr to keep your state like this:

{
  comments: {
    1: {
      id: 1,
      children: [2, 3]
    },
    2: {
      id: 2,
      children: []
    },
    3: {
      id: 3,
      children: [42]
    },
    ...
  }
}

You're right that you'd need to connect() the Comment component so each can recursively query the children it's interested in from the Redux store:

class Comment extends Component {
  static propTypes = {
    comment: PropTypes.object.isRequired,
    childComments: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired
  },

  render() {
    return (
      <div>
        {this.props.comment.text}
        {this.props.childComments.map(child => <Comment key={child.id} comment={child} />)}
      </div> 
    );
  }
}

function mapStateToProps(state, ownProps) {
  return {
    childComments: ownProps.comment.children.map(id => state.comments[id])
  };
}

Comment = connect(mapStateToProps)(Comment);
export default Comment;

We think this is a good compromise. You pass comment as a prop, but component retrieves childrenComments from the store.

Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
  • How do you actually do that with Schemas though? Please could you give an example – alexrogers Apr 07 '16 at 10:49
  • This example uses normalizr: https://github.com/reactjs/redux/tree/master/examples/real-world. Also normalizr has tests which hopefully should show how to use it. – Dan Abramov Apr 09 '16 at 21:06
  • What if your data doesn't come from an API though? Should I just model my data like normalizr does? – Dave May 02 '16 at 19:45
  • In general I would recommend it, yes. See also http://redux.js.org/docs/FAQ.html#organizing-state-nested-data – Dan Abramov May 02 '16 at 20:11
1

Your store (reducer) structure could differ from your desired view model (one you pass as props to components). You could just keep all comments in array and map them to a tree by links in mapStateToProps on high-level 'smart' component. You'll get simple state management in reducer and a handy view model for components to work with.

Victor Suzdalev
  • 2,202
  • 19
  • 28