2
votes

I am having a dilemma as to where Where My Form State Should Live.

React Docs mention:

Identify every component that renders something based on that state. Find a common owner component (a single component above all the components that need the state in the hierarchy). Either the common owner or another component higher up in the hierarchy should own the state. If you can’t find a component where it makes sense to own the state, create a new component simply for holding the state and add it somewhere in the hierarchy above the common owner component.

I have a simple comment UI.

  1. The user enters a comment in the text area.
  2. The user enters name in input field.
  3. The user clicks post and the comment is displayed below.

Components Highrarchy is as follows:

 - CommentSection.jsx ---> main (should "Post" state (?))
   -comment-post.jsx  ---> User Comment Form
   - comment-table.jsx
     - comment.jsx 
       - comment-avatar.jsx 
       - comment-body.jsx 

Issue: In comment-post.jsx the input and textarea fields have onChange() event handler, and there is also a click event handle on submit post button. I can choose to do one of two:

  1. In comment-post.jsx when onChange is trigger send the commentBody to CommentSection.jsx The issue here would be I will be sending the commentBody as soon as user types, then sending name later when it triggers and so on
  2. In comment-post.jsx when onChange is trigger SAVE the value in state, then name the name field in the state, when user clicks submit button send to CommentSection.jsx Benefit is Fields are set first in state, and when the user clicks post they are sent to the parent (As it should be right?)

Should the input fields of the comment i.e. commentBody and Name be saved in the comment-post.jsx (form component) or the parent?

Right now the state I am doing the 2. Saving form fields in state and then sending the state values on submit.

I think the issue is onChange and onClick are two different handlers, the question is which one should pass the values to parent component ideally?

    class CommentSection extends Component {
      state = {
        posts: []
      };

      handleCommentPost = post => {
        const posts = this.state.posts;

        posts.push(post);
        this.setState({ posts });
      };

      render() {
        console.log("comment:", this.state.posts);

        return (
          <React.Fragment>
            <h1>comments</h1>
            <div className="row bootstrap snippets">
              <div className="col-md-6 col-md-offset-2 col-sm-12">
                <div className="comment-wrapper">
                  <Comment_Post onClick={this.handleCommentPost} />
                  <Comment_Table comments={this.state.posts} />
                </div>
              </div>
            </div>
          </React.Fragment>
        );
      }
    }

    class Comment_Table extends Component {
       render() {
        const posts = this.props.comments;
        let count = 0;

        return (
          <div className="panel panel-info">
            <hr />
            <ul className="media-list">
              {posts.map(post => (
                <Comment key={count++} comment={post.commentBody} />
              ))}
            </ul>
          </div>
        );
      }
    }



class Comment extends Component {
  render() {
    return (
      <li className="media">
        <Comment_Avatar userAvatar={this.props.commentAvatar} />
        <Comment_Body userComment={this.props.comment} />
      </li>
    );
  }
}

class Comment_Body extends Component {
  render() {
    const { userComment } = this.props;
    return (
      <div className="media-body">
        <span className="text-muted pull-right">
          <small className="text-muted">30 min ago</small>
        </span>
        <strong className="text-success">@MartinoMont</strong>
        <p>
          {userComment}
          {}
        </p>
      </div>
    );
  }
}
class Comment_Post extends Component {
  state = {
    commentBody: null
  };
  onCommentChange = e => {
    this.setState({ commentBody: e.target.value });
  };

  onCommentPost = e => {
    const commentBody = this.state.commentBody;
    if (commentBody !== null) {
      this.props.onClick({ commentBody });
    }
  };
  onNameInput = e => {};

  onCommentPostError() {}

  render() {
    return (
      <React.Fragment>
        <div className="panel-heading p-heading">Comment panel</div>
        <div className="panel-body">
          <textarea
            onChange={this.onCommentChange}
            className="form-control"
            placeholder="write a comment..."
            rows="3"
          />
          <label htmlFor="fname" onChange={this.onNameInput}>
            Name:{" "}
          </label>
          <input id="fname" placeholder="John" />
          <br />
          <button
            type="button"
            className="btn btn-info pull-right"
            onClick={this.onCommentPost}
          >
            Post
          </button>
          <div className="clearfix" />
        </div>
      </React.Fragment>
    );
  }
}

class Comment_Avatar extends Component {
  render() {
    return (
      <a href="#" className="pull-left">
        <img
          src="https://bootdey.com/img/Content/user_1.jpg"
          alt=""
          className="img-circle"
        />
      </a>
    );
  }
}
1

1 Answers

0
votes

I think the issue is onChange and onClick are two different handlers, the question is which one should pass the values to parent component ideally?

There are two types of Forms that we use in standard User interface design. First is when any change in the Input saves the change. Second, when after making changes to the Form Elements, you press a submit button and then the changes are saved.

Since you have implemented the second type, your onChange should deal with controlling your TextArea state and your onClick should deal with the submit. So your code is just fine.

I am having a dilemma as to where Where My Form State Should Live.

That depends...In your case you only have one Form, and two Form Elements none of which are re-usable. So these uncontrolled Forms work well for you. However, if you wanted to have a Form Component that is re-usable, or if you wanted to have a Form with 15 Fields in it, you would not want to write a separate onChange handler for each one of them. For this, you would want to make a controlled Form Component that can handle all these things for you. Here's an example of what that would look like.

export default class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      values: {}
    };
  }

  @boundMethod
  handleSubmit(event) {
    event.preventDefault();

    this.props.submit(this.state.values);
  }

  @boundMethod
  handleChange(event) {
    const { name, value } = event.target;
    const newValues = Object.assign(
      { ...this.state.values },
      { [name]: value }
    );
    this.setState({
      values: newValues
    });
  }

  public render() {
    const { values } = this.state;
    return (
      <form onSubmit={this.handleSubmit} noValidate={true}>
        <div>
          {React.Children.map(
            this.props.children,
            child => (
                {React.cloneElement(child, {
                  value: values[child.props.name],
                  onChange: this.handleChange
                })}
            )
          )}
          <div>
            <button type="submit">
              Submit
            </button>
          </div>
        </div>
      </form>
    );
  }
}

Then you will be able to use this Form class like so:

<Form
  submit={values => {
    /* work with values */
  }}
>
  <input type="hidden" name="name" />
  <input type="hidden" name="rating" />
</Form>;