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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?