Cohesive Design for a React Component

Introduction

What is Cohesion?

Separation of Concerns (SoC)

Loose coupling

Why Cohesion is important?

Problem

const Expandable = ({
isOpen,
children,
buttonText,
openIcon = <Icon color="blue" name="angle up" />,
closeIcon = <Icon color="blue" name="angle down" />
}) => {
const [open, setOpen] = useState(isOpen);
const expandableContainerRef = useRef();
const onClickHandler = () => {
setOpen(!open);
}
useEffect(() => {
if (expandableContainerRef && expandableContainerRef.current) {
if (open) {
expandableContainerRef.current.style.maxHeight = expandableContainerRef.current.scrollHeight + "px";
} else {
expandableContainerRef.current.style.maxHeight = 0;
}
}
}, [open, expandableContainerRef])
return (
<div className="container">
<button className="button" onClick={onClickHandler}>
{buttonText}
{!!open ? openIcon : closeIcon}
</button>
<div ref={expandableContainerRef} className="body">
{children}
</div>
</div>
);
};
const Expandable = ({
isOpen,
children,
buttonText,
variant,
wordsSize,
rowsSize,
text,
openIcon = <Icon color="blue" name="angle up" />,
closeIcon = <Icon color="blue" name="angle down" />
}) => {
let body = children;
const
[open, setOpen] = useState(isOpen);
const expandableContainerRef = useRef();
const onClickHandler = () => {
setOpen(!open);
}
useEffect(() => { if (expandableContainerRef && expandableContainerRef.current) {
const currentScrollHeight = expandableContainerRef.current.scrollHeight + "px";
if (open) {
expandableContainerRef.current.style.maxHeight = currentScrollHeight;
} else {

if (!variant) {
expandableContainerRef.current.style.maxHeight = 0;
} else {
expandableContainerRef.current.style.maxHeight = defaultBodyHeight || currentScrollHeight;
}
}
}
}, [open, expandableContainerRef, variant, defaultBodyHeight])
if (variant === "limitByWords") {
const truncatedText = text.split(" ").slice(0, wordsSize).join(" ");
body = !!open ? text : truncatedText;
}
if (variant === "limitByRows") {
const truncatedRows = React.Children.toArray(children).splice(0, rowsSize);
body = !!open ? children : truncatedRows;
}
return (
<div className="container">
<button className="button" onClick={onClickHandler}>
{buttonText}
{!!open ? openIcon : closeIcon}
</button>
<div ref={expandableContainerRef} className="body">
{body}
</div>
</div>
);
};

How to solve the problem?

export const Expandable = ({
isOpen,
children,
buttonText,
initialBodyHeight,
openIcon = <Icon color="blue" name="angle up" />,
closeIcon = <Icon color="blue" name="angle down" />
}) => {
...
...
return (
<div className="container">
<button className="button" onClick={onClickHandler}>
{buttonText}
{!!open ? openIcon : closeIcon}
</button>

{
React.Children.map(children, child => React.cloneElement(child, { open, ref: expandableContainerRef, className: "body"}))
}
</div>
);
};
export const DefaultDisplay = React.forwardRef(
({ className, children }, ref) => (
<div ref={ref} className={className}>
{children}
</div>
);
);
export const DisplayWords = React.forwardRef(
({ className, text, wordsSize, open }, ref) => {
const truncatedText = text
.split(" ")
.splice(0, wordsSize)
.join(" ");
return (
<div className={className} ref={ref}>
{open ? text : `${truncatedText}...`}
</div>
);
}
);
export const DisplayRows = React.forwardRef(
({ children, rowsSize, className, open }, ref) => {
return (
<div className={className} ref={ref}>
{open ? children : React.Children.toArray(children).splice(0, rowsSize)}
</div>
);
}
);
export const DefaultExpandable = () => (
<div style={{ margin: "10px" }}>
<Expandable buttonText="Show more" initialBodyHeight="0px"
<DefaultDisplay>
<div>
<div>Body goes here</div>
<div>Body goes here</div>
<div>Body goes here</div>
<div>Body goes here</div>
</div>
</DefaultDisplay>
</Expandable>
</div>
);
export const LimitWordsExpandable = () => (
<div style={{ margin: "10px" }}>
<Expandable buttonText="Show more">
<DisplayWords text="The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included) where begin and end represent the index of items in that array. The original array will not be modified." wordsSize={10} />
</Expandable>
</div>
);
export const LimitRowsExpandable = () => (
<div style={{ margin: "10px" }}>
<Expandable buttonText="Show more">
<DisplayRows rowsSize={2}>
<div>Body goes here</div>
<div>Body goes here</div>
<div>Body goes here</div>
<div>Body goes here</div>
</DisplayRows>
</Expandable>
</div>
);

Bonus

References

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store