Angular Signal Forms とは、フォームの値をシグナルで持ち、検証ルールを関数で宣言する Angular の新しいフォーム API で、Angular 22.0 で安定版になりました。 form(モデルのシグナル, スキーマ) でフォームを作り、テンプレートでは [formField] で入力欄とつなぐだけです。値・エラー・送信中などの状態はすべてシグナルなので、OnPush が既定になった v22 と自然になじみます。

この記事では、Angular 22.2.0 の型定義と公式ドキュメントで API を確認し、ログインフォームと会員登録フォーム(必須・メール形式・パスワード一致・カスタム検証・非同期の重複チェック・配列フィールド)をコピペで動く完成コード で紹介します。Playwright(Chromium)で入力 → エラー表示 → 送信まで操作した結果 も載せ、同じ会員登録フォームを Reactive Forms で書いた版との対応表、v21 実験版からの変更点 もまとめました。

Signal Forms API早見表(v22.2時点)

やりたいこと API 書き方の例
フォームを作る form(model, schema?, options?) f = form(signal({ email: '' }), p => { ... })
入力欄とつなぐ FormField ディレクティブ <input [formField]="f.email" />
<form> の送信を任せる FormRoot ディレクティブ <form [formRoot]="f">
必須 required(path, opts?) required(p.email, { message: '必須です' })
メール形式 email(path, opts?) email(p.email)
文字数・数値 minLength / maxLength / min / max minLength(p.password, 8)
日付の範囲 minDate / maxDate(v22 追加) maxDate(p.birthDate, new Date())
正規表現 pattern(path, regex) pattern(p.password, /[0-9]/)
カスタム検証 validate(path, fn) validate(p.confirm, ({ value, valueOf }) => ...)
非同期検証 validateAsync(path, { params, factory, onSuccess, onError }) 重複チェックなど
HTTP で非同期検証 validateHttp(path, { request, onSuccess, onError }) request: ({ value }) => '/api/check?x=' + value()
条件つきルール 各ルールの when オプション / applyWhen required(p.x, { when: ({ valueOf }) => valueOf(p.y) })
配列の各要素にルール applyEach(path, schema) applyEach(p.skills, item => required(item))
無効化・読み取り専用・非表示 disabled / readonly / hidden disabled(p.name, { when: ... })
反映タイミングを遅らせる debounce(path, ms | 'blur') debounce(p.password, 'blur')
送信処理 options.submission / submit(f, action) { submission: { action: async f => ... } }
値を読む・書く f.email().value() / .value.set() モデルのシグナルも同時に変わる
状態を読む valid() invalid() pending() touched() dirty() submitting() すべてシグナル
エラーを読む errors() / errorSummary() / getError(kind)(v22 追加) f.email().getError('required')
非同期検証をやり直す reloadValidation()(v22 追加) サーバー側の状態が変わったとき
初期状態に戻す reset(value?) f().reset({ email: '' })

import 元は @angular/forms/signals です(従来の Reactive Forms は @angular/forms)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular Signal Forms入門|v22安定版のAPI早見表と完成コード」

検証環境

Angular 22.2.0 / TypeScript 6.0.3 / Node.js 22.22.3 / Vitest 5.0.2 / Playwright 1.56.1(Chromium) で確認しました。型定義(@angular/forms/types/signals.d.ts)では、上の表の API に @publicApi 22.0 が付いています。v21 の型定義では同じ API が @experimental 21.0.0 などでした。

v21(実験版)から v22(安定版)への変更点

v21.0.0・v21.2.24・v22.2.0 の @angular/forms の型定義を比べ、公式ブログ・リリース解説と照らし合わせた結果です。

