Mithril
Mithril

Reputation: 13748

How to implement a self contained component in react redux?

I am building a file manager webui base on react redux(My purpose is to master react and redux through this project)

As you know, a file manager need a tree explorer.I want to build a component which can contain it self and each has self state. like below:

TreeNode can contain children which are TreeNode too.Each TreeNode hold its state {path, children_nodes, right .....}, children_nodes is get from server, path is passed by parent. That's what I imagine. Struct like:

App:
TreeNode
--TreeNode
----TreeNode
----TreeNode
TreeNode
TreeNode
--TreeNode
TreeNode
--TreeNode
----TreeNode
----TreeNode

But trouble come here, because redux connect store to the tree root, all node under the root receive same state...

For example, I have a OPEN_NODE action, which is design to trigger getFileList fucntion base this node's path and set this node's state.open to true.(note: getFileList fucntion not implement yet, just give fake data for now) The screen shot: enter image description here

Click each element , but states are equal.

My code:

containers/App.js

import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Footer from '../components/Footer';
import TreeNode from '../containers/TreeNode';
import Home from '../containers/Home';
import * as NodeActions from '../actions/NodeActions'

export default class App extends Component {

  componentWillMount() {
    // this will update the nodes on state
    this.props.actions.getNodes();
  }

  render() {
    const { nodes } = this.props
    console.log(nodes)
    return (
      <div className="main-app-container">
        <Home />
        <div className="main-app-nav">Simple Redux Boilerplate</div>
        <div>
          {nodes.map(node =>
            <TreeNode key={node.name} info={node} actions={this.props.actions}/>
          )}
        </div>

        {/*<Footer />*/}
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    nodes: state.opener.nodes,
    open: state.opener.open
  };
}


function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(NodeActions, dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

containers/TreeNode.js

import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import classNames from 'classnames/bind'
import * as NodeActions from '../actions/NodeActions'

export default class TreeNode extends Component {

  constructor(props, context) {
    super(props, context)
    this.props = {
      open: false,
      nodes: [],
      info:{}
    }
  }

  handleClick() {
    let {open} = this.props
    if (open) {
      this.props.actions.closeNode()
    } else {
      this.props.actions.openNode()
    }
  }

  render() {
    const { actions, nodes, info } = this.props
    return (
      <div className={classNames('tree-node', { 'open':this.props.open})} onClick={ () => {this.handleClick()} }>
        <a>{info.name}</a>
        {nodes &&
          <div>{nodes.map(node => <TreeNode info={node} />)}</div>
        }
        {!nodes &&
          <div>no children</div>
        }
      </div>
    );
  }
}

TreeNode.propTypes = {
  open:PropTypes.bool,
  info:PropTypes.object.isRequired,
  nodes:PropTypes.array,
  actions: PropTypes.object.isRequired
}

actions/NodeActions.js

import { OPEN_NODE, CLOSE_NODE, GET_NODES } from '../constants/NodeActionTypes';

export function openNode() {
  return {
    type: OPEN_NODE
  };
}

export function closeNode() {
  return {
    type: CLOSE_NODE
  };
}


export function getNodes() {
  return {
    type: GET_NODES
  };
}

reducers/TreeNodeReducer.js

import { OPEN_NODE, CLOSE_NODE, GET_NODES } from '../constants/NodeActionTypes';

const initialState = {
  open: false,
  nodes: [],
  info: {}
}

const testNodes = [
  {name:'t1',type:'t1'},
  {name:'t2',type:'t2'},
  {name:'t3',type:'t3'},
]


function getFileList() {
  return {
    nodes: testNodes
  }
}


export default function opener(state = initialState, action) {
  switch (action.type) {
  case OPEN_NODE:
    var {nodes} = getFileList()
    return {
      ...state,
      open:true,
      nodes:nodes
    };
  case CLOSE_NODE:
    return {
      ...state,
      open:false
    };
  case GET_NODES:
    var {nodes} = getFileList()
    return {
      ...state,
      nodes:nodes
    };
  default:
    return state;
  }
}

For complete code, see my github https://github.com/eromoe/simple-redux-boilerplate

I don't see an example cover such component, and google result nothing helpful. Any idea to overcome this?

update: I see this How to manage state in a tree component in reactjs

But the solution is pass the whole tree to state, can not use in file manager.

Upvotes: 3

Views: 2034

Answers (2)

xiaofan2406
xiaofan2406

Reputation: 3310

All of your TreeNode has the same state from redux because your mapStateToProps for all of them are same.

mapStateToProps can take ownProps (the props of the wrapped component) as second parameter. You can use it to distinguish your TreeNodes. In your case, path is a good option.

considering writing a state selector like this getChildrenNodes(state, path) and return the nodes accordingly.

You may want to consider reading through redux docs first, especially this seciont: http://redux.js.org/docs/recipes/ComputingDerivedData.html

Upvotes: 0

Glund
Glund

Reputation: 547

I'm implementing a Github like app using React and Redux.

For now, I only list repositories and show its files as well as navigate through them.

I don't know if this is considered a good or bad practice, but this is how I implemented my Tree component.

Inside each Tree Component, I have a Link to itself. And I pass some data on the route, so I'm able to get the next tree when I render it.

App

Component

class Tree extends Component {
  constructor(props) {
    super(props);

    this.renderList = this.renderList.bind(this);
  }

  componentWillMount() {
    this.props.getTree(this.props.params.sha);
  }

  componentWillReceiveProps(nextProps) {
    if(nextProps.params.sha !== this.props.params.sha) {
      this.props.getTree(nextProps.params.sha);
    }
  }

  renderList(file) {
    return (
      <tr key={ file.sha }>
        { file.type == 'tree'
       ? <td><Link to={`/repository/${this.props.params.repoName}/tree/${file.path}/${file.sha}`}>{ file.path }</Link></td>
       : <td><Link to={`/repository/${this.props.params.repoName}/blob/${file.sha}/${file.path}`}>{ file.path }</Link></td>}
      </tr>
    )
  }

  render() {
    const treeFile = this.props.tree;
    const fileName = this.props.params.path;

    return (
      <div className="row">
        <h3>{ fileName }</h3>
        <div className="col-md-12">
          <table className="table table-hover table-bordered">
            <tbody>
              { isEmpty(treeFile.tree) ? <tr>Loading</tr> : treeFile.tree.map(this.renderList) }
            </tbody>
          </table>
        </div>
      </div>
    )
  }
}
export default Tree;

Action

const setTree = (tree) => {
  return {
    type: actionTypes.GET_TREE,
    tree
  };
};

export const getTree = (sha) => {

  return (dispatch, getState) => {
    const { repository, profile } = getState();
    const repo = GitHubApi.getRepo(profile.login, repository.name);

    repo.getTree(sha, function(err, data) {
      dispatch(setTree(data));
    });
  }
}

Reducer

const initialState = "";

export const tree = (state = initialState, action) => {
  switch (action.type) {
    case actionTypes.GET_TREE:
      return getTree(state, action);
  }
  return state;
}

const getTree = (state, action) => {
  const { tree } = action;
  return tree;
}

For the complete code, you can check my repository on github

https://github.com/glundgren93/Github-redux

Upvotes: 1

Related Questions