2

I have a React design problem that I am trying to solve, and I am running into an issue counter-intuitive to the idea of encapsulation as I try to breakdown an existing component into parent-child to support an additional use case. I am using React 15.3.2 with Redux, ES6, and ImmutableJS. First, I will illustrate the design problem, then I will provide snippets to illustrate why I feel that I have the need to get data back from children and how that is good for encapsulation, and the roadblock I am hitting.

I have read this stackoverflow which has detailed explanation on why passing data from children to parent component is not a good idea, Pass props to parent component in React.js But I have some concerns, which I will discuss at the end.

Design:

I am starting with a CheckboxSelect component. The Title bar's text depends on the checked items.

Closed:

CheckboxSelect Closed

Open with selections (current implementation):

Checkbox Select Opened

To support additional use-case, the dropdown will now open up with more stuff.

Open with selections (new update):

Checkbox Select Opened New

Initial Code:

I am starting with a CheckboxSelect controlled component with the following props interface:

CheckboxSelect.jsx:

CheckboxSelect.propTypes = {
// what title to display by default with no selection
  defaultTitle: PropTypes.string.isRequired, ie. "Selected Scopes"

// array of selected options, ie. [{key: "comedy", name: "comedy", checked: false }, ...]
  options: PropTypes.array.isRequired,           

// handler for when the user checks a selection, this will update
// the reducer state, which causes the options prop to be refreshed and
// passed in from the outer container
  onCheck: PropTypes.func.isRequired,

  onUncheck: PropTypes.func.isRequired,
  onCheckAll: PropTypes.func,
  onUncheckAll: PropTypes.func,
  className: PropTypes.string,

// controls the open state of the dropdown
  open: PropTypes.bool,

// handler for when the user clicks on the dropdown button, this will update the reducer state,

// which causes the open prop to be refreshed and passed in from the outer container
  onClick: PropTypes.func,

  onCancel: PropTypes.func,
};

// there is currently some logic inside the component to generate the title to display
// produces "comedy, action"
getTitle() {
    const { defaultTitle  } = this.props;
    const checked = this.getChecked();

    let fullTitle;

    if (this.allChecked() || this.allUnchecked()) {
      fullTitle = `${defaultTitle } (${checked.length})`;
    } else {
      fullTitle = checked.map((option) => option.name).join(', ');
    }

    return fullTitle;
  }

  getChecked() {
    const { options } = this.props;
    return options.filter(option => option.checked);
  }

  allChecked() {
    const { options } = this.props;
    return this.getChecked().length === options.length;
  }

  allUnchecked() {
    return this.getChecked().length === 0;
  }

ApplicationContainer.jsx (where the component is being used):

scopeDropDownOptions = (currentMovie) => {
    // returns [{key: "comedy", name: "comedy", checked: false }]
    const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
        return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
          result.push({
            key: scope,
            name: scope,
            checked: (applicableScopes.includes(scope)),
          });
          return result;
        }, []);
} 

onSelectScope = (scope) => {
    const movieScopes = this.applicableScopes.push(scope.key);
    this.onUpdateField('applicable_scopes', movieScopes);
  }

render() {
    ...
    <CheckboxSelect
       defaultTitle="Selected Scopes"
              options={this.scopeDropdownOptions(currentMovie)}
              onCheck={this.onSelectScope}
              onUncheck={this.onDeselectScope}
              onCheckAll={this.onSelectAllScopes}
              onUncheckAll={this.onDeselectAllScopes}
      open={store.get('scopeDropdownOpen')}
    </CheckboxSelect>
}

New Code:

To support the new layout, I would like to break the existing component into two: a DynamicDropdown that contains CheckboxSelect2 as one of the children, along with any other elements that may be dropped down. Here is how the new code will look inside the ApplicationContainer.

ApplicationContainer.jsx