項目 v21.0(実験版) v21.2(実験版) v22(安定版)
状態 @experimental @experimental @publicApi 22.0(安定版)
入力欄とつなぐディレクティブ Field / [field] FormField / [formField] FormField / [formField]
<form> 用ディレクティブ なし FormRoot / [formRoot] FormRoot / [formRoot]
form() の送信オプション なし(submit() を呼ぶ) submission submission(action / onInvalid)
カスタムエラーの作り方 customError() オブジェクト { kind, message } オブジェクト { kind, message }
日付の検証 なし なし minDate() / maxDate()
特定のエラーを取り出す なし なし getError(kind)
非同期検証の再実行 なし なし reloadValidation()
非同期検証のデバウンス なし なし validateAsync / validateHttp の debounce
when オプション 一部のみ 一部のみ すべての検証・disabled / readonly / hidden に統一
markAsTouched() そのフィールドのみ そのフィールドのみ 子孫も含む({ skipDescendants: true } で従来どおり)
カスタムコントロールの touched touched を model で双方向 同左 touched 入力+touch() 出力
hidden() when 必須 同左 22.2 で when なしで常に非表示 にできる

v21.0 の記事やサンプルを見るときは、[field] が [formField] に、customError() がただのオブジェクトに変わっている点に注意 すると、そのまま v22 に読み替えられます。

ログインフォーム(完成コード)

メールとパスワードの必須・形式チェック、送信中表示、サーバーエラーの表示、エラー時のフォーカス移動まで入れた完成形です。

// src/app/forms/login-form.ts
import { Component, signal } from '@angular/core';
import { FormField, FormRoot, email, form, minLength, required } from '@angular/forms/signals';

interface LoginModel {
  email: string;
  password: string;
}

// 疑似API:demo@example.com / password123 だけ成功
function loginApi(v: LoginModel): Promise<boolean> {
  return new Promise((r) => setTimeout(() => r(v.email === 'demo@example.com' && v.password === 'password123'), 300));
}

@Component({
  selector: 'app-login-form',
  imports: [FormField, FormRoot],
  template: `
    <form id="login" [formRoot]="loginForm">
      <label>メール <input id="login-email" type="email" [formField]="loginForm.email" /></label>
      @if (loginForm.email().touched() && loginForm.email().invalid()) {
        @for (e of loginForm.email().errors(); track e.kind) {
          <p class="err err-email">{{ e.message }}</p>
        }
      }

      <label>パスワード <input id="login-password" type="password" [formField]="loginForm.password" /></label>
      @if (loginForm.password().touched() && loginForm.password().invalid()) {
        @for (e of loginForm.password().errors(); track e.kind) {
          <p class="err err-password">{{ e.message }}</p>
        }
      }

      @for (e of loginForm().errors(); track e.kind) {
        <p class="err err-form">{{ e.message }}</p>
      }

      <button id="login-submit" type="submit" [disabled]="loginForm().submitting()">
        {{ loginForm().submitting() ? '送信中…' : 'ログイン' }}
      </button>
      <p id="login-result">{{ result() }}</p>
    </form>
  `,
})
export class LoginForm {
  protected readonly result = signal('');

  // 1. モデル(フォームの値そのもの)はただのシグナル
  private readonly model = signal<LoginModel>({ email: '', password: '' });

  // 2. form(モデル, スキーマ, オプション)
  protected readonly loginForm = form(
    this.model,
    (p) => {
      required(p.email, { message: 'メールを入力してください' });
      email(p.email, { message: 'メールの形式で入力してください' });
      required(p.password, { message: 'パスワードを入力してください' });
      minLength(p.password, 8, { message: 'パスワードは8文字以上です' });
    },
    {
      submission: {
        // 検証がすべて通ったときだけ呼ばれる
        action: async (f) => {
          const ok = await loginApi(f().value());
          if (ok) {
            this.result.set(`ようこそ ${f.email().value()} さん`);
            return;
          }
          // 返したエラーはフォーム(ルート)のエラーになる
          return { kind: 'loginFailed', message: 'メールまたはパスワードが違います' };
        },
        onInvalid: (f) => {
          this.result.set('入力内容を確認してください');
          f().errorSummary()[0]?.fieldTree().focusBoundControl();
        },
      },
    },
  );
}

実行結果(Playwright)

操作 画面に出たエラー・結果
何も入れずに「ログイン」 メールを入力してください / パスワードを入力してください、結果欄 入力内容を確認してください、フォーカスはメール欄へ
demo / short で送信 メールの形式で入力してください / パスワードは8文字以上です
正しい形式・誤ったパスワードで送信(直後) ボタンが 送信中… になり disabled
〃(0.5秒後) メールまたはパスワードが違います
パスワードを入れ直す フォームのエラーが消える
demo@example.com / password123 で送信 エラーなし、ようこそ demo@example.com さん

