0

For the following component, how would I extract the unit logic to allow for composition? In my actual app I am trying to reduce the amount of logic encoded in the component and I have decided that the unit logic (prepend or append, the symbol, how to format the value) does not make sense to be embedded in the main component. In object oriented programming I would use something similar to the strategy pattern to accomplish this design but I was wondering if, in a more functional react world, there was a similar approach to be used.

type Props = {
  value: number;
  active: boolean;
  unit: "$" | "%";
  readonly: boolean;
  disabled: boolean;
  label: string;
  onValueChange: (value: number) => void;
  onActiveChange: (active: boolean) => void;
};

export const PromptSuggestion: React.FC<Props> = ({
  value,
  active,
  unit,
  readonly,
  disabled,
  label,
  onValueChange,
  onActiveChange
}) => {
  return (
    <fieldset>
      <legend>{label}</legend>
      <label htmlFor="Value">Value </label>
      {!readonly && (
        <>
          {unit === "$" && <span>$ </span>}
          <input
            id="Value"
            type="number"
            disabled={disabled}
            value={value}
            onChange={(e) => onValueChange(e.target.valueAsNumber)}
          />
          {unit === "%" && <span> %</span>}
        </>
      )}
      {readonly && (
        <span>
          {unit === "$" ? `$${value.toFixed(2)}` : `${value.toFixed(2)}%`}
        </span>
      )}
      <br />
      <label htmlFor="Active">Active </label>
      {!readonly && (
        <input
          id="Active"
          type="checkbox"
          disabled={disabled}
          checked={active}
          onChange={(e) => onActiveChange(e.target.checked)}
        />
      )}
      {readonly && <span>{active ? "True" : "False"}</span>}
    </fieldset>
  );
};

Edit ComponentStrategyDemonstration

1 Answer 1

0

Strategy vs composition

You are realy asking 2 questions here. How to use composition with React components and how to use the strategy pattern.

Let me first give you the traditional way to get rid of some logic only related to the unit by using composition.

Composition: Create an Amount component

It seems to me that the fact the value can (must) have a unit, is a property of the value itself. So I would probably model a special type in my domain model that represents values with units.

On the React side of things we are usually concerned with the UI and here too, we can see that if two screens need to render a value with a unit, it makes sense for those components to use a specialized component for the job.

Example

Based on your code, here is an example to get you started:

export const Amount: React.FC<Props> = ({
  id,
  readonly,
  disabled,
  value,
  unit,
  onValueChange,
}) => (
  <> 
    {readonly ? (
      {unit === "$" && <span>$ </span>}
      <input 
          id={id} 
          type="number"
          disabled={disabled}
          value={value}
          onChange={(e) => onValueChange(e.target.valueAsNumber)}
      />
      {unit === "%" && <span> %</span>}
    ) : (
      <span>
        {unit === "$" ? `$${value.toFixed(2)}` : `${value.toFixed(2)}%`}
      </span>
    )}
  </>
);

Then use that in your main component:

export const PromptSuggestion: React.FC<Props> = ({
  value,
  active,
  unit,
  readonly,
  disabled,
  label,
  onValueChange,
  onActiveChange
}) => {
  return (
    <fieldset>
      <legend>{label}</legend>
      <label htmlFor="Value">Value </label>
      <Amount 
        id="Value"
        readonly={readonly}
        disabled={disabled}
        value={value}
        unit={unit}
        onValueChange={onValueChange}
      />
      <br />
      <label htmlFor="Active">Active </label>
      {!readonly && (
        <input
          id="Active"
          type="checkbox"
          disabled={disabled}
          checked={active}
          onChange={(e) => onActiveChange(e.target.checked)}
        />
      )}
      {readonly && <span>{active ? "True" : "False"}</span>}
    </fieldset>
  );
};

We can do better here and even further reduce the unrelated factors in the amount component by giving it rest params and let it pass those props down to the input:

