BeeNag
BeeNag

Reputation: 1844

React - Accessing updated values from nested components

I have a table with rows and each cell within a row can be edited. There is a button associated with each row that will submit all the cells in that row to a database. What I would like to know is how I can access the new values, if the data in the cell has been changed, from the component that the submit event is actually occurring on?

Here is how I am building the table at the moment:

Main Table:

import React from 'react';
import TableWithDataHeader from './TableWithDataHeader.jsx';
import Row from './Row.jsx';
import AppStore from '../../stores/AppStore';

export default class Table extends React.Component {

    handleSubmitEvent = (e) => {
        e.preventDefault();
        /////What do I do here to get the updated values???////
    };

    render() {
        return (
            <div>
                <div className="row">
                    <table className="table table-striped table-bordered">
                        <thead>
                            <TableWithDataHeader />
                        </thead>
                        <Row data={AppStore.getCells().historycells} handleSubmitEvent={this.handleSubmitEvent} />
                    </table>
                </div>
            </div>
        );
    }
}

Row Component:

import React from 'react';
import TableBody from './TableBody.jsx';
import AppStore from '../../stores/AppStore';

export default class Row extends React.Component {
    state = {data: this.props.data};

    handleSubmitEvent = (e) => {
        this.props.handleSubmitEvent(e);
    };

    render() {

        let {data} = this.state;

        return (
            <tbody>
                <tr id={AppStore.getRowId()}>
                    {this.state.data.map(cell => {
                        return (
                            <TableBody key={cell.id} cell={cell.contents} />
                        );
                    })}
                    <td className="table-button">
                        <button type="submit" className="btn btn-primary" onClick={this.handleSubmitEvent.bind(this)}>Save Row</button>
                    </td>
                </tr>
            </tbody>
        );
    }
}

Cell Component:

import React from 'react';
import EditableCells from './EditableCells.jsx';

export default class TableBody extends React.Component {

    render() {
        return (
            <EditableCells data={this.props.cell} />
        );
    }
}

Edit Cell Component:

import React from 'react';

export default class EditableCells extends React.Component {
    state = {isEditMode: false, data: ""};

    componentWillMount() {
        this.setState({
            isEditMode: this.props.isEditMode,
            data: this.props.data
        });
    }

    handleEditCell = (e) => {
        this.setState({isEditMode: true});
    };

    handleKeyDown = (e) => {
        switch (e.keyCode) {
            case 13: //Enter
            this.setState({isEditMode: false});
            break;
        }
    };

    handleChange = (e) => {
        this.setState({data: e.target.value});
    };

    render() {

        let {isEditMode, data} = this.state;

        let cellHtml;

        if (this.state.isEditMode) {
            cellHtml = <input type="text" className="form-control" value={this.state.data} onKeyDown={this.handleKeyDown} onChange={this.handleChange} />
        } else {
            cellHtml = <div onClick={this.handleEditCell}>{this.state.data}</div>
        }
        return (
            <td className="text-center">{cellHtml}</td>
        );
    }
}

Any help on how I would be able to do this would be very helpful, and, of course, much appreciated!

Thanks for your time

Upvotes: 1

Views: 3243

Answers (2)

rishat
rishat

Reputation: 8376

React only pours data changes and, if necessary, re-renders the changed part of DOM from top to bottom. What you can do is give a child component access to parent component's state through props:

class ParentComponent extends Component {
  handleWhatever(updated) {
    this.setState({
      whatever: updated
    });
  }

  render() {
    return (
      <ChildComponent onEdit={::this.handleWhatever} />
    );
  }
}

and

class ChildComponent extends Component {
  render() {
    return (
      <SomethingEditable onChange={this.props.onEdit} />
    );
  }
}

So, the idea is:

  1. In ParentComponent, you define a method that, when called, changes this component's state, i.e. ParentComponent's.
  2. In ChildComponent, you're having something that is capable of calling a function every time it is changed (the most common example: input element and its onBlur event).
  3. Every time you change something inside ChildComponent, it calls the function that has been passed from ParentComponent, which, in turn, provides data in bottom-to-top direction and delivers it to ParentComponent.
  4. The function is called, bound to ParentComponent, so setState gets called then, which changes the state of ParentComponent, and it reflects (or not) in re-render of the whole subtree (React takes care of picking only those components that really have to be re-rendered).

This is a classic way to solve the problem of moving data in direction that is opposite to how React expects it to flow.

You can have as many nesting layers as you need, just don't forget to pass that function through all the layers.

Upvotes: 1

dannyjolie
dannyjolie

Reputation: 11339

Try to keep you application state at the highest most level in the component tree. The easy way out in this case is to let your Table component handle the data state, and also supply event handlers. Pass these down as props to the Row, and further down as needed. Proof of concept: https://jsfiddle.net/dannyjolie/4jfxeag8/1/

The general rule of thumb is to never use component state at all, and leverage all change in application change to the very top most component, but input fields is sometimes an exception from the rule as you may not want to modify the application state while typing.

The basics of it:

export default class Table extends React.Component {
  constructor(props) {
    super(props);
    this.state.cells = ['Foo', 'Bar']; // Fetch something from your store, or better, pass the cells as props from parent component
    this.handleSubmitEvent = this.handleSubmitEvent.bind(this);
    this.handleChangeEvent = this.handleChangeEvent.bind(this);
  }
  handleSubmitEvent () {
    // Up to date cells are stored in this.state
  }
  handleChangeEvent (value, cell) {
    // All values are available in this.state, do with that as you wish :)
  }
  render () {
    ....
    <Row handleSubmitEvent={this.handleSubmitEvent} 
         handleChangeEvent={this.handleChangeEvent}
         data={this.state.cells} />
    ....
  }
}

Upvotes: 4

Related Questions