scopeDropDownOptions = (currentMovie) => {
        // returns [{key: "comedy", name: "comedy", checked: false }]
        const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
            return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
              result.push({
                key: scope,
                name: scope,
                checked: (applicableScopes.includes(scope)),
              });
              return result;
            }, []);
    } 

    onSelectScope = (scope) => {
        const {store } = this.props;
        const cachedApplicableScopes =  store.get('cachedApplicableScopes').push(scope.key);
        store.get('cachedApplicableScopes').push(scope.key);
        this.actions.setCachedApplicableScopes(cachedApplicableScopes);
        // wait until apply is clicked before update
     }

    render() {
        ...
        <DynamicDropdown
            className="scope-select"
            title={this.scopeDropdownTitle()}
            open={store.get('scopeDropdownOpen')}
            onClick={this.onScopeDropdownClick}
            onCancel={this.onScopeDropdownCancel}
          >
                <CheckboxSelect2
                  options={this.scopeDropdownOptions(currentMovie)}
                  onCheck={this.onSelectScope}
                  onUncheck={this.onDeselectScope}
                  onCheckAll={this.onSelectAllScopes}
                  onUncheckAll={this.onDeselectAllScopes}
                  visble={store.get('scopeDropdownOpen')}
                />
        // ... other children like confirmation message and buttons
          </DynamicDropdown>
    }

    // logic related to CheckboxSelect2 title generation moved to the ApplicationContainer.  Not ideal in my opinion as it breaks encapsulation.   Further discussions in the Choices section

    getScopesChecked() {
        const options = this.scopeDropdownOptions(this.currentMovie);
        return options.filter(option => option.checked);
      }

      scopesAllChecked() {
        const options = this.scopeDropdownOptions(this.currentMovie);
        return this.getScopesChecked().length === options.length;
      }

      scopesAllUnchecked() {
        return this.getScopesChecked().length === 0;
      }

      scopeDropdownTitle() {
        const defaultTitle = "Selected Scopes";
        const checked = this.getScopesChecked();

        let fullTitle;

        if (this.scopesAllChecked() || this.scopesAllUnchecked()) {
          fullTitle = `${defaultTitle} (${checked.length})`;
        } else {
          fullTitle = checked.map((option) => option.name).join(', ');
        }

        return fullTitle;
      }

Problem:

The problem I have is with populating the title props of the DynamicDropdown element with the New Code, since it depends on the result of the CheckboxSelect2 selection.

Keep in mind CheckboxSelect2 is a dumb controlled component, we pass an onCheck handler to it. The this.onSelectScope inside the ApplicationContainer, is responsible for updating the reducer state of what has been checked, which in turn refresh the props and causes the DynamicDropdown and CheckboxSelect2 to be re-rendered.

Choices:

In the old code, there is a group of logic used to figure out the title to display for the dropdown. Here are the choices I am presented with:

  1. To keep encapsulation of letting the CheckboxSelect2 summarize the title, I tried initially keeping the same title logic inside CheckboxSelect2, and accessing it via a ref.

ApplicationContainer.jsx

<DynamicDropdown
    title={this.childCheckboxSelect.getTitle()}
> 
    <CheckboxSelect2
        ref={(childCheckboxSelect) => this.childCheckboxSelect =     childCheckboxSelect}
    >
</DynamicDropdown>

At the time that DynamicDropdown is re-rendered, CheckboxSelect2 hasn't received the new props yet from the parent via the one-way data-flow, so as a child, it actually cannot generate the most up-to-date title for the DynamicDropdown based on what has been checked. With this implementation, my title is one-step behind what was actually checked.

  1. As shown in the ApplicationContainer.jsx for the New Code section above, the logic for the scopeDropdownTitle could be moved out from CheckboxSelect2 to ApplicationContainer.jsx, so it sits a level above DynamicDropdown, enabling the DynamicDropdown to get the updated title from the reducer state as it renders. While this solution works, it totally breaks my view on encapsulation, where the responsibility for determining what title to be displayed, is something that the CheckboxSelect2 should know about. Basically the title logic in ApplicationContainer.jsx, now also pre-generates the options props meant to passed to CheckboxSelect2 to render that component, the CheckboxSelect2 logic is bleeding into the outer container.

Let's look at the argument in the stackoverflow post Pass props to parent component in React.js and how it relates to this design problem and an analogy:

  1. "The parent already has that child prop. ...  if the child has a prop, then it is because its parent provided that prop to the child!"  Sure, the ApplicationContainer has all the knowledge it needs to generate the title for the parent DynamicDropdown based on the checked states of the child CheckboxSelect2, but then is it really the responsibility of the ApplicationContainer?

    Let me give an analogy of a Manager asking an Employee to produce a Report. You can say, the Manager already has all the info, he can surely produce the Report himself! Having a controlled component where the container manages update to the props for the child via callbacks, seems to me like a Manager passing a bucket to the Employee, the Employee passes back bits and pieces of information in the bucket, and tell the Manager to do the work to summarize things instead of the Employee producing a good summary (which is good encapsulation).

  2. "Additionally, you could have used a ref directly on the child" I think in the Choices 1) I stated above, using ref does not seem to work when you want up-to-date information from the child as there is a circular dependency issue with one-way dataflow (ie. parent needs to get up-to-date summary information from the child, but the child first depends on the up-to-date information from the parent).

If you have read this end-to-end and understood the problem, I appreciate your effort! Let me know what you think.

Community
  • 1
  • 1
frank
  • 1,283
  • 1
  • 19
  • 39
  • @SebastianLorber since you provide a detailed answer to http://stackoverflow.com/questions/22639534/pass-props-to-parent-component-in-react-js that I am referencing. I would appreciate hearing your thoughts on this question I have as well. – frank Apr 02 '17 at 19:27
  • I'm sorry, but this is the only thing I could think about when reading your question: https://www.youtube.com/watch?v=QM1iUe6IofM – Fluidity Apr 02 '17 at 19:29

0 Answers0