DOM も確認したところ、[formRoot] を付けた <form> には novalidate が、required() を付けた入力欄には required 属性が自動で付き、入力欄の name 属性は ng.form0.email になっていました。

[formRoot] は送信時のページ遷移を止めて submission.action を呼び、検証エラーがあれば代わりに onInvalid を呼びます。 送信すると全フィールドが touched になるので、「touched のときだけエラー表示」にすると入力前は静かで、送信時に一斉に出る自然な動きになります。action から返したエラーはフォーム全体のエラーになり、fieldTree: f.email を付ければ特定の項目のエラーにできます。

会員登録フォーム(完成コード)

必須・メール形式・非同期の重複チェック・パスワードの強さ(カスタム検証)・パスワード一致(項目をまたぐ検証)・配列フィールド・同意チェックを入れた完成形です。

// src/app/forms/register-form.ts
import { Component, Signal, resource, signal } from '@angular/core';
import {
  FormField, FormRoot, applyEach, debounce, email, form, maxLength,
  minLength, pattern, required, validate, validateAsync,
} from '@angular/forms/signals';

interface RegisterModel {
  name: string;
  email: string;
  password: string;
  confirm: string;
  skills: string[];
  agree: boolean;
}

// 疑似API:登録済みメールかどうかを 500ms 後に返す
const TAKEN = ['taken@example.com', 'admin@example.com'];
function isEmailTaken(mail: string): Promise<boolean> {
  return new Promise((r) => setTimeout(() => r(TAKEN.includes(mail.toLowerCase())), 500));
}

@Component({
  selector: 'app-register-form',
  imports: [FormField, FormRoot],
  template: `
    <form id="register" [formRoot]="f">
      <label>お名前 <input id="reg-name" [formField]="f.name" /></label>
      @if (f.name().touched()) { @for (e of f.name().errors(); track e.kind) { <p class="err err-name">{{ e.message }}</p> } }

      <label>メール <input id="reg-email" type="email" [formField]="f.email" /></label>
      @if (f.email().pending()) { <p class="pending">確認中…</p> }
      @if (f.email().touched()) { @for (e of f.email().errors(); track e.kind) { <p class="err err-email">{{ e.message }}</p> } }

      <label>パスワード <input id="reg-password" type="password" [formField]="f.password" /></label>
      @if (f.password().touched()) { @for (e of f.password().errors(); track e.kind) { <p class="err err-password">{{ e.message }}</p> } }

      <label>確認用 <input id="reg-confirm" type="password" [formField]="f.confirm" /></label>
      @if (f.confirm().touched()) { @for (e of f.confirm().errors(); track e.kind) { <p class="err err-confirm">{{ e.message }}</p> } }

      <fieldset>
        <legend>得意なこと(1〜3件)</legend>
        @for (s of f.skills; track $index) {
          <div>
            <input class="reg-skill" [formField]="s" />
            <button type="button" class="skill-remove" (click)="removeSkill($index)">削除</button>
            @if (s().touched()) { @for (e of s().errors(); track e.kind) { <span class="err err-skill">{{ e.message }}</span> } }
          </div>
        }
        <button type="button" id="skill-add" (click)="addSkill()">追加</button>
        @for (e of f.skills().errors(); track e.kind) { <p class="err err-skills">{{ e.message }}</p> }
      </fieldset>

      <label><input id="reg-agree" type="checkbox" [formField]="f.agree" /> 利用規約に同意する</label>
      @if (f.agree().touched()) { @for (e of f.agree().errors(); track e.kind) { <p class="err err-agree">{{ e.message }}</p> } }

      <button id="reg-submit" type="submit">登録</button>
      <p id="reg-status">valid={{ f().valid() }} pending={{ f().pending() }}</p>
      <pre id="reg-result">{{ result() }}</pre>
    </form>
  `,
})
export class RegisterForm {
  protected readonly result = signal('');
  private readonly model = signal<RegisterModel>({
    name: '', email: '', password: '', confirm: '', skills: [''], agree: false,
  });

