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.