- Vue.js
Piniaの使い方|Pinia 4対応・設計からテストまで実行結果つき
Piniaとは、複数のコンポーネントで共有する状態(ログイン情報やカートの中身など)を「ストア」にまとめて管理する、Vue公式の状態管理ライブラリです。 Vuexの後継として位置づけられ、2026年9月時点の最新版は Pinia 4.0.3(2026年8月12日公開)です。
この記事では、Option Store と Setup Store の2つの書き方を同じ機能で並べて比較し、どちらを選ぶかの判断基準を示します。さらに、分割代入でリアクティブが失われる現象をブラウザ画面の値で再現し、非同期action・永続化・ストア同士の参照を含むログイン/カートのストアを作って Chromium で操作した結果と、Vitest でのテスト実行結果をそのまま載せています。API の説明だけでなく、実際のアプリにそのまま持ち込める形を目指しました。
Piniaの早見表
| やりたいこと | 書き方 |
|---|---|
| Pinia を登録する | createApp(App).use(createPinia()) |
| ストアを定義する | export const useCartStore = defineStore('cart', ...) |
| ストアを使う | const cart = useCartStore() |
| 状態を読む・書く | cart.items / cart.count++(.value 不要) |
| 状態を分割代入する | const { items } = storeToRefs(cart) |
| action を分割代入する | const { add } = cart(そのままでよい) |
| 複数の値をまとめて変更 | cart.$patch({ a: 1, b: 2 }) / cart.$patch((s) => {...}) |
| 初期値に戻す | cart.$reset()(Setup Store は自作が必要) |
| 変更を監視する | cart.$subscribe((mutation, state) => {...}) |
| action の呼び出しを監視 | cart.$onAction(({ name, after }) => {...}) |
| 別のストアを使う | action / getter の中で useAuthStore() |
| リロード後も残す | pinia-plugin-persistedstate + { persist: true } |
| テストで使う | setActivePinia(createPinia()) / createTestingPinia() |
Option Store と Setup Store の対応は次のとおりです。
| 役割 | Option Store | Setup Store |
|---|---|---|
| 状態 | state: () => ({ count: 0 }) |
const count = ref(0) |
| 計算値 | getters: { double: (s) => s.count * 2 } |
const double = computed(...) |
| 処理 | actions: { increment() { this.count++ } } |
function increment() { count.value++ } |
$reset() |
自動で使える | 自分で定義する |
composable(useRoute 等)の利用 |
しにくい | そのまま使える |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Piniaの使い方|Pinia 4対応・設計からテストまで実行結果つき」
検証環境とインストール
| 項目 | バージョン |
|---|---|
| Node.js | 22.22.2 |
| vue / vite | 3.5.43 / 8.3.1 |
| pinia | 4.0.3 |
| @vue/devtools-api | 8.2.1 |
| pinia-plugin-persistedstate | 4.7.1 |
| vitest / @vue/test-utils / @pinia/testing | 5.0.2 / 2.5.1 / 2.0.1 |
| 画面確認 | Playwright 1.56.1 + Chromium |
npm i pinia @vue/devtools-api
Pinia 4 では @vue/devtools-api(v8)を一緒にインストールする必要があります(peerDependencies が vue ^3.5.11、typescript >=5.6.0、@vue/devtools-api ^8.1.5)。
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 永続化を使う場合
createApp(App).use(pinia).mount('#app')
Pinia 3 から 4 で変わったこと
Pinia 4.0.0 は2026年7月14日公開です。公式のリリースノートでは、破壊的変更は技術的なもの2点とされています。
| 項目 | Pinia 3 | Pinia 4 |
|---|---|---|
| 配布形式 | ESM と CommonJS | ESM のみ |
| devtools 連携 | @vue/devtools-api v7(自動で入る) |
v8、自分でインストール |
| エラー・警告 | 従来の実装 | 診断(Nostics)ベースに刷新 |
storeToRefs |
― | nullish の値を適切にスキップ |
| SSR | ― | skipHydrate の扱い改善、Set/Map の状態の扱い改善(4.0.3) |
| TypeScript(peer) | >=4.5.0 |
>=5.6.0 |
@pinia/testing |
1.x | 2.x(pinia >=4.0.2 が必要) |
| ストアの書き方 | ― | 変更なし(この記事のコードは3でも同じ) |
defineStore や storeToRefs などのAPIの使い方は変わっていないので、アップグレードの作業は主に依存パッケージの更新です。ちなみに Pinia 3 では Vue 2 のサポートが終了しています。
Option Store と Setup Store を書き比べる
同じカウンターを2つの書き方で作ります。
// src/stores/counter-option.js
import { defineStore } from 'pinia'
// Option Store:state / getters / actions をオブジェクトで書く
export const useCounterOptionStore = defineStore('counter-option', {
state: () => ({ count: 0, label: '在庫数' }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
// src/stores/counter-setup.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Setup Store:ref = state、computed = getters、function = actions
export const useCounterSetupStore = defineStore('counter-setup', () => {
const count = ref(0)
const label = ref('在庫数')
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
// Setup Store では $reset を自分で用意する
function $reset() {
count.value = 0
label.value = '在庫数'
}
// state はすべて return する(返さないと devtools・SSR・プラグインから見えない)
return { count, label, double, increment, $reset }
})
どちらを選ぶかの判断表
| こんなとき | おすすめ | 理由 |
|---|---|---|
| Vuex から移行する、Options API に慣れている | Option Store | 構造が Vuex とほぼ同じ |
$reset() をよく使う |
Option Store | 自動で用意される |
コンポーネントを <script setup> で書いている |
Setup Store | 同じ書き方で統一できる |
ストアの中で useRoute() や composable を使う |
Setup Store | 関数の中で自由に呼べる |
watch をストア内で使う |
Setup Store | そのまま書ける |
| チームで書き方をそろえたい | どちらか一方に統一 | 混在も動くが読みにくくなる |
新規で <script setup> を使うプロジェクトなら Setup Store に統一すると、コンポーネントとストアで同じ知識が使えます。Setup Store では state にしたい ref をすべて return することと、$reset を自作することの2点だけ覚えておきましょう。
storeToRefs:分割代入でリアクティブが失われる罠
ストアをそのまま分割代入すると、その時点の値がコピーされるだけで、あとから変わっても画面は更新されません。 実際に画面で確かめます。
<!-- src/App.vue(抜粋) -->
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterSetupStore } from './stores/counter-setup'
const setup = useCounterSetupStore()
// NG:そのまま分割代入すると、その時点の値のコピーになる
const { count: countCopy } = useCounterSetupStore()
// OK:storeToRefs で state と getters を ref として取り出す
const { count, double } = storeToRefs(setup)
// action はそのまま分割代入してよい
const { increment } = setup
</script>
<template>
<p id="setup">Setup: store.count={{ setup.count }} / 分割代入={{ countCopy }} / storeToRefs={{ count }} (double={{ double }})</p>
<button id="setup-inc" @click="increment">Setup +1</button>
<button id="setup-reset" @click="setup.$reset()">$reset</button>
</template>
Chromium で「Setup +1」を3回押し、次に $reset を押した結果です。
初期表示
#setup => Setup: store.count=0 / 分割代入=0 / storeToRefs=0 (double=0)
Setup +1 を3回
#setup => Setup: store.count=3 / 分割代入=0 / storeToRefs=3 (double=6)
Setup $reset(自作)
#setup => Setup: store.count=0 / 分割代入=0 / storeToRefs=0 (double=0)
分割代入した countCopy だけが 0 のままで、storeToRefs で取り出した count と getter の double はストアに追従しました。
| 取り出し方 | state・getters | actions |
|---|---|---|
const { count } = store |
追従しない(コピー) | 使える |
const { count } = storeToRefs(store) |
追従する(ref) | 取り出されない |
store.count と都度書く |
追従する | 使える |
$patch・$reset・$subscribe
const opt = useCounterOptionStore()
opt.increment() // action
opt.$patch({ count: 100, label: 'まとめて変更' }) // オブジェクトで複数変更
opt.$patch((state) => { state.count += 10 }) // 関数で変更(配列の push などに便利)
opt.$reset() // 初期値に戻す
Option Store のボタンを順に押したときの画面の値です。
Option +1 を2回 #opt => Option: 在庫数 2 / double=4
$patch(オブジェクト) #opt => Option: まとめて変更 100 / double=200
$patch(関数) #opt => Option: まとめて変更 110 / double=220
$reset #opt => Option: 在庫数 0 / double=0
Setup Store で $reset を定義せずに呼ぶと、例外になります。 Vitest で確かめたエラー文です。
// tests/reset.test.js
import { it, expect } from 'vitest'
import { setActivePinia, createPinia, defineStore } from 'pinia'
import { ref } from 'vue'
it('$reset を定義していない Setup Store で $reset() を呼ぶ', () => {
setActivePinia(createPinia())
const useNoReset = defineStore('no-reset', () => {
const count = ref(0)
return { count }
})
const store = useNoReset()
store.count = 5
expect(() => store.$reset()).toThrow()
try { store.$reset() } catch (e) { console.log('エラー:', e.message) }
})
stdout | tests/reset.test.js > $reset を定義していない Setup Store で $reset() を呼ぶ
エラー: 🍍: Store "no-reset" is built using the setup syntax and does not implement $reset().
$subscribe は state の変更を監視し、mutation.type で変更の種類がわかります。 flush: 'sync' を付けて変更の直後に記録しました。
const types = []
store.$subscribe((mutation) => types.push(mutation.type), { flush: 'sync' })
store.count++
store.$patch({ count: 10 })
store.$patch((s) => { s.count++ })
console.log(types)
[ 'direct', 'patch object', 'patch function' ]
$subscribe は既定ではコンポーネントが破棄されると解除されます。ストアに watch を付けるのに近い使い方で、変更内容をログやサーバーへ送る処理に向いています。
実践:ログインとカートのストア設計
ここからは、実際のアプリで多い 「ログイン情報」と「カート」 のストアを作ります。ストアは機能(ドメイン)ごとに分け、必要なときに別のストアを呼び出すのが基本の設計です。
| ストア | 持つ状態 | 役割 |
|---|---|---|
auth |
user、isLoggedIn |
ログイン・ログアウト、リロード後も保持 |
cart |
items、catalog、loading、error |
商品取得、追加、合計計算 |
ログイン情報(永続化つき)
// src/stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore(
'auth',
() => {
const user = ref(null) // { name, role }
const isLoggedIn = computed(() => user.value !== null)
function login(name) {
user.value = { name, role: 'member' }
}
function logout() {
user.value = null
}
return { user, isLoggedIn, login, logout }
},
// pinia-plugin-persistedstate:localStorage に保存(リロードしても残る)
{ persist: true },
)
npm i pinia-plugin-persistedstate
Setup Store では、第3引数のオプションに persist: true を書きます(Option Store では state などと同じオブジェクトに書きます)。既定では localStorage に、ストアのIDをキーとして state 全体が保存されます。persist: { pick: ['user'] } で保存する項目を絞れます。
pinia-plugin-persistedstate 4.7.1 の peerDependencies は pinia >=3.0.0 で、Pinia 4.0.3 と組み合わせて動作することを下の実行結果で確認しました。トークンなどの秘密の値は localStorage に保存しない設計にしておくと安心です。
カート(非同期action・ストア同士の参照)
// src/stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAuthStore } from './auth'
export const useCartStore = defineStore('cart', () => {
const items = ref([]) // { id, name, price, qty }
const loading = ref(false)
const error = ref(null)
const catalog = ref([])
const totalQty = computed(() => items.value.reduce((n, i) => n + i.qty, 0))
const totalPrice = computed(() => items.value.reduce((n, i) => n + i.price * i.qty, 0))
// 非同期 action:ローディングとエラーを state で持つ
async function fetchCatalog(url = '/items.json') {
loading.value = true
error.value = null
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
catalog.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
function add(product) {
// 別のストアは action / getter の中で呼び出して使う
const auth = useAuthStore()
if (!auth.isLoggedIn) {
error.value = 'ログインしてください'
return false
}
const found = items.value.find((i) => i.id === product.id)
if (found) found.qty++
else items.value.push({ ...product, qty: 1 })
return true
}
function clear() {
items.value = []
}
return { items, loading, error, catalog, totalQty, totalPrice, fetchCatalog, add, clear }
})
public/items.json には [{ "id": 1, "name": "ボールペン", "price": 120 }, { "id": 2, "name": "ノート", "price": 200 }, { "id": 3, "name": "ホチキス", "price": 650 }] を置きました。
別のストアは、ファイルの先頭ではなく action や getter の中で useAuthStore() を呼ぶのがポイントです。こうするとストア同士が互いを参照していても、呼び出し順の問題が起きにくくなります。
<!-- src/App.vue(カート部分の抜粋) -->
<script setup>
import { storeToRefs } from 'pinia'
import { useAuthStore } from './stores/auth'
import { useCartStore } from './stores/cart'
const auth = useAuthStore()
const cart = useCartStore()
const { items, loading, error, catalog, totalQty, totalPrice } = storeToRefs(cart)
cart.$subscribe((mutation, state) => {
console.log(`[subscribe] type=${mutation.type} 件数=${state.items.length}`)
})
</script>
<template>
<p id="auth">ログイン: {{ auth.isLoggedIn ? auth.user.name : '未ログイン' }}</p>
<button id="login" @click="auth.login('佐藤')">ログイン</button>
<button id="fetch" @click="cart.fetchCatalog()">商品を取得</button>
<button id="fetch-ng" @click="cart.fetchCatalog('/nothing.json')">存在しないURLで取得</button>
<p id="status">loading={{ loading }} error={{ error }}</p>
<button v-for="p in catalog" :key="p.id" class="add" @click="cart.add(p)">{{ p.name }}を追加</button>
<p id="cart">カート: {{ totalQty }}点 / {{ totalPrice }}円 / {{ items.map((i) => `${i.name}x${i.qty}`).join(',') }}</p>
</template>
実行結果(開発サーバーで操作)
vite --port 7414 で起動し、Playwright でボタンを押して画面を読み取りました(ログイン後はボールペンを2回、ホチキスを1回追加)。
存在しないURLで取得
#status => loading=false error=Unexpected token '<', "<!doctype "... is not valid JSON
商品を取得
#status => loading=false error=
未ログインで追加
#status => loading=false error=ログインしてください
#cart => カート: 0点 / 0円 /
ログイン後に追加
#auth => ログイン: 佐藤
#cart => カート: 3点 / 890円 / ボールペンx2,ホチキスx1
localStorage.auth = {"user":{"name":"佐藤","role":"member"}}
リロード後
#auth => ログイン: 佐藤
#cart => カート: 0点 / 0円 /
読み取れるポイントです。
- 存在しないURLでも
HTTP 404ではなく JSON の解析エラーになりました。 Vite の開発サーバーは、見つからないパスにindex.html(200)を返すためです。本番のAPIでも起こりうるので、res.headers.get('content-type')を確認すると原因がわかりやすくなります cartからauthを参照し、未ログインでは追加を止められたpersist: trueのauthはリロード後も残り、付けていないcartは空に戻った
コンソールには $subscribe のログが [subscribe] type=direct 件数=1 のように出力されました。Setup Store では ref への代入がすべて direct として届きます。
Vuex との対応表
| Vuex 4 | Pinia |
|---|---|
createStore({ modules }) で1つの大きなストア |
defineStore で機能ごとに小さなストア |
state |
state(Setup Store では ref) |
getters |
getters(Setup Store では computed) |
mutations + actions |
actions だけ(直接変更もできる) |
commit('add', x) / dispatch('fetch') |
store.add(x) / await store.fetch() |
mapState / mapGetters |
storeToRefs(store) |
modules + namespaced |
ストアの ID(defineStore('cart', ...)) |
rootState / rootGetters |
action 内で別ストアを useXxxStore() |
| 文字列で型が付きにくい | 型推論がそのまま効く |
Pinia には mutations がなく、action の中でも state を直接変更できます。 devtools にも変更の履歴が表示されるため、Vuex の「mutation を通す」ルールがなくても変更を追えます。
Vitest でストアをテストする
npm i -D vitest @vue/test-utils @pinia/testing happy-dom
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: { environment: 'happy-dom' },
})
ストア単体のテスト(createPinia / setActivePinia)
// tests/cart.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '../src/stores/cart'
import { useAuthStore } from '../src/stores/auth'
describe('cart store', () => {
beforeEach(() => {
// テストごとに新しい Pinia を用意して、状態を持ち越さない
setActivePinia(createPinia())
})
it('未ログインでは追加できない', () => {
const cart = useCartStore()
expect(cart.add({ id: 1, name: 'ノート', price: 200 })).toBe(false)
expect(cart.error).toBe('ログインしてください')
})
it('ログイン後は数量と合計が増える', () => {
useAuthStore().login('佐藤')
const cart = useCartStore()
cart.add({ id: 1, name: 'ノート', price: 200 })
cart.add({ id: 1, name: 'ノート', price: 200 })
expect(cart.totalQty).toBe(2)
expect(cart.totalPrice).toBe(400)
})
it('fetchCatalog:失敗すると error に入り loading は戻る', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500 }))
const cart = useCartStore()
const p = cart.fetchCatalog()
expect(cart.loading).toBe(true)
await p
expect(cart.loading).toBe(false)
expect(cart.error).toBe('HTTP 500')
vi.unstubAllGlobals()
})
})
コンポーネントのテスト(createTestingPinia)
<!-- src/components/CartBadge.vue -->
<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/cart'
const cart = useCartStore()
const { totalQty } = storeToRefs(cart)
</script>
<template>
<span class="badge">{{ totalQty }}点</span>
<button @click="cart.clear()">空にする</button>
</template>
// tests/badge.test.js
import { it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CartBadge from '../src/components/CartBadge.vue'
import { useCartStore } from '../src/stores/cart'
it('createTestingPinia:初期状態を注入し、actionはスパイになる', async () => {
const wrapper = mount(CartBadge, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
initialState: { cart: { items: [{ id: 1, name: 'ノート', price: 200, qty: 3 }] } },
}),
],
},
})
expect(wrapper.text()).toContain('3点')
const cart = useCartStore()
await wrapper.find('button').trigger('click')
expect(cart.clear).toHaveBeenCalledTimes(1)
// 既定では action は実行されない(stubActions: true)ので中身は残る
expect(wrapper.text()).toContain('3点')
})
実行結果
RUN v5.0.2 .../pinia
✓ tests/badge.test.js > createTestingPinia:初期状態を注入し、actionはスパイになる 33ms
✓ tests/cart.test.js > cart store > 未ログインでは追加できない 13ms
✓ tests/cart.test.js > cart store > ログイン後は数量と合計が増える 2ms
✓ tests/cart.test.js > cart store > fetchCatalog:失敗すると error に入り loading は戻る 2ms
✓ tests/reset.test.js > $reset を定義していない Setup Store で $reset() を呼ぶ 15ms
Test Files 3 passed (3)
Tests 5 passed (5)
| テストの種類 | 使うもの | ポイント |
|---|---|---|
| ストアのロジック | setActivePinia(createPinia()) |
beforeEach で毎回作り直す |
| コンポーネント | createTestingPinia({ createSpy: vi.fn }) |
initialState で状態を注入、action はスパイ |
| action も実行したい | createTestingPinia({ stubActions: false }) |
本物の action が動く |
| fetch を含む action | vi.stubGlobal('fetch', ...) |
ネットワークに出ない |
createTestingPinia の initialState はストアのIDをキーに書きます(ここでは cart)。なお、$reset を自作した Setup Store を createTestingPinia で使ったところ、$reset はスパイになりつつ、値も初期値(0)に戻りました。
よくあるつまずきと直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| 分割代入した値が画面で変わらない | ストアを直接分割代入した | storeToRefs(store) |
storeToRefs で action が取れない |
action は ref ではない | action は store から直接分割代入 |
does not implement $reset() |
Setup Store で未定義 | $reset を自作して return |
| Setup Store の値が devtools や永続化に出ない | ref を return していない | state はすべて return |
getActivePinia() のエラー |
app.use(pinia) 前、またはテストで未設定 |
登録順を確認、テストは setActivePinia |
| リロードで状態が消える | Pinia はメモリ上だけ | pinia-plugin-persistedstate |
| 開発サーバーで fetch が JSON エラー | 存在しないパスに index.html が返る | URLとレスポンスの形式を確認 |
よくある質問
Q. ref や composable だけで状態を共有するのと何が違いますか? A. モジュールの外に ref を置けば共有はできますが、Pinia を使うと devtools での状態の確認、SSR での状態の受け渡し、プラグインによる永続化、テスト用の差し替えがそろって使えます。アプリ全体で使う状態は Pinia にまとめると管理しやすくなります。
Q. Option Store と Setup Store は混在してもよいですか? A. 動作上は問題なく、同じアプリで両方使えます。読みやすさのために、新しく作るストアはどちらかに統一するのがおすすめです。
Q. Pinia 3 から 4 に上げるとコードの修正は必要ですか?
A. ストアの書き方は変わっていません。@vue/devtools-api を依存に追加し、@pinia/testing を使っていれば 2.x に上げます。CommonJS で Pinia を読み込んでいる構成は ESM に移します。
Q. Nuxt でも同じ書き方ですか?
A. @pinia/nuxt モジュールを使うと、ストアの定義や storeToRefs の使い方は同じです。SSR でサーバーの状態がクライアントに引き継がれる点がNuxtならではの利点です。
まとめ
- Pinia 4.0.3 が最新。ESMのみ・
@vue/devtools-apiv8 の同時インストールが主な変更で、ストアの書き方は変わらない <script setup>のプロジェクトは Setup Store に統一すると書き方がそろう。state は全部 return、$resetは自作- 分割代入は
storeToRefs。直接分割代入すると 画面で 0 のままになることを確認 - 非同期 action は
loadingとerrorを state に持ち、finallyで戻す - 別のストアは action の中で呼び出す。永続化は
persist: trueで、リロード後も残ることを確認 - テストは
setActivePinia(createPinia())とcreateTestingPiniaを使い分ける
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Piniaの使い方|Pinia 4対応・設計からテストまで実行結果つき」
株式会社RJCでは、状態管理の設計やテストの書き方を、実際のコードを見せ合いながら仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。