  protected readonly f = form(
    this.model,
    (p) => {
      required(p.name, { message: 'お名前を入力してください' });
      maxLength(p.name, 20, { message: 'お名前は20文字以内です' });

      required(p.email, { message: 'メールを入力してください' });
      email(p.email, { message: 'メールの形式で入力してください' });
      // 非同期検証:同期検証がすべて通ってから実行される
      validateAsync(p.email, {
        params: ({ value }) => value(),
        factory: (mail: Signal<string | undefined>) =>
          resource({
            params: () => mail(),
            loader: ({ params }) => isEmailTaken(params),
          }),
        debounce: 300,
        onSuccess: (taken) => (taken ? { kind: 'emailTaken', message: 'このメールは登録済みです' } : null),
        onError: () => ({ kind: 'checkFailed', message: '確認できませんでした。時間をおいて再度お試しください' }),
      });

      required(p.password, { message: 'パスワードを入力してください' });
      minLength(p.password, 8, { message: '8文字以上で入力してください' });
      pattern(p.password, /[0-9]/, { message: '数字を1文字以上含めてください' });
      debounce(p.password, 'blur'); // 入力中ではなく、フォーカスが外れたときにモデルへ反映

      // カスタム検証(項目をまたぐ):確認用がパスワードと一致するか
      validate(p.confirm, ({ value, valueOf }) =>
        value() === valueOf(p.password) ? null : { kind: 'mismatch', message: 'パスワードが一致しません' },
      );

      // 配列:各要素に必須、配列全体に件数制限
      applyEach(p.skills, (item) => {
        required(item, { message: '入力するか削除してください' });
      });
      validate(p.skills, ({ value }) =>
        value().length > 3 ? { kind: 'tooMany', message: '3件までです' } : null,
      );

      required(p.agree, { message: '同意が必要です' });
    },
    {
      submission: {
        action: async (tree) => {
          this.result.set(JSON.stringify(tree().value(), null, 1));
        },
        onInvalid: (tree) => {
          this.result.set('エラー: ' + tree().errorSummary().map((e) => e.kind).join(', '));
        },
      },
    },
  );

  addSkill() {
    this.model.update((m) => ({ ...m, skills: [...m.skills, ''] }));
  }
  removeSkill(i: number) {
    this.model.update((m) => ({ ...m, skills: m.skills.filter((_, idx) => idx !== i) }));
  }
}

実行結果(Playwright)

操作 画面の結果
空のまま「登録」 お名前を入力してください / メールを入力してください / パスワードを入力してください / 入力するか削除してください / 同意が必要です、結果欄 エラー: required, required, required, required, required
メールに taken@example.com を入力(0.1秒後) 確認中… を表示、valid=false pending=true
〃(1秒後) このメールは登録済みです、valid=false pending=false
メールを new@example.com に変更 メールのエラーが消える
パスワード abcdefgh → フォーカスを外す 数字を1文字以上含めてください
パスワード abcdefg1、確認用 abcdefg2 パスワードはエラーなし、確認用に パスワードが一致しません
確認用を abcdefg1 に エラーが消える
「追加」を3回(4件に) 3件までです
1件削除し、3件すべて入力 配列のエラーが消える
同意にチェック valid=true pending=false
「登録」 下の JSON を表示
{
 "name": "あおい",
 "email": "new@example.com",
 "password": "abcdefg1",
 "confirm": "abcdefg1",
 "skills": [
  "設計",
  "テスト",
  "レビュー"
 ],
 "agree": true
}

コードの読みどころ

1. 非同期検証は「同期検証が通ってから」動く

validateAsync は required と email が通ってから実行されるので、空欄や形式エラーの間は問い合わせが発生しません。実行中は pending() が true、valid() も invalid() も false です。既定では実行中の検証は送信を妨げないため、完了まで待たせたいときは submission に ignoreValidators: 'none' を指定します。

HTTP で問い合わせるだけなら、request(URLを返す関数)・onSuccess・onError を渡す validateHttp でより短く書けます(この記事では疑似 API を使った validateAsync 版のみ実行しています)。

2. 項目をまたぐ検証は valueOf で他のフィールドを読む

