- Vue.js
Vue provide/injectの使い方とInjectionKeyの型
provide/inject とは、祖先コンポーネントが渡した値を、途中のコンポーネントを経由せずに子孫コンポーネントが直接受け取れる Vue の仕組みです。 祖先で provide(キー, 値) を呼び、子孫で inject(キー) を呼びます。深い階層へ props を何段も受け渡す「バケツリレー」を、中継なしの形に置き換えられます。
この記事では、バケツリレーを provide/inject に書き換える前後のコードを並べ、props・provide/inject・Pinia の使い分けを比較表にしました。ref をそのまま渡した場合と .value を渡した場合の違い、inject が undefined になるときの警告文と型エラーは、Vue 3.5.43 で動かした出力をそのまま載せています。型を付ける InjectionKey、未提供のときに例外を出す専用の use 関数、readonly、app.provide まで、コピーして使える形でまとめます。
provide/inject の早見表
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 値を渡す | provide('theme', 'dark') |
祖先の setup(script setup のトップレベル)で呼ぶ |
| 値を受け取る | const theme = inject('theme') |
子孫の setup で呼ぶ。文字列キーの型は unknown |
| リアクティブに渡す | provide('count', count) |
ref をそのまま渡す。count.value を渡すとコピーになる |
| 型を付ける | const key: InjectionKey<T> = Symbol() |
provide と inject の両方に型が効く |
| 既定値を付ける | inject('theme', 'light') |
未提供でも警告が出ず、型から undefined が外れる |
| 既定値を関数で作る | inject('options', () => ({ ... }), true) |
第3引数 true でファクトリー関数として扱う |
| 未提供を早めに知らせる | 専用の use 関数で throw |
戻り値の型から undefined を外せる |
| 子孫からの書き換えを防ぐ | provide(key, readonly(state)) |
更新用の関数を一緒に渡す |
| アプリ全体に渡す | app.provide('apiBase', '/api/v1') |
すべてのコンポーネントで inject できる |
| setup の外で受け取る | app.runWithContext(() => inject(key)) |
アプリ全体に provide した値が対象 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue provide/injectの使い方とInjectionKeyの型」
この記事の検証環境
| 項目 | 版 |
|---|---|
| vue / @vue/compiler-sfc | 3.5.43 |
| pinia | 4.0.3 |
| @vue/test-utils / jsdom | 2.5.1 / 30.1.1 |
| TypeScript / vue-tsc | 5.9.3 / 3.3.12 |
| Node.js | 22.22.2 |
SFC(.vue ファイル)は @vue/compiler-sfc でコンパイルし、Node.js 上の jsdom にマウントして確かめました。ブラウザと Vite の開発サーバーでの表示、Vue Devtools での表示は未実行です。
props のバケツリレーを provide/inject に書き換える
画面の一番上が持っているユーザー情報を、3階層下のバッジで表示し、名前の変更もできる例です。
flowchart TD
App["App(user を持つ)"] --> Layout["Layout(中継だけ)"]
Layout --> Sidebar["Sidebar(中継だけ)"]
Sidebar --> Badge["Badge(user を表示・変更)"]
App -. "provide / inject なら直接届く" .-> Badge
書き換える前:4つのファイルで props とイベントを中継する
<!-- BeforeApp.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import BeforeLayout from './BeforeLayout.vue'
const user = ref({ name: '佐藤', role: '管理者' })
function rename(name: string) { user.value.name = name }
</script>
<template>
<BeforeLayout :user="user" @rename="rename" />
</template>
<!-- BeforeLayout.vue -->
<script setup lang="ts">
import BeforeSidebar from './BeforeSidebar.vue'
// 自分では使わないのに、下へ渡すためだけに受け取っている
defineProps<{ user: { name: string; role: string } }>()
defineEmits<{ rename: [name: string] }>()
</script>
<template>
<main><BeforeSidebar :user="user" @rename="$emit('rename', $event)" /></main>
</template>
<!-- BeforeSidebar.vue -->
<script setup lang="ts">
import BeforeBadge from './BeforeBadge.vue'
// ここも中継だけ
defineProps<{ user: { name: string; role: string } }>()
defineEmits<{ rename: [name: string] }>()
</script>
<template>
<aside><BeforeBadge :user="user" @rename="$emit('rename', $event)" /></aside>
</template>
<!-- BeforeBadge.vue -->
<script setup lang="ts">
defineProps<{ user: { name: string; role: string } }>()
defineEmits<{ rename: [name: string] }>()
</script>
<template>
<button @click="$emit('rename', '鈴木')">{{ user.name }}({{ user.role }})</button>
</template>
マウント直後と、ボタンをクリックした後の HTML です。
<main> <aside><button>佐藤(管理者)</button></aside> </main>
<main> <aside><button>鈴木(管理者)</button></aside> </main>
動作は正しいものの、Layout と Sidebar は自分では使わない props とイベントを宣言しています。user に項目を足すたびに、中継の2ファイルも直すことになります。
書き換えた後:キーと use 関数を1ファイルにまとめる
provide する側と inject する側の約束事を、1つの TypeScript ファイルに集めます。このファイルがこの記事のテンプレートです。
// userContext.ts
import { inject, provide, readonly, ref, type DeepReadonly, type InjectionKey, type Ref } from 'vue'
export interface User { name: string; role: string }
export interface UserContext {
user: DeepReadonly<Ref<User>> // 読み取り専用で渡す
rename: (name: string) => void // 更新は関数経由だけ
}
// キーは Symbol。InjectionKey<T> で provide と inject の型がそろう
export const userKey: InjectionKey<UserContext> = Symbol('user')
// 渡す側(祖先コンポーネントの setup で呼ぶ)
export function provideUser(initial: User): UserContext {
const user = ref<User>(initial)
const rename = (name: string) => { user.value.name = name }
const context: UserContext = { user: readonly(user), rename }
provide(userKey, context)
return context
}
// 受け取る側(子孫コンポーネントの setup で呼ぶ)
export function useUser(): UserContext {
const context = inject(userKey, null) // 既定値 null を渡すと「not found」の警告を出さずに自分で判定できる
if (!context) {
throw new Error('useUser() は provideUser() を呼んだコンポーネントの子孫で使ってください')
}
return context // ここから先は undefined を含まない型
}
<!-- AfterApp.vue -->
<script setup lang="ts">
import AfterLayout from './AfterLayout.vue'
import { provideUser } from './userContext'
provideUser({ name: '佐藤', role: '管理者' })
</script>
<template>
<AfterLayout />
</template>
<!-- AfterLayout.vue:props もイベントもなくなった -->
<script setup lang="ts">
import AfterSidebar from './AfterSidebar.vue'
</script>
<template>
<main><AfterSidebar /></main>
</template>
<!-- AfterSidebar.vue:こちらも中継なし -->
<script setup lang="ts">
import AfterBadge from './AfterBadge.vue'
</script>
<template>
<aside><AfterBadge /></aside>
</template>
<!-- AfterBadge.vue -->
<script setup lang="ts">
import { useUser } from './userContext'
const { user, rename } = useUser()
function overwrite() {
// readonly なので書き込みは反映されず、開発時は警告が出る
// @ts-expect-error 型でも止まる(Cannot assign to 'name' because it is a read-only property.)
user.value.name = '直接書き換え'
}
</script>
<template>
<button class="rename" @click="rename('鈴木')">{{ user.name }}({{ user.role }})</button>
<button class="overwrite" @click="overwrite">直接書き換える</button>
</template>
マウント直後、名前変更ボタンのクリック後、直接書き換えボタンのクリック後の順の実行結果です。
<main> <aside><button class="rename">佐藤(管理者)</button><button class="overwrite">直接書き換える</button></aside> </main>
<main> <aside><button class="rename">鈴木(管理者)</button><button class="overwrite">直接書き換える</button></aside> </main>
-- 直接書き換えを試す --
[Vue warn] Set operation on key "name" failed: target is readonly. { name: '鈴木', role: '管理者' }
<main> <aside><button class="rename">鈴木(管理者)</button><button class="overwrite">直接書き換える</button></aside> </main>
| 比べる点 | 書き換える前(props) | 書き換えた後(provide/inject) |
|---|---|---|
| 中継コンポーネントの宣言 | defineProps と defineEmits が2ファイルに必要 |
不要 |
| 項目を足すときに直す場所 | 4ファイル | userContext.ts と使う側 |
| 値の更新 | イベントを3段さかのぼる | rename() を呼ぶだけ |
| 子孫からの直接の書き換え | props は読み取り専用 | readonly で渡せば同じく防げる(警告が出て値は変わらない) |
props・provide/inject・Pinia の使い分け比較表
| 観点 | props / emit | provide / inject | Pinia |
|---|---|---|---|
| 届く範囲 | 直接の親子 | provide したコンポーネントの子孫 | アプリ全体(コンポーネントの外からも使える) |
| 状態の寿命 | 親コンポーネントと同じ | provide したコンポーネントと同じ(破棄されると消える) | アプリが動いている間 |
| 同じ画面に複数の独立した状態 | 置ける | 置ける(provide した場所ごとに別の値) | ストアは基本1つ(id ごと) |
| 型安全性 | defineProps の型で親子とも検査される |
InjectionKey を使えば付く。未提供の可能性は undefined として型に出る |
ストアの定義から推論される |
| データの流れの追いやすさ | テンプレートを見ればわかる | キーや use 関数を検索して追う | ストアを開けば一か所にまとまっている |
| デバッグのしやすさ | 親のテンプレートをたどる | provide した場所を探す必要がある。use 関数に集約すると探しやすい | Vue Devtools 向けの連携機能がある(今回は未実行) |
| 追加の導入 | なし | なし | pinia を追加し app.use(createPinia()) |
選び方の目安です。
| 状況 | 向いている方法 |
|---|---|
| 親から1〜2階層下へ渡す | props / emit |
| 3階層以上の中継が続き、途中のコンポーネントは値を使わない | provide / inject |
| タブ・フォーム・表など、部品の内側だけで共有したい状態 | provide / inject |
| 同じ部品を1画面に複数置き、それぞれ別の状態を持たせたい | provide / inject |
| ログイン中のユーザー、カートなど、離れた画面どうしで共有する状態 | Pinia |
| ルーターの処理など、コンポーネントの外からも読み書きしたい | Pinia |
参考として、Pinia で「離れた2つのコンポーネントが同じ状態を使う」最小の例も動かしました。
import { computed, h, ref } from 'vue'
import { createPinia, defineStore } from 'pinia'
const useCartStore = defineStore('cart', () => {
const items = ref([])
const count = computed(() => items.value.length)
function add(name) { items.value.push(name) }
return { items, count, add }
})
const Header = { setup() { const cart = useCartStore(); return () => h('header', `カート: ${cart.count}件`) } }
const Product = { setup() { const cart = useCartStore(); return () => h('button', { onClick: () => cart.add('ノート') }, '追加') } }
const App = { render: () => h('div', [h(Header), h(Product)]) }
<div> <header>カート: 0件</header><button>追加</button> </div>
<div> <header>カート: 1件</header><button>追加</button> </div>
コンポーネントの外から: 1
最後の行は、コンポーネントの外で useCartStore(pinia).count を読んだ結果です。祖先・子孫の関係がなくても同じ状態に届くのが、provide/inject との違いです。
ref をそのまま渡す場合と .value を渡す場合の違い
provide/inject でつまずきやすいのが、リアクティブ性です。ref はそのまま渡します。
<!-- RefParent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import RefChild from './RefChild.vue'
const count = ref(0)
provide('countRef', count) // ref をそのまま渡す → リアクティブ
provide('countValue', count.value) // .value を渡す → その時点の数値 0 のコピー
</script>
<template>
<button class="inc" @click="count++">親: {{ count }}</button>
<RefChild />
</template>
<!-- RefChild.vue -->
<script setup lang="ts">
import { inject, type Ref } from 'vue'
const countRef = inject<Ref<number>>('countRef')!
const countValue = inject<number>('countValue')!
</script>
<template>
<p class="child">ref で受け取り: {{ countRef }} / .value で受け取り: {{ countValue }}</p>
</template>
マウント直後と、親のボタンを2回クリックした後の表示です。
親: 0 | ref で受け取り: 0 / .value で受け取り: 0
親: 2 | ref で受け取り: 2 / .value で受け取り: 0
.value で渡したほうは 0 のままです。provide は受け取った値をそのまま保管するだけなので、数値を渡せば数値のコピーが、ref を渡せば ref そのものが子孫に届きます。
| 渡すもの | 子孫での追従 | 補足 |
|---|---|---|
ref(そのまま) |
する | テンプレートでは自動で中身が表示される。script では .value で読む |
reactive のオブジェクト |
する | プロパティを読む形で使う |
computed |
する | 読み取り専用の派生値を渡すのに向く |
count.value や state.count |
しない | その時点の値のコピー |
ゲッター関数 () => props.count |
する | 子孫側で computed(getCount) などにして使う |
props の値を provide したいときも同じ考え方で、provide(key, props.count) ではなく、computed(() => props.count) か toRef(props, 'count') を渡します。
表の各行も動かして確かめました。reactive の値を1増やし、props の count を 1 から 7 に変える前後の、子孫での表示です。
reactive=0 computed=0 getter=1 toRef=1 props.countのコピー=1
reactive=1 computed=2 getter=7 toRef=7 props.countのコピー=1
provide('propVal', props.count) で渡した最後の値だけが 1 のまま残っています。
inject が undefined になるときの警告文と対処
実行時:injection not found の警告
祖先のどこにも provide がない状態で inject を呼んだ結果です。
<!-- Lonely.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { userKey } from './userContext'
const context = inject(userKey) // 祖先のどこにも provide がない
console.log('inject の戻り値:', context)
</script>
<template>
<p>{{ context?.user.value.name ?? '(未提供)' }}</p>
</template>
[Vue warn]: injection "Symbol(user)" not found.
at <Lonely ref="VTU_COMPONENT" >
at <VTUROOT>
inject の戻り値: undefined
<p>(未提供)</p>
警告に Symbol(user) と表示されているのは、キーを Symbol('user') と説明つきで作ったためです。Symbol には説明の文字列を付けておくと、どのキーが見つからないのかが警告から読み取れます。
よくある原因は次の4つです。
| 原因 | 確かめ方 | 直し方 |
|---|---|---|
| provide したコンポーネントが祖先にいない(兄弟や子にある) | コンポーネントの親子関係を確認 | 共通の祖先で provide する |
| キーが一致していない(文字列の綴り違い、Symbol を別々に作った) | 同じ定数を import しているか確認 | キーを1ファイルで export して共有する |
| 自分自身が provide した値を、同じコンポーネントで inject している | provide と inject が同じファイルにある | 同じコンポーネント内では変数をそのまま使う |
| setup の外(イベントハンドラや非同期処理の後)で inject している | 下の警告文が出る | setup のトップレベルで inject し、変数に持っておく |
4つ目の場合は、別の警告になります。
[Vue warn]: inject() can only be used inside setup() or functional components.
戻り値: undefined
型:undefined と unknown のエラー
型の面でも、未提供の可能性は表に出てきます。
<!-- TypeErr.vue -->
<script setup lang="ts">
import { inject, provide } from 'vue'
import { userKey } from './userContext'
const context = inject(userKey)
console.log(context.user.value.name) // (1) undefined かもしれない
const theme = inject('theme')
console.log(theme.toUpperCase()) // (2) 文字列キーは unknown
provide(userKey, { user: { name: '佐藤' } }) // (3) キーの型と合わない値
</script>
vue-tsc --noEmit の出力です。
TypeErr.vue(6,13): error TS18048: 'context' is possibly 'undefined'.
TypeErr.vue(9,13): error TS18046: 'theme' is of type 'unknown'.
TypeErr.vue(11,28): error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'Readonly<Ref<{ readonly name: string; readonly role: string; }, { readonly name: string; readonly role: string; }>>'.
| エラー | 意味 | 直し方 |
|---|---|---|
TS18048 possibly 'undefined' |
inject(key) の型は T | undefined |
既定値を渡す/use 関数で判定する/確実なら ! を付ける |
TS18046 is of type 'unknown' |
文字列キーには型の情報がない | InjectionKey を使う、または inject<string>('theme') と型引数を書く |
| TS2353(provide 側) | InjectionKey の型と違う値を渡した |
キーの型どおりの値を渡す |
3つ目のように、InjectionKey を使うと provide する側の間違いも型で見つかります。文字列キーに型引数を書く方法では、provide 側は検査されません。
undefined への対処3パターン
| 方法 | コード | 向いている場面 |
|---|---|---|
| 既定値を渡す | inject('theme', 'light') |
provide がなくても動いてほしい設定値 |
| use 関数で例外を出す | useUser()(前掲) |
provide が必須の部品。置き場所の誤りにすぐ気づける |
| 非 null アサーション | inject(key)! |
必ず provide されるとわかっている小さな範囲 |
既定値の実行結果です。theme と options は既定値が使われ、既定値のない locale だけ警告が出ています。
const theme = inject('theme', 'light') // 既定値
const options = inject('options', () => ({ dense: false }), true) // 第3引数 true:ファクトリー関数として扱う
const locale = inject<string>('locale')
[Vue warn]: injection "locale" not found.
at <DefaultChild ref="VTU_COMPONENT" >
at <VTUROOT>
<p>light / false / </p>
use 関数のほうは、provide のない場所で使うと次のように例外になります(出力から例外の行を抜粋)。
Error: useUser() は provideUser() を呼んだコンポーネントの子孫で使ってください
inject(userKey, null) と既定値に null を渡しているので、injection not found の警告は出ず、自分で用意したメッセージだけが表示されます。
InjectionKey と use 関数のテンプレートを読み解く
前掲の userContext.ts を、ほかの用途に使い回すときの要点です。
| 部品 | 役割 | 書き方 |
|---|---|---|
| 値の型 | 渡す内容を1つのインターフェースにまとめる | interface UserContext { ... } |
| キー | provide と inject をつなぐ。型も運ぶ | export const userKey: InjectionKey<UserContext> = Symbol('user') |
| provide 関数 | 状態を作り、readonly にして渡す |
provideUser(initial) |
| use 関数 | inject して、未提供なら例外にする | useUser() |
- キーは Symbol にすると、ほかのライブラリや別の部品が使う文字列キーと重なりません。
- 状態は
readonly()で包み、更新用の関数を一緒に渡すと、値を変える場所が provide 側の1か所にまとまります。子孫が直接書き込もうとすると、実行時はSet operation on key "name" failed: target is readonly.の警告が出て値は変わらず、型でも止まります。 - 使う側は
useUser()を呼ぶだけになり、キーやinjectを直接意識しなくて済みます。あとで中身を Pinia のストアに差し替える場合も、use 関数の中だけを直せば済みます。
近い祖先の provide が優先される
同じキーを複数の祖先が provide している場合は、近いほうが使われます。
const Leaf = { setup() { const t = inject('theme'); return () => h('span', t) } }
const Mid = { setup() { provide('theme', 'dark'); return () => h(Leaf) } }
const Root = { setup() { provide('theme', 'light'); return () => h('div', [h(Leaf), ' / ', h(Mid)]) } }
<div><span>light</span> / <span>dark</span></div>
Root 直下の Leaf は light、Mid の下の Leaf は dark を受け取りました。画面の一部だけテーマや設定を切り替える、といった使い方ができます。
app.provide:アプリ全体に渡す
createApp の戻り値に対して provide を呼ぶと、そのアプリのすべてのコンポーネントで inject できます。API のベース URL のような設定値や、プラグインが機能を配る場面に向いています。
import { createApp, h, inject } from 'vue'
const Child = { setup() { const api = inject('apiBase'); return () => h('p', `apiBase = ${api}`) } }
const app = createApp({ render: () => h(Child) })
app.provide('apiBase', '/api/v1')
app.mount(el)
// setup の外で受け取りたいとき
console.log('runWithContext:', app.runWithContext(() => inject('apiBase')))
<p>apiBase = /api/v1</p>
runWithContext: /api/v1
| 渡す場所 | 届く範囲 | 用途の例 |
|---|---|---|
app.provide() |
アプリ全体 | 設定値、プラグインの機能 |
コンポーネントの provide() |
そのコンポーネントの子孫 | 画面や部品の内側で共有する状態 |
app.provide() は app.mount() より前に呼びます。app.runWithContext() で受け取れるのは app.provide() した値で、特定のコンポーネントが provide した値は対象外です。
テストで provide を差し込む
use 関数で例外を出す形にすると、子孫コンポーネントを単体でマウントしたときにも例外になります。テストでは、@vue/test-utils の global.provide で代わりの値を渡せます。Symbol のキーは [userKey] と角かっこで書きます。
import { mount } from '@vue/test-utils'
import { readonly, ref } from 'vue'
import AfterBadge from './AfterBadge.vue'
import { userKey } from './userContext'
const calls = []
const user = ref({ name: 'テスト太郎', role: '閲覧者' })
const wrapper = mount(AfterBadge, {
global: {
provide: {
[userKey]: { user: readonly(user), rename: (name) => calls.push(name) },
},
},
})
console.log(wrapper.find('.rename').text())
await wrapper.find('.rename').trigger('click')
console.log('rename に渡された値:', calls)
テスト太郎(閲覧者)
rename に渡された値: [ '鈴木' ]
祖先コンポーネントを用意しなくても、バッジ単体の表示と、更新関数が呼ばれたことを確かめられます。props と同じように、差し替えやすい入口として使えるのが provide/inject の利点です。
Options API での書き方
Options API のコンポーネントでは、provide オプションと inject オプションで宣言します。this の値をリアクティブに渡したいときは、provide を関数にして computed で包みます。
import { computed } from 'vue'
const Kid = {
inject: ['message', 'count'],
template: '<p>{{ message }} / {{ count }}</p>',
}
const Parent = {
components: { Kid },
data: () => ({ count: 1 }),
provide() {
return { message: 'hello', count: computed(() => this.count) }
},
template: '<Kid />',
}
マウント直後と、親の count を 5 に変えた後の表示です。
<p>hello / 1</p>
<p>hello / 5</p>
よくある質問
Q. provide/inject は Options API でも使えますか?
A. 使えます。provide オプションと inject オプションで宣言します(前の節に動かした例があります)。Composition API で provide した値を Options API の inject で受け取る、という組み合わせもできます(この組み合わせは今回未実行です)。
Q. inject した値を子孫側で書き換えてもよいですか?
A. ref をそのまま渡していれば書き換えられますが、どこで値が変わったのかを追いにくくなります。readonly() で渡し、更新は provide 側が用意した関数(この記事の rename())を通す形にすると、変更の入口が1か所にまとまります。
Q. provide/inject があれば Pinia は不要ですか? A. 役割が違います。provide/inject は「あるコンポーネントの子孫」に範囲を絞って共有する仕組みで、状態はそのコンポーネントと一緒に消えます。Pinia はアプリ全体で使う状態を置く場所で、コンポーネントの外からも読み書きできます。部品の内側の状態は provide/inject、画面をまたぐ状態は Pinia、と分けると整理しやすくなります。
Q. 文字列キーと Symbol キーはどちらを使えばよいですか?
A. TypeScript を使うなら InjectionKey 型の Symbol が合います。provide と inject の両方に型が付き、キーの重なりも起きません。app.provide で配る設定値など、単純で数が少ないものは文字列キーでも扱いやすいです。
まとめ
- provide/inject は、祖先から子孫へ中継なしで値を渡す仕組み。途中のコンポーネントから props とイベントの宣言をなくせる
- リアクティブにしたい値は ref のまま渡す。
count.valueを渡すとその時点のコピーになり、今回の実行では親が 2 になっても子孫は 0 のままだった - provide がないと
injection "..." not foundの警告が出て、戻り値はundefined。型でもpossibly 'undefined'(TS18048)として現れる InjectionKeyの Symbol をキーにすると、provide 側・inject 側の両方で型が検査される。文字列キーの戻り値はunknown- 専用の use 関数で未提供のときに例外を出すと、戻り値の型から
undefinedが外れ、置き場所の誤りにもすぐ気づける - 状態は
readonly()で渡し、更新用の関数を添える。アプリ全体に配る値はapp.provide() - 1〜2階層は props、子孫に閉じた共有は provide/inject、画面をまたぐ状態は Pinia が目安
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue provide/injectの使い方とInjectionKeyの型」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。