Jotai × tRPC 初期配線:型安全 RPC とローカル状態の統合
TypeScript で開発していると、「型の恩恵を最大限に受けたい」と思いますよね。特に、サーバーとクライアント間のデータのやり取りで型が壊れてしまうと、バグの温床になってしまいます。
今回は、型安全な RPC フレームワークである tRPC と、軽量で柔軟な状態管理ライブラリ Jotai を組み合わせた初期配線の方法をご紹介します。この組み合わせにより、サーバーからクライアントまで一貫した型安全性を保ちながら、ローカル状態とリモート状態を統合的に管理できるのです。
実際のプロジェクトで導入する際の具体的な手順を、コード例とともに段階的に解説していきますね。
背景
Jotai とは何か
Jotai は、React アプリケーションのための最小限の状態管理ライブラリです。「atom(原子)」という小さな状態の単位を組み合わせて、複雑な状態を構築していく思想が特徴ですね。
Redux や Zustand などの既存のライブラリと比較すると、以下のような特徴があります。
| # | 特徴 | 説明 |
|---|---|---|
| 1 | 最小限の API | シンプルで学習コストが低い |
| 2 | アトミックな設計 | 必要な状態だけを購読できる |
| 3 | TypeScript フレンドリー | 型推論が優れている |
| 4 | React Suspense 対応 | 非同期処理と相性が良い |
tRPC とは何か
tRPC は、End-to-End の型安全性を実現する RPC フレームワークです。GraphQL のようなスキーマ定義言語を使わず、TypeScript の型システムだけで型安全な API を構築できます。
以下の図は、tRPC を使った場合のデータフローを示しています。
mermaidflowchart LR
client["クライアント<br/>(React)"] -->|型安全な呼び出し| trpc["tRPC Client"]
trpc -->|HTTP Request| server["tRPC Server"]
server -->|型情報を共有| router["Router<br/>(型定義)"]
router -->|Response| server
server -->|HTTP Response| trpc
trpc -->|型安全なデータ| client
図で理解できる要点:
- クライアントとサーバー間で型情報が自動的に共有される
- 手動でスキーマを書く必要がない
- エンドツーエンドで型安全性が保証される
なぜ両者を組み合わせるのか
Jotai は優れたローカル状態管理を提供しますが、サーバーとの通信部分は別途実装する必要があります。一方、tRPC は型安全なサーバー通信を実現しますが、クライアントサイドの状態管理機能は持っていません。
この二つを組み合わせることで、次のようなメリットが得られるのです。
| # | メリット | 詳細 |
|---|---|---|
| 1 | 一貫した型安全性 | サーバーからクライアントまで途切れない型チェック |
| 2 | 柔軟な状態管理 | リモート状態とローカル状態を統一的に扱える |
| 3 | 最小限の再レンダリング | Jotai のアトミック設計で効率的な更新 |
| 4 | 開発体験の向上 | 自動補完とエラー検出が強力 |
課題
型安全性の断絶
従来の REST API や GraphQL を使った開発では、クライアントとサーバー間で型情報が断絶しがちです。OpenAPI や GraphQL Code Generator などのツールで型を生成できますが、手動での同期作業が必要になってしまいます。
以下の図は、従来の開発における型の断絶ポイントを示しています。
mermaidflowchart TB
subgraph server_side["サーバーサイド"]
server_type["TypeScript 型定義"]
end
subgraph boundary["境界(型が断絶)"]
api["REST API / GraphQL"]
end
subgraph client_side["クライアントサイド"]
codegen["コード生成ツール"]
client_type["生成された型"]
manual["手動同期"]
end
server_type -->|エクスポート| api
api -->|スキーマ| codegen
codegen -->|生成| client_type
server_type -.->|手動で同期| manual
manual -.->|更新| client_type
図で理解できる要点:
- API の境界で型情報が失われる
- コード生成ツールによる間接的な型共有が必要
- サーバー側の型変更が自動的にクライアントに反映されない
状態管理の複雑化
サーバーから取得したデータをクライアントで状態として管理する場合、以下のような課題に直面します。
| # | 課題 | 影響 |
|---|---|---|
| 1 | キャッシュ管理 | 同じデータを何度も取得してしまう |
| 2 | 楽観的更新 | UI の応答性を保つための実装が複雑 |
| 3 | エラーハンドリング | ローカルとリモートで異なる処理が必要 |
| 4 | ローディング状態 | 複数の非同期処理の状態管理が煩雑 |
セットアップの難しさ
Jotai と tRPC を組み合わせようとすると、初期設定でつまずくポイントがいくつかあります。特に、以下のような疑問が生じやすいですね。
- tRPC のクライアントをどこで初期化するか
- Jotai の atom 内で tRPC をどう呼び出すか
- 型情報をどうやって両者間で共有するか
- エラーやローディング状態をどう統合管理するか
解決策
アーキテクチャの全体像
Jotai と tRPC を統合したアーキテクチャでは、tRPC を通じてサーバーから取得したデータを Jotai の atom で管理します。これにより、型安全性を保ちながら柔軟な状態管理が実現できるのです。
以下の図は、提案するアーキテクチャの全体像を示しています。
mermaidflowchart TB
subgraph react["React コンポーネント"]
component["Component"]
end
subgraph jotai_layer["Jotai レイヤー"]
atom_query["queryAtom<br/>(tRPC 呼び出し)"]
atom_local["localAtom<br/>(ローカル状態)"]
end
subgraph trpc_layer["tRPC レイヤー"]
trpc_client["tRPC Client"]
trpc_router["Router 型定義"]
end
subgraph server["サーバー"]
trpc_server["tRPC Server"]
db[("Database")]
end
component -->|useAtom| atom_query
component -->|useAtom| atom_local
atom_query -->|trpc.user.get| trpc_client
trpc_client -->|HTTP| trpc_server
trpc_server -->|クエリ| db
trpc_router -.->|型共有| atom_query
trpc_router -.->|型共有| trpc_client
図で理解できる要点:
- React コンポーネントは atom を通じてデータにアクセス
- atom 内で tRPC クライアントを呼び出す
- 型情報は Router から自動的に共有される
プロジェクトのセットアップ
まず、必要なパッケージをインストールします。Next.js プロジェクトを想定していますが、他の React フレームワークでも同様の手順で導入できますよ。
パッケージのインストール
bash# tRPC 関連
yarn add @trpc/server @trpc/client @trpc/react-query @trpc/next
# React Query(tRPC が内部で使用)
yarn add @tanstack/react-query
# Jotai
yarn add jotai
# Zod(バリデーション用)
yarn add zod
TypeScript の設定確認
tsconfig.json で厳格な型チェックを有効にしておくと、型安全性の恩恵を最大限に受けられます。
json{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": false
}
}
tRPC サーバーの構築
Router の型定義
tRPC のサーバーサイドでは、まず Router を定義します。この Router の型が、クライアント側で自動的に利用されるのです。
typescript// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
// tRPC インスタンスの初期化
const t = initTRPC.create();
// Router とプロシージャの作成
export const router = t.router;
export const publicProcedure = t.procedure;
プロシージャの実装
次に、実際の API エンドポイント(プロシージャ)を定義します。ここでは、ユーザー情報を取得する例を示しますね。
typescript// server/routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
// ユーザー型の定義(Zod スキーマ)
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
createdAt: z.date(),
});
export const userRouter = router({
// ユーザー取得
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
// データベースからユーザーを取得(例)
const user = await db.user.findUnique({
where: { id: input.id },
});
return user;
}),
});
ルートルーターの作成
個別のルーターを統合して、アプリケーション全体のルートルーターを作成します。
typescript// server/routers/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
// すべてのルーターを統合
export const appRouter = router({
user: userRouter,
post: postRouter,
});
// Router の型をエクスポート(重要!)
export type AppRouter = typeof appRouter;
この AppRouter 型が、クライアント側で型安全な呼び出しを実現するための鍵になります。
tRPC クライアントの設定
クライアントの初期化
クライアント側では、tRPC クライアントを初期化します。Next.js の場合、utils/trpc.ts に配置するのが一般的ですね。
typescript// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
// tRPC React フックの作成
export const trpc = createTRPCReact<AppRouter>();
型引数に AppRouter を渡すことで、サーバー側の型情報がクライアント側でも利用できるようになります。
Provider の設定
React アプリケーションのルートで、tRPC と React Query の Provider を設定します。
typescript// pages/_app.tsx
import { useState } from 'react';
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from '@/utils/trpc';
function MyApp({ Component, pageProps }) {
// QueryClient の初期化
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1分間キャッシュ
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
tRPC Provider の追加
tRPC 専用の Provider も追加します。
typescript// pages/_app.tsx(続き)
function MyApp({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
// tRPC クライアントの設定
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc', // API エンドポイント
}),
],
})
);
return (
<trpc.Provider
client={trpcClient}
queryClient={queryClient}
>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}
Jotai との統合
ここからが本題です。tRPC と Jotai を統合して、型安全な状態管理を実現していきましょう。
tRPC を呼び出す atom の作成
Jotai の atomWithQuery を使うと、React Query ライクな非同期処理を atom で扱えます。しかし、tRPC との統合ではもう少し工夫が必要なのです。
typescript// atoms/user.ts
import { atom } from 'jotai';
import { trpc } from '@/utils/trpc';
// tRPC クライアントを取得する atom(singleton)
const trpcClientAtom = atom(() => {
return trpc.useContext();
});
ユーザー情報を管理する atom
ユーザー ID を受け取ってユーザー情報を取得する atom を作成します。
typescript// atoms/user.ts(続き)
import { atomFamily } from 'jotai/utils';
// ユーザー ID ごとに atom を作成
export const userAtomFamily = atomFamily((userId: string) =>
atom(async (get) => {
// tRPC 経由でユーザー情報を取得
const client = get(trpcClientAtom);
const user = await client.user.getById.fetch({
id: userId,
});
return user;
})
);
atomFamily を使うことで、ユーザー ID ごとに個別の atom インスタンスが作成され、効率的なキャッシュが実現できますよ。
ローカル状態との統合
リモートから取得したデータと、ローカルで管理する状態を組み合わせる例も見てみましょう。
typescript// atoms/userEdit.ts
import { atom } from 'jotai';
import { userAtomFamily } from './user';
// 編集中のユーザー情報を管理する atom
export const editingUserAtom = atom(
// 読み取り:リモートのユーザー情報を取得
(get) => {
const userId = get(selectedUserIdAtom);
if (!userId) return null;
return get(userAtomFamily(userId));
},
// 書き込み:ローカルで編集内容を保持
(get, set, update: Partial<User>) => {
const current = get(editingUserAtom);
if (!current) return;
// 編集内容をマージ
set(localEditAtom, { ...current, ...update });
}
);
このパターンにより、サーバーのデータを元に、ローカルでの編集状態を管理できるようになります。
具体例
ユーザー一覧の表示
実際のコンポーネントで、Jotai と tRPC を組み合わせて使う例をご紹介します。
型定義の準備
まず、共通で使う型を定義しておきます。
typescript// types/user.ts
export type User = {
id: string;
name: string;
email: string;
createdAt: Date;
};
export type UserListFilter = {
search: string;
sortBy: 'name' | 'createdAt';
sortOrder: 'asc' | 'desc';
};
フィルター状態の atom
ユーザー一覧のフィルター条件を管理する atom を作成します。
typescript// atoms/userList.ts
import { atom } from 'jotai';
import type { UserListFilter } from '@/types/user';
// フィルター条件の初期値
const initialFilter: UserListFilter = {
search: '',
sortBy: 'name',
sortOrder: 'asc',
};
// フィルター条件を管理する atom
export const userFilterAtom =
atom<UserListFilter>(initialFilter);
ユーザー一覧取得の atom
フィルター条件に基づいてユーザー一覧を取得する atom を作成します。
typescript// atoms/userList.ts(続き)
import { trpc } from '@/utils/trpc';
// ユーザー一覧を取得する atom
export const userListAtom = atom(async (get) => {
const filter = get(userFilterAtom);
// tRPC 経由でユーザー一覧を取得
const users = await trpc.user.list.query({
search: filter.search,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
});
return users;
});
型情報は tRPC の Router から自動的に推論されるため、手動で型を指定する必要はありません。
コンポーネントでの利用
作成した atom をコンポーネントで使ってみましょう。
typescript// components/UserList.tsx
import { useAtom, useAtomValue } from 'jotai';
import {
userListAtom,
userFilterAtom,
} from '@/atoms/userList';
import { Suspense } from 'react';
export function UserList() {
return (
<div>
<UserListFilter />
<Suspense fallback={<div>読み込み中...</div>}>
<UserListContent />
</Suspense>
</div>
);
}
フィルターコンポーネント
フィルター条件を変更する UI を作成します。
typescript// components/UserListFilter.tsx
function UserListFilter() {
const [filter, setFilter] = useAtom(userFilterAtom);
return (
<div>
<input
type='text'
placeholder='ユーザー名で検索'
value={filter.search}
onChange={(e) =>
setFilter({
...filter,
search: e.target.value,
})
}
/>
<select
value={filter.sortBy}
onChange={(e) =>
setFilter({
...filter,
sortBy: e.target.value as 'name' | 'createdAt',
})
}
>
<option value='name'>名前順</option>
<option value='createdAt'>登録日順</option>
</select>
</div>
);
}
一覧表示コンポーネント
実際にユーザー一覧を表示するコンポーネントです。
typescript// components/UserListContent.tsx
function UserListContent() {
// ユーザー一覧を取得
const users = useAtomValue(userListAtom);
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<time>{user.createdAt.toLocaleDateString()}</time>
</li>
))}
</ul>
);
}
useAtomValue で atom の値を読み取るだけで、tRPC を通じてサーバーからデータが取得され、型安全に利用できるのです。
ミューテーション(更新処理)の実装
次に、データを更新する処理を実装してみましょう。
更新用プロシージャの定義
サーバー側に、ユーザー情報を更新するプロシージャを追加します。
typescript// server/routers/user.ts(追加)
export const userRouter = router({
// 既存の getById...
// ユーザー更新
update: publicProcedure
.input(
z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
})
)
.mutation(async ({ input }) => {
// データベースを更新
const updated = await db.user.update({
where: { id: input.id },
data: {
name: input.name,
email: input.email,
},
});
return updated;
}),
});
更新処理の atom
クライアント側で、更新処理を実行する atom を作成します。
typescript// atoms/userMutation.ts
import { atom } from 'jotai';
import { trpc } from '@/utils/trpc';
import type { User } from '@/types/user';
// ユーザー更新処理の atom
export const updateUserAtom = atom(
null,
async (
get,
set,
update: Partial<User> & { id: string }
) => {
try {
// tRPC mutation を実行
const updated = await trpc.user.update.mutate({
id: update.id,
name: update.name,
email: update.email,
});
// キャッシュを更新
set(userAtomFamily(update.id), updated);
return { success: true, data: updated };
} catch (error) {
return { success: false, error };
}
}
);
この atom は書き込み専用で、実行するとサーバーへの更新リクエストが送信されます。
更新フォームコンポーネント
実際に更新フォームを作成してみましょう。
typescript// components/UserEditForm.tsx
import { useAtom, useSetAtom } from 'jotai';
import { useState } from 'react';
import { updateUserAtom } from '@/atoms/userMutation';
export function UserEditForm({
userId,
}: {
userId: string;
}) {
const user = useAtomValue(userAtomFamily(userId));
const updateUser = useSetAtom(updateUserAtom);
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
const [loading, setLoading] = useState(false);
return (
<form onSubmit={handleSubmit}>
{/* フォーム内容は次のブロックで */}
</form>
);
}
送信処理の実装
フォームの送信処理を実装します。
typescript// components/UserEditForm.tsx(続き)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
// atom を通じて更新処理を実行
const result = await updateUser({
id: userId,
name,
email,
});
setLoading(false);
if (result.success) {
alert('更新が完了しました');
} else {
alert('エラーが発生しました');
}
};
フォーム UI
最後に、フォームの UI 部分を完成させます。
typescript// components/UserEditForm.tsx(フォーム部分)
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor='name'>名前</label>
<input
id='name'
type='text'
value={name}
onChange={(e) => setName(e.target.value)}
disabled={loading}
/>
</div>
<div>
<label htmlFor='email'>メールアドレス</label>
<input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
</div>
<button type='submit' disabled={loading}>
{loading ? '更新中...' : '更新する'}
</button>
</form>
);
エラーハンドリングとローディング状態
実践的なアプリケーションでは、エラーとローディング状態の管理が重要です。
エラー状態を含む atom
エラーとローディング状態を含めた atom を作成します。
typescript// atoms/userWithState.ts
import { atom } from 'jotai';
import { trpc } from '@/utils/trpc';
type AsyncState<T> = {
data: T | null;
loading: boolean;
error: Error | null;
};
// 状態付きユーザー取得 atom
export const userWithStateAtom = atomFamily(
(userId: string) =>
atom<AsyncState<User>>(async (get) => {
try {
const user = await trpc.user.getById.query({
id: userId,
});
return {
data: user,
loading: false,
error: null,
};
} catch (error) {
return {
data: null,
loading: false,
error: error as Error,
};
}
})
);
エラー表示コンポーネント
エラー状態を考慮したコンポーネントを作成します。
typescript// components/UserProfile.tsx
export function UserProfile({
userId,
}: {
userId: string;
}) {
const state = useAtomValue(userWithStateAtom(userId));
if (state.loading) {
return <div>読み込み中...</div>;
}
if (state.error) {
return (
<div>
<p>エラーが発生しました</p>
<code>{state.error.message}</code>
</div>
);
}
if (!state.data) {
return <div>ユーザーが見つかりません</div>;
}
return (
<div>
<h2>{state.data.name}</h2>
<p>{state.data.email}</p>
</div>
);
}
楽観的更新の実装
UI の応答性を高めるため、楽観的更新を実装してみましょう。
楽観的更新の atom
typescript// atoms/optimisticUpdate.ts
import { atom } from 'jotai';
import { trpc } from '@/utils/trpc';
// 楽観的更新を行う atom
export const optimisticUpdateUserAtom = atom(
null,
async (
get,
set,
update: Partial<User> & { id: string }
) => {
// 現在の値を保存(ロールバック用)
const previous = get(userAtomFamily(update.id));
// 即座に UI を更新(楽観的更新)
set(userAtomFamily(update.id), {
...previous,
...update,
});
try {
// サーバーに更新リクエストを送信
const result = await trpc.user.update.mutate(update);
// 成功したら、サーバーからの値で上書き
set(userAtomFamily(update.id), result);
return { success: true };
} catch (error) {
// 失敗したら元に戻す(ロールバック)
set(userAtomFamily(update.id), previous);
return { success: false, error };
}
}
);
楽観的更新により、ユーザーは即座にフィードバックを得られ、より快適な操作感が実現できますよ。
デバッグとエラーコード対応
開発中によく遭遇するエラーと、その解決方法をご紹介します。
エラー 1: TypeError: Cannot read property 'user' of undefined
発生条件: tRPC クライアントが初期化される前に atom が実行された場合に発生します。
解決方法:
typescript// atoms/user.ts(修正版)
export const userAtomFamily = atomFamily((userId: string) =>
atom(async (get) => {
// クライアントの存在チェックを追加
const context = trpc.useContext();
if (!context) {
throw new Error('tRPC context not initialized');
}
const user = await context.user.getById.fetch({
id: userId,
});
return user;
})
);
エラー 2: Error 400: Bad Request - Invalid input
エラーコード: HTTP 400 エラーメッセージ:
luaTRPCClientError: Invalid input: Expected string, received number
発生条件: Zod スキーマと実際の入力値の型が一致していない場合に発生します。
解決方法:
| # | 手順 | 詳細 |
|---|---|---|
| 1 | スキーマの確認 | サーバー側の input() で定義した Zod スキーマを確認 |
| 2 | 型の変換 | クライアント側で適切な型に変換してから送信 |
| 3 | TypeScript の活用 | 型エラーが出ていないか確認 |
修正例:
typescript// 誤り:数値を文字列として送信していない
await trpc.user.getById.query({ id: 123 });
// 正しい:文字列に変換
await trpc.user.getById.query({ id: String(123) });
まとめ
Jotai と tRPC を組み合わせることで、サーバーからクライアントまで一貫した型安全性を保ちながら、柔軟で効率的な状態管理が実現できました。
本記事でご紹介した内容を振り返ってみましょう。
| # | ポイント | 効果 |
|---|---|---|
| 1 | tRPC の Router 型を活用 | エンドツーエンドの型安全性 |
| 2 | Jotai の atom で tRPC を呼び出し | 柔軟な状態管理と再利用性 |
| 3 | atomFamily によるキャッシュ | 効率的なデータ管理 |
| 4 | 楽観的更新の実装 | 優れた UX の実現 |
初期配線さえ整えてしまえば、あとは atom を追加していくだけで機能を拡張できます。型推論が強力に働くため、リファクタリングも安心して行えるのが魅力ですね。
特に、サーバー側で型を変更すると、クライアント側で即座に型エラーが検出されるため、バグを早期に発見できます。これにより、開発速度と品質の両方が向上するのです。
ぜひ、あなたのプロジェクトでも Jotai と tRPC の組み合わせを試してみてください。最初は設定が少し複雑に感じるかもしれませんが、一度環境を整えてしまえば、驚くほど快適な開発体験が待っていますよ。
関連リンク
articleJotai × tRPC 初期配線:型安全 RPC とローカル状態の統合
articleJotai エコシステム最前線:公式&コミュニティ拡張の地図と選び方
articleJotai 可観測性:ログ/トレース/メトリクスで状態異常を見つける
articleJotai の永続化戦略比較:atomWithStorage/Redux 風/カスタム Storage
articleJotai 非同期で Suspense が発火しない問題の切り分けガイド
articleJotai でフォームを分割統治:フィールド粒度の atom 設計と検証戦略
articleJotai × tRPC 初期配線:型安全 RPC とローカル状態の統合
articletRPC が型推論しない時の対処:as const・型循環・import サイクルの解消
articletRPC と GraphQL 徹底比較:設計自由度・型安全・オーバーフェッチの実態
articletRPC 使い方入門:Todo API を 50 行で作るフルスタック体験
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articletRPC チートシート:Router/Procedure/ctx/useQuery/useMutation 早見表
articleElectron IPC 設計チートシート:チャネル命名・型安全・エラーハンドリング定型
articleDocker セキュアイメージ設計:非 root・最小ベース・Capabilities 削減の実装指針
articleJotai × tRPC 初期配線:型安全 RPC とローカル状態の統合
articleDevin による段階的リファクタリング設計:ストラングラーパターン適用ガイド
articleJest のカバレッジが 0% になる原因と対処:sourceMap/babel 設定の落とし穴
articleGitHub Copilot で機密文字列を誤提案しないための緊急対策:ポリシーと検知ルール
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来