valueOf(p.password) はシグナルとして追跡されるので、パスワード側を変えても確認用のエラーが自動で再計算 され、エラーも確認用フィールドに直接付きます。

3. 配列は「モデルを更新する」だけで入力欄が増減する

@for (s of f.skills; track $index) で各要素を回し、追加・削除は モデルのシグナルを update するだけ です。

4. debounce(p.password, 'blur') で反映タイミングを変える

入力のたびではなく、フォーカスが外れたときにモデルへ反映します。Reactive Forms の updateOn: 'blur' にあたります。

Reactive Forms で同じフォームを書いた場合

比較のため、同じ会員登録フォームを Reactive Forms で書き、同じ環境で動かしました。

// src/app/forms/reactive-register.ts(検証とフォーム定義の部分)
const TAKEN = ['taken@example.com', 'admin@example.com'];
const emailTaken: AsyncValidatorFn = async (c: AbstractControl): Promise<ValidationErrors | null> => {
  await new Promise((r) => setTimeout(r, 500));
  return TAKEN.includes(String(c.value).toLowerCase()) ? { emailTaken: true } : null;
};
const matchPassword: ValidatorFn = (g: AbstractControl) =>
  g.get('password')?.value === g.get('confirm')?.value ? null : { mismatch: true };
const maxItems = (n: number): ValidatorFn => (a: AbstractControl) =>
  (a as FormArray).length > n ? { tooMany: true } : null;

// コンポーネント内
private readonly fb = inject(FormBuilder).nonNullable;
protected readonly f = this.fb.group(
  {
    name: ['', [Validators.required, Validators.maxLength(20)]],
    email: ['', { validators: [Validators.required, Validators.email], asyncValidators: [emailTaken] }],
    password: ['', { validators: [Validators.required, Validators.minLength(8), Validators.pattern(/[0-9]/)], updateOn: 'blur' }],
    confirm: [''],
    skills: this.fb.array([this.fb.control('', Validators.required)], maxItems(3)),
    agree: [false, Validators.requiredTrue],
  },
  { validators: matchPassword },
);

テンプレートでは [formGroup] と formControlName、formArrayName を使い、エラーは f.controls.email.hasError('emailTaken') のように種類ごとに書きました。

実行結果: 空で送信すると お名前を入力してください と invalid: INVALID、taken@example.com では 確認中… の後に このメールは登録済みです が出ました。Reactive Forms はメッセージを hasError('...') ごとにテンプレートへ書き、Signal Forms はルールに持たせて errors() を回すだけ という違いがあります。

Reactive Forms → Signal Forms 移行対応表

Reactive Forms Signal Forms メモ
ReactiveFormsModule FormField, FormRoot import 元は @angular/forms/signals
fb.group({ ... }) form(signal({ ... }), schema) 値の型はモデルから推論
[formGroup]="f" + (ngSubmit) <form [formRoot]="f"> + submission novalidate も自動
formControlName="email" [formField]="f.email" 文字列ではなく型つきの参照
Validators.required required(p.email, { message }) メッセージをルールに持たせる
Validators.email / minLength / pattern email() / minLength() / pattern() ほぼ同名
Validators.requiredTrue required(p.agree) チェックボックスの false は「空」扱い
グループの ValidatorFn(パスワード一致) validate(p.confirm, ({ valueOf }) => ...) エラーが確認用フィールドに付く
AsyncValidatorFn validateAsync / validateHttp resource ベース、debounce あり
FormArray + push / removeAt 配列モデル + update / applyEach @for (s of f.skills; ...)
updateOn: 'blur' debounce(p.x, 'blur')
control.value / valueChanges f.x().value()(シグナル) computed / effect でそのまま使える
control.errors / hasError('x') f.x().errors() / getError('x')
pending / touched / dirty pending() / touched() / dirty() すべてシグナル
markAllAsTouched() 送信時に自動 / markAsTouched() v22 は子孫も含む
disable() / enable() disabled(p.x, { when }) 状態から宣言的に決まる
reset() f().reset(value?)

