Oleksandr Fomin
Oleksandr Fomin

Reputation: 2376

What is a correct usage of smart/dumb component pattern?

I have a DropdownFilter presentational component that I want to reuse for multiple different filters.

const DropdownFilter: React.FC<DropdownFilterProps> = ({
  filterValue,
  filterOptions,
  onClickHandler,
}) => {
  return (
    <Dropdown>
      <Dropdown.Toggle id="dropdown">{filterValue}</Dropdown.Toggle>
      <Dropdown.Menu>
        {filterOptions.map((option, i) => (
          <Dropdown.Item onClick={() => onClickHandler(option)} key={i}>
            {option}
          </Dropdown.Item>
        ))}
      </Dropdown.Menu>
    </Dropdown>
  );
};

export default DropDownFilter;

The state of each filter is controlled by a filterReducer in Redux. Does that mean that I have to create several container components to get the data for each separate filter?

const StatusFilterContainer: React.FC = () => {
  const statusFilterValue = useSelector(getStatusFilterValue);
  const statusFilterOptions = ["Show All", "Pending", "Completed", "Cancelled"];

  const onStatusFilterChange = (filterValue: string): void => {
    dispatch(setStatusFilterValue(filterValue));
  };

  return (
    <>
      <DropDownFilter
        onClickHandler={onStatusFilterChange}
        filterOptions={statusFilterOptions}
        filterValue={statusFilterValue}
      />
    </>
  );
};

export default StatusFilterContainer;
const TypeFilterContainer: React.FC = () => {
  const typeFilterValue = useSelector(getTypeFilterValue);
  const typeFilterOptions = ["Show All", "Refill", "Withdrawal"];

  const onStatusFilterChange = (filterValue: string): void => {
    dispatch(setTypeFilterValue(filterValue));
  };

  return (
    <>
      <DropDownFilter
        onClickHandler={onStatusFilterChange}
        filterOptions={typeFilterOptions}
        filterValue={typeFilterValue}
      />
    </>
  );
};

export default TypeFilterContainer;

Upvotes: 2

Views: 4180

Answers (1)

Drew Reese
Drew Reese

Reputation: 203418

What is a correct usage of smart/dumb component pattern?

IMO the correct usage is generally to limit the scope of state and logic as much as possible, only exposing out state and mutators on the API when necessary (i.e. lifting state up). There is also the consideration of separations of concerns. For example, the value of some input field of a form submitted to a backend isn't really the concern of the entire app, so keeping it in component state is fine, but something like an authenticated user could be a concern for any component in the app, so lift it up.

Repetitive WET (Write Everything Twice) code:

Creating a more DRY solution by converting the duplicated logic is rather straight forward. Basically collect what is abstractly identical/common (i.e. using a selector, dispatch action function) and factor out what is different (i.e. the options, the actual selector and action), these can be passed as props.

Abstractly this is a component that is provided options to render, selects some state, and dispatches some action on option change.

const FilterContainer: React.FC = ({
  options,
  selectorFn,
  setFilterValueAction,
}) => {
  const filterValue = useSelector(selectorFn);

  const onStatusFilterChange = (filterValue: string): void => {
    dispatch(setFilterValueAction(filterValue));
  };

  return (
    <>
      <DropDownFilter
        onClickHandler={onStatusFilterChange}
        filterOptions={options}
        filterValue={filterValue}
      />
    </>
  );
};

Notice now this is very similar to the original DropDownFilter component (it's basically just a proxy at this point), we could just move the little bit of logic into DropDownFilter directly (or the JSX from, depending on your philosophical bend).

const DropdownFilter: React.FC<DropdownFilterProps> = ({
  filterOptions,
  stateSelector,
  onChangeAction,
}) => {
  const filterValue = useSelector(stateSelector);

  const onStatusFilterChange = (filterValue: string): void => {
    dispatch(onChangeAction(filterValue));
  };

  return (
    <Dropdown>
      <Dropdown.Toggle id="dropdown">{filterValue}</Dropdown.Toggle>
      <Dropdown.Menu>
        {filterOptions.map((option, i) => (
          <Dropdown.Item onClick={() => onStatusFilterChange(option)} key={i}>
            {option}
          </Dropdown.Item>
        ))}
      </Dropdown.Menu>
    </Dropdown>
  );
};

We've now internalized the selector, dispatch, and rendering logic, just need the options, selector to get state, and the action to take when the option changes.

Usage:

const StatusFilterContainer: React.FC = () => (
  <DropdownFilter
    filterOptions={["Show All", "Pending", "Completed", "Cancelled"]}
    onChangeAction={setStatusFilterValue}
    stateSelector={getStatusFilterValue}
  />
);

const TypeFilterContainer: React.FC = () => (
  <DropdownFilter
    filterOptions={["Show All", "Refill", "Withdrawal"]}
    onChangeAction={setTypeFilterValue}
    stateSelector={getTypeFilterValue}
  />
);

Upvotes: 6

Related Questions