export const Amount: React.FC<Props> = ({
  value,
  unit,
  readonly,
  onValueChange,
  ...rest,  // <-- rest params
}) => (
  <> 
    {readonly ? (
      {unit === "$" && <span>$ </span>}
      <input 
          type="number"
          value={value}
          onChange={(e) => onValueChange(e.target.valueAsNumber)}
          { ...rest }  {/* <-- pass rest params down */}
      />
      {unit === "%" && <span> %</span>}
    ) : (
      <span>
        {unit === "$" ? `$${value.toFixed(2)}` : `${value.toFixed(2)}%`}
      </span>
    )}
  </>
);

Strategy: Injecting the format

A strategy pattern normally means injecting the decision of what / how to display into the component instead of coding it in the component. Our Amount component can play the role of pattern. It contains the ;pgic specific to values with units. At the moment, the PromptSuggestion component contains the decision to use the Amount component for values with a unit. But we could delegate that decision to it's parent, by passing the component to use as a prop:

export const PromptSuggestion: React.FC<Props> = ({
  value,
  active,
  unit,
  readonly,
  disabled,
  label,
  onValueChange,
  onActiveChange,
  ValueComponent = Amount,
}) => {
  return (
    <fieldset>
      <legend>{label}</legend>
      <label htmlFor="Value">Value </label>
      <ValueComponent 
        id="Value"
        readonly={readonly}
        disabled={disabled}
        value={value}
        unit={unit}
        onValueChange={onValueChange}
      />
      <br />
      <label htmlFor="Active">Active </label>
      {!readonly && (
        <input
          id="Active"
          type="checkbox"
          disabled={disabled}
          checked={active}
          onChange={(e) => onActiveChange(e.target.checked)}
        />
      )}
      {readonly && <span>{active ? "True" : "False"}</span>}
    </fieldset>
  );
};

Here, I added an extra prop ValueComponent, that defaults to Amount. But calling code can now choose to use a custom value component, as long as it takes the same props (has the same interface) as the Value component.

Likewise, we could give the Value component an extra prop format and use a strategy pattern to inject the decicion logic at that point.

However, I think suggesting Amount as the strategy here is interesting, because it touches upon another point: children. When you think about it, React has a built-in way of using the strategy pattern with React components and that is to use the special children props to get at the nested components:

export const Amount: React.FC<Props> = ({
  readonly
  value,
  unit,
  children,
}) => (
  <> 
    {readonly ? (
      {unit === "$" && <span>$ </span>}
      {children}
      {unit === "%" && <span> %</span>}
    ) : (
      <span>
        {unit === "$" ? `$${value.toFixed(2)}` : `${value.toFixed(2)}%`}
      </span>
    )}
  </>
);

Now, you would use it like this:

export const PromptSuggestion: React.FC<Props> = ({
  value,
  active,
  unit,
  readonly,
  disabled,
  label,
  onValueChange,
  onActiveChange
}) => {
  return (
    <fieldset>
      <legend>{label}</legend>
      <label htmlFor="Value">Value </label>
      <ValueComponent 
        readonly={readonly}
        value={value}
        unit={unit}
      >
        <input 
            id="Value" 
            type="number"
            disabled={disabled}
            value={value}
            onChange={(e) => onValueChange(e.target.valueAsNumber)}
        />
      </Value>
      <br />
      <label htmlFor="Active">Active </label>
      {!readonly && (
        <input
          id="Active"
          type="checkbox"
          disabled={disabled}
          checked={active}
          onChange={(e) => onActiveChange(e.target.checked)}
        />
      )}
      {readonly && <span>{active ? "True" : "False"}</span>}
    </fieldset>
  );
};

Notice how using children allows the Value component to postpone the decisions around what HTML element to use, what ID it should have, what events to listen for etc? It delegates these decisions to the parent component, which can inject any sub components it wants.

2
  • Thank you, this was the sort of insight I was looking for. As a follow up question, would using a strategy pattern (i.e. passing in a ValueStrategy which could be a DollarStrategy or PercentageStrategy) be considered an anti-pattern of sorts? Sep 14 at 16:29
  • No I don't think it would be an anti-pattern at all. In fact it seems completely reasonable to me to create a generic Amount component that can have different strategies for how to format the number and the unit. Sep 18 at 11:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.