shadcn/ui の asChild/Slot を極める:継承可能な API 設計と責務分離

React でコンポーネントライブラリを設計する際、「柔軟性」と「型安全性」のバランスをどう取るかは永遠の課題です。shadcn/ui が採用している asChild
と Slot
のパターンは、この課題に対する非常にエレガントな解決策を提供しています。
この記事では、shadcn/ui の根幹を支える Radix UI の asChild
と Slot
の仕組みを深掘りし、継承可能な API 設計と責務分離の観点から、その設計思想と実装方法を徹底的に解説します。
背景
従来の as
プロパティの限界
React のコンポーネントライブラリでは、レンダリングする HTML 要素を変更できるようにする as
プロパティがよく使われてきました。
typescript// 従来の as プロパティの例
<Button as='a' href='/home'>
ホームへ
</Button>
このアプローチは一見便利ですが、いくつかの問題を抱えています。
typescript// 型安全性の問題
interface ButtonProps {
as?: React.ElementType;
// どの要素でも受け入れるため、型推論が困難
}
主な課題は以下の通りです。
# | 課題 | 詳細 |
---|---|---|
1 | 型安全性の欠如 | as で指定した要素固有のプロパティが型推論されない |
2 | プロパティの競合 | 親コンポーネントと子要素のプロパティが衝突する |
3 | 複雑なコンポーネントへの対応 | カスタムコンポーネントを渡す際の制約が多い |
4 | ref の転送問題 | ref の適切な転送が困難 |
DOM の肥大化問題
もう一つの課題は、コンポーネントのラッピングによる DOM の肥大化です。
typescript// ラッピングによる DOM の肥大化
<Tooltip>
<Dialog>
<button>クリック</button>
</Dialog>
</Tooltip>
// 結果として生成される DOM
<div> {/* Tooltip wrapper */}
<div> {/* Dialog wrapper */}
<button>クリック</button>
</div>
</div>
この構造では、セマンティックではない div
要素が増え、アクセシビリティやパフォーマンスに悪影響を及ぼします。
以下の図は、従来のアプローチと asChild パターンの DOM 構造の違いを示しています。
mermaidflowchart TB
subgraph old["従来のアプローチ"]
tooltip1["Tooltip (div)"] --> dialog1["Dialog (div)"]
dialog1 --> btn1["button"]
end
subgraph new["asChild パターン"]
tooltip2["Tooltip<br/>(機能のみ)"] -.->|props/handlers| btn2["button"]
dialog2["Dialog<br/>(機能のみ)"] -.->|props/handlers| btn2
end
style old fill:#ffeeee
style new fill:#eeffee
図で理解できる要点:
- 従来のアプローチでは各コンポーネントが DOM 要素を生成し、ネストが深くなる
- asChild パターンでは機能のみを継承し、DOM はフラットに保たれる
課題
コンポーネント設計における責務の曖昧さ
React のコンポーネント設計では、「見た目の制御」と「振る舞いの提供」という 2 つの責務が混在しがちです。
typescript// 責務が混在している例
function Button({ onClick, className, children }) {
// 見た目の制御(スタイル)
const baseStyles =
'px-4 py-2 bg-blue-500 text-white rounded';
// 振る舞いの提供(クリックハンドラ)
const handleClick = (e) => {
console.log('Button clicked');
onClick?.(e);
};
// どちらの責務も持っている
return (
<button
className={`${baseStyles} ${className}`}
onClick={handleClick}
>
{children}
</button>
);
}
この設計では、以下の問題が発生します。
- 要素の変更が困難:
button
以外の要素をレンダリングしたい場合に対応できない - スタイルの上書きが複雑:基本スタイルとカスタムスタイルのマージロジックが必要
- 振る舞いの再利用が難しい:他のコンポーネントで同じ振る舞いを使いたい場合に重複が発生
プロパティマージの複雑性
複数のコンポーネントを組み合わせる際、プロパティのマージ処理は非常に複雑になります。
typescript// プロパティマージの課題
function MyButton({ onClick, className, ...props }) {
// 親の onClick と子の onClick をどうマージする?
// className の結合順序は?
// aria-* 属性の優先順位は?
return (
<CustomComponent
{...props}
onClick={(e) => {
onClick?.(e);
// 親の処理も実行したい
}}
className={/* どう結合する? */}
/>
);
}
特に以下の点が課題となります。
# | プロパティタイプ | マージの課題 |
---|---|---|
1 | イベントハンドラ | 両方実行するか、どちらを優先するか |
2 | className | 結合順序による優先度の制御 |
3 | style | オブジェクトのマージと優先順位 |
4 | aria-* 属性 | アクセシビリティの保証 |
5 | ref | 複数の ref をどう転送するか |
型安全性の確保
TypeScript を使用する場合、動的な要素の変更に対する型推論が課題です。
typescript// 型推論の課題
interface ButtonProps {
as?: React.ElementType;
children: React.ReactNode;
// as="a" の場合は href が必要だが、型で表現できない
}
// 使用時に型エラーが出ない(本来は href が必要)
<Button as='a'>リンク</Button>;
この課題を解決するためには、条件型を駆使した複雑な型定義が必要になり、保守性が低下します。
以下の図は、プロパティマージにおける課題を可視化しています。
mermaidflowchart LR
parent["親コンポーネント"] -->|onClick_A| merge["マージ処理"]
parent -->|className_A| merge
child["子コンポーネント"] -->|onClick_B| merge
child -->|className_B| merge
merge --> q1{"どちらを<br/>優先?"}
merge --> q2{"どう<br/>結合?"}
q1 -.->|課題| result["最終的な<br/>プロパティ"]
q2 -.->|課題| result
style merge fill:#ffeeee
style q1 fill:#ffeeee
style q2 fill:#ffeeee
図で理解できる要点:
- 親と子で同じプロパティが存在する場合、優先順位の決定が必要
- イベントハンドラと className では異なるマージロジックが求められる
解決策
asChild と Slot の基本コンセプト
Radix UI が提供する asChild
と Slot
のパターンは、これらの課題を解決するために設計されました。基本的な考え方は以下の通りです。
責務の明確な分離:
- コンポーネントは「振る舞い」のみを提供
- レンダリングする要素は利用者が決定
typescript// asChild パターンの基本形
import { Slot } from '@radix-ui/react-slot';
function Button({ asChild, ...props }) {
// asChild が true なら Slot、false なら button をレンダリング
const Comp = asChild ? Slot : 'button';
return <Comp {...props} />;
}
このシンプルな実装により、以下が実現できます。
typescript// デフォルトの button として使用
<Button onClick={handleClick}>クリック</Button>
// カスタム要素として使用
<Button asChild>
<a href="/home">ホームへ</a>
</Button>
Slot コンポーネントの仕組み
Slot
コンポーネントは、React の cloneElement
を利用して、プロパティを子要素にマージします。
typescript// Slot の内部実装のイメージ
function Slot({ children, ...props }) {
if (React.isValidElement(children)) {
return React.cloneElement(children, {
...props,
...children.props,
});
}
return null;
}
この実装により、以下の処理が自動的に行われます。
プロパティのマージ:
typescript// 親から渡されたプロパティ
<Button onClick={parentClick} className="parent-class" asChild>
{/* 子のプロパティ */}
<a onClick={childClick} className="child-class" href="/home">
リンク
</a>
</Button>
// 結果として生成される要素
<a
onClick={/* childClick が優先 */}
className="parent-class child-class"
href="/home"
>
リンク
</a>
イベントハンドラの優先順位
Radix UI の Slot
実装では、イベントハンドラに関して明確な優先順位ルールがあります。
typescript// イベントハンドラのマージロジック
function mergeEventHandlers(parentHandler, childHandler) {
return (event) => {
// 子のハンドラを先に実行
childHandler?.(event);
// 子で preventDefault されていなければ親も実行
if (!event.defaultPrevented) {
parentHandler?.(event);
}
};
}
このルールにより、以下の振る舞いが保証されます。
# | 状況 | 結果 |
---|---|---|
1 | 親と子の両方にハンドラがある | 子が先に実行され、その後親が実行 |
2 | 子で preventDefault() を呼ぶ | 親のハンドラは実行されない |
3 | どちらか一方のみ | そのハンドラのみが実行される |
className のマージ戦略
className
のマージには tailwind-merge
などのライブラリを使用することが推奨されます。
typescriptimport { twMerge } from 'tailwind-merge';
function Slot({ children, ...props }) {
if (React.isValidElement(children)) {
return React.cloneElement(children, {
...props,
...children.props,
// 親と子の className をマージ
className: twMerge(
props.className,
children.props.className
),
});
}
return null;
}
これにより、Tailwind CSS のクラスが適切に上書きされます。
typescript// 親のスタイル
<Button className='bg-blue-500 px-4' asChild>
{/* 子のスタイルで bg-red-500 が bg-blue-500 を上書き */}
<a className='bg-red-500 py-2'>リンク</a>
</Button>
// 結果: bg-red-500 px-4 py-2
責務分離の実現
asChild
パターンにより、コンポーネントの責務が明確に分離されます。
typescript// 振る舞いを提供するコンポーネント
function DialogTrigger({ asChild, ...props }) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
// Dialog を開く振る舞いを提供
onClick={openDialog}
aria-haspopup='dialog'
aria-expanded={isOpen}
{...props}
/>
);
}
利用者は見た目を自由に決定できます。
typescript// ケース1: デフォルトの button
<DialogTrigger>開く</DialogTrigger>
// ケース2: カスタムスタイルのボタン
<DialogTrigger asChild>
<button className="custom-button">開く</button>
</DialogTrigger>
// ケース3: アイコンボタン
<DialogTrigger asChild>
<IconButton icon={<MenuIcon />} />
</DialogTrigger>
以下の図は、asChild パターンにおける責務分離を示しています。
mermaidflowchart TB
subgraph library["ライブラリの責務"]
behavior["振る舞いの提供"]
behavior --> onclick["onClick ハンドラ"]
behavior --> aria["aria-* 属性"]
behavior --> state["状態管理"]
end
subgraph user["利用者の責務"]
appearance["見た目の決定"]
appearance --> element["要素の選択"]
appearance --> styling["スタイリング"]
appearance --> content["コンテンツ"]
end
library -.->|asChild| merge["Slot による<br/>マージ"]
user -.->|children| merge
merge --> final["最終的な<br/>コンポーネント"]
style library fill:#e3f2fd
style user fill:#fff3e0
style merge fill:#e8f5e9
図で理解できる要点:
- ライブラリは振る舞い(onClick、aria 属性、状態管理)を提供
- 利用者は見た目(要素、スタイル、コンテンツ)を決定
- Slot が両者をマージして最終的なコンポーネントを生成
具体例
基本的な実装例
まず、asChild
をサポートするシンプルなボタンコンポーネントを実装してみます。
パッケージのインストール
bashyarn add @radix-ui/react-slot
基本的な Button コンポーネント
typescriptimport { Slot } from '@radix-ui/react-slot';
import { forwardRef } from 'react';
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
ButtonProps
インターフェースは、通常の button
要素のプロパティを継承し、asChild
プロパティを追加しています。
typescriptconst Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, className, ...props }, ref) => {
// asChild が true なら Slot、false なら button
const Comp = asChild ? Slot : 'button';
return (
<Comp ref={ref} className={className} {...props} />
);
}
);
Button.displayName = 'Button';
forwardRef
を使用することで、ref を適切に転送できます。これは、親コンポーネントが DOM 要素への参照を取得する際に必要です。
使用例
typescript// ケース1: デフォルトの button として使用
<Button onClick={() => console.log('clicked')}>
クリック
</Button>
このケースでは、通常の button
要素がレンダリングされます。
typescript// ケース2: Next.js の Link として使用
import Link from 'next/link';
<Button asChild>
<Link href='/dashboard'>ダッシュボードへ</Link>
</Button>;
asChild
を指定することで、Button
の振る舞いが Link
コンポーネントに継承されます。
typescript// ケース3: アンカータグとして使用
<Button asChild>
<a
href='https://example.com'
target='_blank'
rel='noopener noreferrer'
>
外部リンク
</a>
</Button>
a
要素を使用する場合、href
などのアンカー固有のプロパティが適切に機能します。
スタイリングを含む実装例
shadcn/ui スタイルの、より実践的なボタンコンポーネントを実装します。
スタイル定義
typescriptimport {
cva,
type VariantProps,
} from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';
const buttonVariants = cva(
// 基本スタイル
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
ghost:
'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
class-variance-authority
を使用することで、バリアントベースのスタイル管理が可能になります。
Button コンポーネントの実装
typescriptinterface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, variant, size, asChild = false, ...props },
ref
) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={twMerge(
buttonVariants({ variant, size, className })
)}
ref={ref}
{...props}
/>
);
}
);
twMerge
を使用して、バリアントのスタイルとカスタムクラス名を適切にマージしています。
使用例
typescript// バリアントの使用
<Button variant='destructive' size='lg'>
削除
</Button>
typescript// カスタムクラス名の追加
<Button className='w-full' variant='outline'>
全幅ボタン
</Button>
typescript// asChild と組み合わせ
<Button asChild variant='ghost'>
<Link href='/profile'>プロフィール</Link>
</Button>
複数コンポーネントの合成
asChild
の真価は、複数のコンポーネントを合成する際に発揮されます。
Tooltip と Button の合成
typescriptimport {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
function Example() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant='outline'>
ホバーしてください
</Button>
</TooltipTrigger>
<TooltipContent>
<p>ツールチップの内容</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
TooltipTrigger
に asChild
を指定することで、Button
が Tooltip のトリガーとして機能します。DOM には余計なラッパー要素が生成されません。
Dialog と Button の合成
typescriptimport {
Dialog,
DialogContent,
DialogTrigger,
} from '@/components/ui/dialog';
function Example() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>ダイアログを開く</Button>
</DialogTrigger>
<DialogContent>
<p>ダイアログの内容</p>
</DialogContent>
</Dialog>
);
}
多重合成の例
typescript// Tooltip、Dialog、Button を同時に合成
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Dialog>
<DialogTrigger asChild>
<Button variant='outline'>開く</Button>
</DialogTrigger>
<DialogContent>
<p>ダイアログの内容</p>
</DialogContent>
</Dialog>
</TooltipTrigger>
<TooltipContent>
<p>クリックでダイアログを開きます</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
この例では、Button
に対して Tooltip と Dialog の機能が両方継承されています。
以下の図は、複数コンポーネントの合成フローを示しています。
mermaidflowchart TB
tooltip["Tooltip<br/>(asChild)"] -->|onMouseEnter<br/>onMouseLeave| merge1["Slot マージ①"]
dialog["Dialog<br/>(asChild)"] -->|onClick<br/>aria-haspopup| merge1
merge1 -->|マージされた<br/>props| merge2["Slot マージ②"]
button["Button"] -->|className<br/>variant| merge2
merge2 --> final["最終的な button 要素"]
final -.->|生成される HTML| dom["<button<br/> onClick=...<br/> onMouseEnter=...<br/> aria-haspopup=...<br/> class=...>"]
style merge1 fill:#e8f5e9
style merge2 fill:#e8f5e9
style final fill:#fff3e0
図で理解できる要点:
- 各コンポーネントが独自の振る舞い(イベントハンドラや aria 属性)を提供
- Slot が段階的にプロパティをマージ
- 最終的に単一の button 要素として DOM に出力
TypeScript での型安全な実装
asChild
を使用する際、TypeScript の型推論を適切に機能させるための実装例です。
AsChildProps 型の定義
typescripttype AsChildProps<DefaultElementProps> =
| ({ asChild?: false } & DefaultElementProps)
| { asChild: true; children: React.ReactNode };
この型定義により、asChild
の値に応じて異なるプロパティが要求されます。
Button コンポーネントへの適用
typescripttype ButtonPropsBase = {
variant?: 'default' | 'destructive' | 'outline' | 'ghost';
size?: 'default' | 'sm' | 'lg';
className?: string;
};
type ButtonDefaultProps = ButtonPropsBase &
React.ButtonHTMLAttributes<HTMLButtonElement>;
type ButtonProps = AsChildProps<ButtonDefaultProps>;
typescriptconst Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
if (props.asChild) {
// asChild が true の場合
return <Slot ref={ref}>{props.children}</Slot>;
}
// asChild が false の場合
const { variant, size, className, ...rest } = props;
return (
<button
ref={ref}
className={twMerge(
buttonVariants({ variant, size, className })
)}
{...rest}
/>
);
}
);
使用時の型チェック
typescript// OK: asChild が false の場合、onClick などが使える
<Button onClick={handleClick} variant="default">
クリック
</Button>
// OK: asChild が true の場合、children が必須
<Button asChild>
<a href="/home">ホーム</a>
</Button>
// エラー: asChild が true なのに children がない
<Button asChild />
// エラー: asChild が true なのに onClick を指定
<Button asChild onClick={handleClick}>
<a href="/home">ホーム</a>
</Button>
カスタム Slot の実装
Radix UI の Slot
をカスタマイズして、独自のマージロジックを実装することもできます。
イベントハンドラのカスタムマージ
typescriptimport React from 'react';
import { Slot as RadixSlot } from '@radix-ui/react-slot';
function mergeProps(parentProps: any, childProps: any) {
const merged = { ...parentProps };
// すべてのプロパティをループ
for (const key in childProps) {
const parentValue = parentProps[key];
const childValue = childProps[key];
// イベントハンドラの場合
if (
key.startsWith('on') &&
typeof childValue === 'function'
) {
merged[key] = (...args: any[]) => {
// 子を先に実行
childValue?.(...args);
// 親を後に実行
parentValue?.(...args);
};
}
// className の場合
else if (key === 'className') {
merged[key] = twMerge(parentValue, childValue);
}
// style の場合
else if (key === 'style') {
merged[key] = { ...parentValue, ...childValue };
}
// その他のプロパティは子を優先
else {
merged[key] = childValue;
}
}
return merged;
}
このカスタムマージロジックにより、プロパティごとに異なる処理を適用できます。
カスタム Slot コンポーネント
typescriptconst CustomSlot = forwardRef<any, any>(
({ children, ...props }, ref) => {
if (React.isValidElement(children)) {
const mergedProps = mergeProps(props, children.props);
return React.cloneElement(children, {
...mergedProps,
ref,
});
}
return null;
}
);
使用例
typescriptfunction MyButton({ asChild, onClick, ...props }) {
const Comp = asChild ? CustomSlot : 'button';
return (
<Comp
onClick={(e) => {
console.log('親の onClick');
onClick?.(e);
}}
{...props}
/>
);
}
// 使用時
<MyButton
asChild
onClick={() => console.log('props の onClick')}
>
<button onClick={() => console.log('子の onClick')}>
クリック
</button>
</MyButton>;
// 出力順序:
// 1. "子の onClick"
// 2. "親の onClick"
// 3. "props の onClick"
ref の適切な転送
複数のコンポーネントが合成される場合、ref の転送にも注意が必要です。
複数 ref のマージ
typescriptfunction mergeRefs<T>(
...refs: (React.Ref<T> | undefined)[]
): React.RefCallback<T> {
return (instance: T | null) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(instance);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current =
instance;
}
});
};
}
Button コンポーネントでの使用
typescriptconst Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild, ...props }, forwardedRef) => {
const internalRef = useRef<HTMLButtonElement>(null);
// 内部の ref と外部から渡された ref をマージ
const mergedRef = mergeRefs(internalRef, forwardedRef);
const Comp = asChild ? Slot : 'button';
return <Comp ref={mergedRef} {...props} />;
}
);
使用例
typescriptfunction Parent() {
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
// ref を通じて DOM 要素にアクセス
buttonRef.current?.focus();
}, []);
return (
<Button ref={buttonRef} asChild>
<a href='/home'>ホーム</a>
</Button>
);
}
この実装により、asChild
を使用した場合でも、親コンポーネントから DOM 要素への参照を取得できます。
まとめ
shadcn/ui の asChild
と Slot
パターンは、React コンポーネント設計における重要な進化です。このパターンにより、以下が実現されています。
実現された価値
# | 項目 | 詳細 |
---|---|---|
1 | 責務の明確な分離 | ライブラリは振る舞いのみを提供し、見た目は利用者が決定 |
2 | DOM の最適化 | 不要なラッパー要素を排除し、セマンティックな HTML を生成 |
3 | 柔軟な合成 | 複数のコンポーネントの機能を単一の要素に統合可能 |
4 | 型安全性の向上 | TypeScript による厳密な型チェックが可能 |
5 | 保守性の改善 | 明確な API により、コードの理解と拡張が容易 |
設計原則
asChild
/ Slot
パターンは、以下の設計原則を体現しています。
単一責任の原則: 各コンポーネントは「振る舞い」または「見た目」のいずれか一方に責任を持ちます。
開放閉鎖の原則: 既存のコンポーネントを変更せず、新しい要素への適用が可能です。
依存性逆転の原則: 具体的な HTML 要素に依存せず、抽象的なインターフェースに依存します。
適用のベストプラクティス
このパターンを効果的に活用するためには、以下を意識しましょう。
- コンポーネントライブラリの設計時:デフォルトの振る舞いを提供しつつ、
asChild
で柔軟性を確保する - 利用者としての使用時:DOM 構造を意識し、セマンティックな要素を選択する
- 型定義の整備:
AsChildProps
型を活用し、型安全性を確保する - ref の転送:
forwardRef
と ref のマージを適切に実装する - プロパティのマージ:
twMerge
などを使用し、スタイルの競合を回避する
今後の展望
React のエコシステムでは、asChild
/ Slot
パターンがますます重要になっていくでしょう。このパターンは、コンポーネントの再利用性と柔軟性を高めながら、型安全性とパフォーマンスを維持する優れた方法です。
shadcn/ui や Radix UI の成功は、このパターンの有効性を証明しています。今後、より多くのライブラリがこのアプローチを採用し、React コンポーネント設計の標準的な手法になっていくことが期待されます。
関連リンク
- article
shadcn/ui の asChild/Slot を極める:継承可能な API 設計と責務分離
- article
shadcn/ui カラートークン早見表:ブランドカラー最適化&明暗コントラスト基準
- article
shadcn/ui を Monorepo(Turborepo/pnpm)に導入するベストプラクティス
- article
shadcn/ui と Headless UI/Vanilla Radix を徹底比較:実装量・a11y・可読性の差
- article
shadcn/ui の思想を徹底解剖:なぜ「コピーして使う」アプローチが拡張性に強いのか
- article
shadcn/ui でダッシュボードをデザインするベストプラクティス
- article
CI/CD で更新を自動化:GitHub Actions と WordPress の安全デプロイ
- article
NestJS クリーンアーキテクチャ:UseCase/Domain/Adapter を疎結合に保つ設計術
- article
WebSocket プロトコル設計:バージョン交渉・機能フラグ・後方互換のパターン
- article
MySQL 読み書き分離設計:ProxySQL で一貫性とスループットを両立
- article
Motion(旧 Framer Motion)アニメオーケストレーション設計:timeline・遅延・相互依存の整理術
- article
WebRTC で遠隔支援:画面注釈・ポインタ共有・低遅延音声の実装事例
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来