- Replace .eslintrc.cjs with eslint.config.mjs (ESLint 9 flat config)
using direct eslint-plugin-solid + @typescript-eslint/parser approach
- Add @typescript-eslint/parser to root devDependencies
- Add main/module/types top-level fields to packages/core/package.json
- Add resolve.conditions to packages/core/vite.config.ts
- Create packages/core/tsconfig.test.json for test type-checking
- Remove empty paths:{} from packages/core/tsconfig.json
226 lines
7.5 KiB
Markdown
226 lines
7.5 KiB
Markdown
Chris created a Dropdown menu as a shared component for the team.
|
|
|
|
At first, it was simple. It just needed to show a list when a button was pressed.
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
<Dropdown items={['A', 'B']} />
|
|
```
|
|
|
|
But then, requirements started pouring in.
|
|
|
|
"Please add icons inside the menu."
|
|
"Add a search bar, too."
|
|
"Move the button position to the right."
|
|
|
|
Chris kept adding Props like showSearch, iconPosition, and customButton, eventually creating a monster component with over 20 props.
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
// ❌ Prop Explosion
|
|
<Dropdown
|
|
items={items}
|
|
showSearch={true}
|
|
searchPlaceholder="Search..."
|
|
icon="arrow"
|
|
position="bottom-right"
|
|
onSelect={handleSelect}
|
|
buttonColor="blue"
|
|
// ... It never ends
|
|
/>
|
|
```
|
|
|
|
Such "Configuration-based" components are rigid.
|
|
|
|
Today, we will learn how to secure true reusability through the Compound Component Pattern, which involves assembling parts like the HTML <select> tag, and the Headless UI concept, which completely separates style from logic.
|
|
|
|
## 1\. Wisdom from HTML
|
|
|
|
Let's look at the HTML <select> tag.
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
<select>
|
|
<option value="1">Option 1</option>
|
|
<option value="2">Option 2</option>
|
|
</select>
|
|
```
|
|
|
|
We don't write <select options={\['1', '2'\]} />.
|
|
|
|
Instead, two components, <select> and <option>, work together. <select> manages the state (selected value), and <option> handles the UI. This is the prototype of the Compound Component Pattern.
|
|
|
|
## 2\. Implementing Compound Components
|
|
|
|
To implement this pattern in React, we need the Context API. This is because the parent component needs to share state implicitly with its children.
|
|
|
|
### Step 1: Create Context
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
// SelectContext.tsx
|
|
import { createContext, useContext, useState } from 'react';
|
|
|
|
type SelectContextType = {
|
|
isOpen: boolean;
|
|
toggle: () => void;
|
|
selected: string;
|
|
select: (value: string) => void;
|
|
};
|
|
|
|
const SelectContext = createContext<SelectContextType | null>(null);
|
|
|
|
// Custom hook for safe usage
|
|
export const useSelectContext = () => {
|
|
const context = useContext(SelectContext);
|
|
if (!context) throw new Error("Select.* components must be used within Select.");
|
|
return context;
|
|
};
|
|
```
|
|
|
|
### Step 2: Assemble Parent and Child Components
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
// Select.tsx
|
|
export function Select({ children }: { children: React.ReactNode }) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [selected, setSelected] = useState("");
|
|
|
|
const value = {
|
|
isOpen,
|
|
toggle: () => setIsOpen(!isOpen),
|
|
selected,
|
|
select: (val: string) => {
|
|
setSelected(val);
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SelectContext.Provider value={value}>
|
|
<div className="select-root">{children}</div>
|
|
</SelectContext.Provider>
|
|
);
|
|
}
|
|
|
|
// Child Components (Sub Components)
|
|
Select.Trigger = function Trigger({ children }: { children: React.ReactNode }) {
|
|
const { toggle, selected } = useSelectContext();
|
|
return <button onClick={toggle}>{selected || children}</button>;
|
|
};
|
|
|
|
Select.Menu = function Menu({ children }: { children: React.ReactNode }) {
|
|
const { isOpen } = useSelectContext();
|
|
if (!isOpen) return null;
|
|
return <ul className="select-menu">{children}</ul>;
|
|
};
|
|
|
|
Select.Option = function Option({ value, children }: { value: string, children: React.ReactNode }) {
|
|
const { select } = useSelectContext();
|
|
return <li onClick={() => select(value)}>{children}</li>;
|
|
};
|
|
```
|
|
|
|
### Step 3: Usage
|
|
|
|
Now Chris has escaped Prop Hell. He can freely place the elements he wants, where he wants them.
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
// ✅ Readable and Flexible
|
|
<Select>
|
|
<Select.Trigger>Open Menu</Select.Trigger>
|
|
<Select.Menu>
|
|
<div className="search-box">You can even put a search bar here</div>
|
|
<Select.Option value="apple">Apple 🍎</Select.Option>
|
|
<Select.Option value="banana">Banana 🍌</Select.Option>
|
|
</Select.Menu>
|
|
</Select>
|
|
```
|
|
|
|
## 3\. Headless UI: Logic Without Style
|
|
|
|
The structure has become flexible thanks to Compound Components, but if styles (e.g., className="select-menu") are still hardcoded inside, it's difficult to use in other design systems.
|
|
|
|
Headless UI goes one step further.
|
|
|
|
"I'll provide the functionality (logic), you apply the style (UI)."
|
|
|
|
Representative libraries include Radix UI, Headless UI (Tailwind), and React Aria.
|
|
|
|
### Conceptual Code of a Headless Component
|
|
|
|
typescript
|
|
|
|
```typescript
|
|
// useSelect.ts (Separate logic into a Hook)
|
|
function useSelect(items) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
// ... Complex logic like keyboard navigation, focus management, ARIA attributes ...
|
|
|
|
return {
|
|
isOpen,
|
|
triggerProps: {
|
|
'aria-expanded': isOpen,
|
|
onClick: toggle
|
|
},
|
|
menuProps: {
|
|
role: 'listbox'
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
Developers can take this hook and apply designs using whatever they like, whether it's styled-components or Tailwind CSS. This is the pinnacle of reusability.
|
|
|
|
## 4\. When to Use Which Pattern?
|
|
|
|
| | | |
|
|
| --- | --- | --- |
|
|
| Pattern | Characteristics | Recommended Situation |
|
|
| Monolithic | Everything is one chunk. Controlled via Props. | Simple atomic components like buttons, inputs. |
|
|
| Compound | Parent-Child cooperation. Flexible structure. | UIs with complex internal structures like Dropdowns, Accordions, Tabs, Modals. |
|
|
| Headless | No style, logic only. | When building shared libraries where design requirements vary wildly or Accessibility (A11y) is critical. |
|
|
|
|
## Key Takeaways
|
|
|
|
Problem: Stuffing all requirements into Props turns a component into an unmaintainable monster.
|
|
Compound Component: Uses <Context> to share state and allows the user to assemble child components (Select.Option) themselves. This is Inversion of Control.
|
|
Headless UI: Completely separates function and style to maximize design freedom.
|
|
|
|
* * *
|
|
|
|
Now that we've made the structure flexible, it's time to separate business logic from UI code. If useEffect and return <div> are mixed in one component, it's hard to read.
|
|
|
|
Let's look at the Custom Hook Pattern, the modern interpretation of the "Presentational & Container" pattern, to separate them cleanly.
|
|
|
|
Continuing in: “Separation of Concerns: Separating View and Business Logic (Custom Hook)”
|
|
|
|
### 🔗 References
|
|
|
|
[Radix UI - Primitives](https://www.radix-ui.com/primitives)
|
|
[React Docs - Context](https://react.dev/learn/passing-data-deeply-with-context)
|
|
[Kent C. Dodds - Compound Components](https://kentcdodds.com/blog/compound-components-with-react-hooks)
|
|
|
|
[← Read more posts](https://www.visionexperiencedeveloper.com/en)
|
|
|
|
## Comments (0)
|
|
|
|
Write a comment
|
|
|
|
0/1000 characters
|
|
|
|
Submit
|
|
|
|
No comments yet. Be the first to comment!
|
|
|
|
### More in Architecture and Design
|
|
|
|
[**Building a CI/CD Pipeline with Vercel and Managing Environment Variables** 1/26/2026](https://www.visionexperiencedeveloper.com/en/building-a-cicd-pipeline-with-vercel-and-managing-environment-variables) [**Testing by Layer with Vitest: What to Test and What to Give Up** 1/25/2026](https://www.visionexperiencedeveloper.com/en/testing-by-layer-with-vitest-what-to-test-and-what-to-give-up) [**React Clean Architecture: A 3-Layer Separation Strategy (Domain, Data, Presentation)** 1/24/2026](https://www.visionexperiencedeveloper.com/en/react-clean-architecture-a-3-layer-separation-strategy-domain-data-presentation) [**Frontend DTOs: Building a Mapper Layer Unshaken by Backend API Changes** 1/23/2026](https://www.visionexperiencedeveloper.com/en/frontend-dtos-building-a-mapper-layer-unshaken-by-backend-api-changes) [**Separation of Concerns: Separating View and Business Logic (Custom Hook)** 1/22/2026](https://www.visionexperiencedeveloper.com/en/separation-of-concerns-separating-view-and-business-logic-custom-hook) |