既存画面を一度に書き換える必要はありません。 @angular/forms/signals/compat には、Reactive Forms の FormControl をモデルに含めたまま Signal Forms を作れる compatForm() や、Signal Forms を Reactive Forms 側で扱う SignalFormControl などの互換 API が用意されている(いずれも @publicApi 22.0)ので、新しく作るフォームから Signal Forms にしていくと無理なく移行できます。

Signal Forms をテストする

スキーマの動きはコンポーネントなしでテストできます。form() は注入コンテキストが必要なので TestBed.runInInjectionContext の中で作ります。

// src/app/forms/register-form.spec.ts
import { TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { form, required, validate } from '@angular/forms/signals';

describe('Signal Forms のスキーマ', () => {
  it('パスワード一致の検証', () => {
    TestBed.runInInjectionContext(() => {
      const model = signal({ password: 'abc12345', confirm: 'x' });
      const f = form(model, (p) => {
        required(p.password);
        validate(p.confirm, ({ value, valueOf }) =>
          value() === valueOf(p.password) ? null : { kind: 'mismatch', message: '一致しません' },
        );
      });
      expect(f.confirm().errors().map((e) => e.kind)).toEqual(['mismatch']);
      f.confirm().value.set('abc12345');
      expect(f.confirm().valid()).toBe(true);
      expect(model().confirm).toBe('abc12345'); // フィールドへの書き込みはモデルに反映
    });
  });
});

ng test --watch=false(Vitest 5.0.2)で成功しました。フィールドに値を書くとモデルのシグナルも変わる ことがテストでも確認できます。

つまずきやすいポイント

症状 原因 直し方
[formField] がテンプレートでエラーになる コンポーネントの imports に FormField がない imports: [FormField] を追加
古い記事のとおり [field] と書いて動かない v21.0 の書き方 [formField] に置き換える
画面を開いた瞬間からエラーが出る 検証は入力前から実行されている touched() のときだけ表示する
送信してもページがリロードされる <form> に [formRoot] がない [formRoot]="f" を付ける(または (submit) で submit(f, ...) を呼ぶ)
非同期検証が動かない 同期検証がまだ通っていない 仕様どおり。required などを満たすと実行される
非同期検証の途中で valid() も invalid() も false 検証中(pending() が true) pending() で「確認中…」を出す
配列の要素を追加しても入力欄が増えない 配列を push して同じ参照を set した update(m => ({ ...m, skills: [...m.skills, ''] })) のように新しい配列を作る

よくある質問

Q. Signal Forms は本番で使えますか? A. はい。Angular 22.0 で安定版(@publicApi 22.0)になりました。 v21 では実験版だったため、v21 時代のサンプルは [field] などの書き方が異なる場合があります。WebMCP 連携(experimentalWebMcpTool)だけは引き続き実験的な機能です。

Q. エラーメッセージはどこに書くのがよいですか? A. ルールの message オプションに書く のがおすすめです。テンプレートは errors() を回すだけになり、表示の書き方がどのフィールドでも同じになります。種類ごとに出し分けたい場合は getError('required') を使います。

Q. 送信処理でサーバーから返ったエラーを特定の項目に出すには? A. submission.action から { kind: 'taken', message: '...', fieldTree: f.email } のように fieldTree を付けたエラーを返す と、その項目のエラーとして表示されます。

まとめ

  • Signal Forms は Angular 22.0 で安定版。 form() と [formField]、[formRoot] が基本の3点です。
  • 検証ルールは required / email / validate / validateAsync などの関数で宣言し、メッセージもルールに持たせる と、テンプレートがすっきりします。
  • 項目をまたぐ検証は valueOf、配列は applyEach とモデルの update で書けます。
  • 非同期検証は同期検証が通ってから動き、実行中は pending() が true になります。
  • v21 実験版からは [field] → [formField]、customError() → オブジェクトなどが変わっています。 v22 では getError・minDate・reloadValidation も加わりました。
  • Reactive Forms は引き続き使えるので、新しいフォームから Signal Forms にしていく のが無理のない進め方です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular Signal Forms入門|v22安定版のAPI早見表と完成コード」

株式会社RJCでは、新しい API を実際に動かしながら、フォームのような身近な画面づくりの工夫をチームで共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社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で一緒に開発しながら、 成長を楽しみませんか?