404 Not Found

404 Not Found


nginx

イベント処理

イベント処理は、React アプリの神経終末のようなものです。ユーザーがボタンをクリックしたり、テキストを入力したり、フォームを送信したりすると、これらの操作はイベントを通じて処理されます。React のイベントシステムは、ネイティブの DOM イベントよりもスマートで、統一性があります。


1. 学習内容



2. フォームの操作に関する話

(1) 課題:フォーム送信における3つの落とし穴

ボブはユーザー登録フォームを作成しており、ネイティブのJavaScriptでイベントハンドラを記述しています:

JAVASCRIPT
// Native JS:Various compatibility issues and details need to be handled manually

// 1. Get DOM Element
const form = document.getElementById('register-form')
const nameInput = document.getElementById('name')
const submitBtn = document.getElementById('submit-btn')

// 2. Event Binding
form.addEventListener('submit', function(event) {
  event.preventDefault()  // Prevent the page from refreshing
  
  // 3. Manually Retrieving Form Data
  const formData = new FormData(form)
  const data = Object.fromEntries(formData)
  
  // 4. Submit Data
  fetch('/api/register', {
    method: 'POST',
    body: JSON.stringify(data)
  })
})

// 5. Real-Time Input Validation
nameInput.addEventListener('input', function(event) {
  if (event.target.value.length < 2) {
    showError('Name (at least) 2 characters')
  } else {
    hideError()
  }
})

// 6. Prevent duplicate submissions when the Submit button is clicked
let isSubmitting = false
submitBtn.addEventListener('click', function() {
  if (isSubmitting) return
  isSubmitting = true
  submitBtn.disabled = true
  submitBtn.textContent = 'Submitting......'
})

ボブの問題:

(2) Reactにおけるイベント処理

JSX
function RegisterForm() {
  const [name, setName] = React.useState('')
  const [isSubmitting, setIsSubmitting] = React.useState(false)

  // Submit for Processing
  function handleSubmit(event) {
    event.preventDefault()
    setIsSubmitting(true)
    
    fetch('/api/register', {
      method: 'POST',
      body: JSON.stringify({ name })
    }).finally(() => setIsSubmitting(false))
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Please enter your name"
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting......' : 'Register'}
      </button>
    </form>
  )
}

メリット:コード行数が30行から15行に削減され、DOMの操作が不要となり、イベントの解除に関する問題やブラウザ間の互換性の問題も発生しなくなりました。



3. Reactの複合イベント

ReactのイベントはネイティブなDOMイベントではなく、SyntheticEventsです。Reactはトップレベルでこれらを監視し、クロスブラウザ対応のイベントシステムをシミュレートしています。

特集 ネイティブDOMイベント Reactの複合イベント
バインディング方法 addEventListener / onclick JSXに直接記述
ブラウザの互換性 差異の手動処理が必要 自動標準化
メモリ管理 リスナーは手動で削除する必要がある 自動クリーンアップ
イベントオブジェクト ネイティブイベント SyntheticEvent
気泡の発生を防ぐ event.stopPropagation() 同一(標準化)
ブロックのデフォルト event.preventDefault() 同一(標準化)
100%
graph TB
    A[The user clicks the button] --> B[React Root Node Event Handling]
    B --> C[Create a Synthetic Event Object]
    C --> D[Simulated Bubbles/Capture Phase]
    D --> E[Call JSX Bound to the middle handler]
    
    style B fill:#61dafb,color:#000
    style C fill:#1890ff,color:#fff


4. よくあるイベントのクイックリファレンス

イベント名 トリガー条件 一般的なシナリオ イベントオブジェクトの種類
onClick 要素がクリックされた ボタン、リンク、カード MouseEvent
onChange 入力フィールドの内容の変更 フォームの入力欄、ドロップダウンメニュー ChangeEvent
onSubmit フォームを送信 ログイン/登録/検索 FormEvent
onFocus 要素にフォーカスが当たる 入力フィールドがハイライトされる FocusEvent
onBlur 要素のフォーカスが外れる 入力の検証 FocusEvent
onKeyDown キーを押してください ショートカットキー、Enterキーで検索 KeyboardEvent
onKeyUp プレスリリース リアルタイム検索 KeyboardEvent
onMouseEnter マウスオーバー ホバー効果 MouseEvent
onMouseLeave マウスアウト ホバー効果 MouseEvent
onScroll スクロール 無限スクロール UIEvent

(1) ▶ サンプル:よくあるイベントの包括的な概要

JSX
// ============================================
// Example:Complete Event Handling for a Login Form
// ============================================

function LoginForm() {
  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [errors, setErrors] = React.useState({})
  const [focusedField, setFocusedField] = React.useState('')

  function handleSubmit(event) {
    event.preventDefault()
    
    // Form Validation
    const newErrors = {}
    if (!email.includes('@')) newErrors.email = 'The email address format is incorrect.'
    if (password.length < 6) newErrors.password = 'Password must be at least 6 chars'
    
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors)
      return
    }
    
    // Submit Login
    console.log('Log In:', { email, password })
  }

  function handleKeyDown(event) {
    // Press Escape key to clear focus
    if (event.key === 'Escape') {
      event.target.blur()
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
      <h2>Log In</h2>

      {/* Enter your email address */}
      <div style={{ marginBottom: '16px' }}>
        <label>Email:</label>
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          onFocus={() => setFocusedField('email')}
          onBlur={() => { setFocusedField(''); setErrors({...errors, email: undefined}) }}
          onKeyDown={handleKeyDown}
          style={{
            width: '100%',
            padding: '8px',
            borderColor: errors.email ? '#ff4d4f' : focusedField === 'email' ? '#1890ff' : '#d9d9d9'
          }}
        />
        {errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
      </div>

      {/* Password Entry */}
      <div style={{ marginBottom: '16px' }}>
        <label>Password:</label>
        <input
          type="password"
          value={password}
          onChange={e => setPassword(e.target.value)}
          onFocus={() => setFocusedField('password')}
          onBlur={() => setFocusedField('')}
          onKeyDown={handleKeyDown}
          style={{
            width: '100%',
            padding: '8px',
            borderColor: errors.password ? '#ff4d4f' : focusedField === 'password' ? '#1890ff' : '#d9d9d9'
          }}
        />
        {errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
      </div>

      {/* Submit Button */}
      <button
        type="submit"
        onClick={() => console.log('Clicked the "Log In" button')}
        onMouseEnter={() => console.log('Button on Mouse Hover')}
        onMouseLeave={() => console.log('Mouse leaves button')}
        style={{
          width: '100%',
          padding: '10px',
          backgroundColor: '#1890ff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        Log In
      </button>
    </form>
  )
}
▶ 試してみよう

5. イベントパラメータの受け渡し

(1) パラメータの直接渡し

それを矢印関数で囲み、追加のパラメータを直接渡します:

JSX
function UserList({ users, onDelete }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          {/* Arrow Functions:Incoming user.id As an additional parameter */}
          <button onClick={() => onDelete(user.id)}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  )
}

// Usage
<UserList
  users={users}
  onDelete={(id) => console.log('Delete User:', id)}
/>

(2) イベントオブジェクトとカスタムパラメータの両方を取得する

JSX
function ColorPicker({ colors, onSelect }) {
  return (
    <div>
      <p>Select a Color:</p>
      {colors.map(color => (
        <button
          key={color}
          onClick={(event) => {
            // event:Native Synthesis Event Object
            // color:Custom Parameters
            onSelect(color)
            console.log('Click here:', event.clientX, event.clientY)
          }}
          style={{
            backgroundColor: color,
            width: '40px',
            height: '40px',
            border: '2px solid #ddd',
            borderRadius: '50%',
            margin: '4px',
            cursor: 'pointer'
          }}
        />
      ))}
    </div>
  )
}

(3) イベントオブジェクトを渡す3つの方法

メソッド 構文 適用可能なシナリオ
暗黙の引数渡し onClick={handleClick} 追加のパラメータは不要
矢印関数 onClick={() => handleClick(id)} パラメータが必要
同時に通過 onClick={(e) => handleClick(e, id)} イベントオブジェクトとカスタムパラメータが必要


6. デフォルト動作とバブリングの防止

(1) デフォルトの動作を防ぐ:preventDefault()

最も一般的な使用例は、フォームが送信された際にページが再読み込みされないようにすることです:

JSX
function SearchForm() {
  const [query, setQuery] = React.useState('')

  function handleSubmit(event) {
    event.preventDefault()  // Prevent the page from refreshing
    // Execute Custom Search Logic
    console.log('Search:', query)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  )
}

// Other scenarios where the default behavior needs to be overridden:
// - Link Redirection:<a href="#" onClick={e => e.preventDefault()}>
// - Context Menu:onContextMenu={e => e.preventDefault()}
// - Drag-and-Drop File Upload:onDragOver={e => e.preventDefault()}

(2) 気泡の発生を防ぐ:stopPropagation()

JSX
function Modal() {
  return (
    // Click the mask layer to close the pop-up window
    <div
      style={overlayStyle}
      onClick={() => console.log('Click the mask layer → Close Pop-up')}
    >
      {/* Clicking on the pop-up content does not display a speech bubble */}
      <div
        style={modalStyle}
        onClick={(event) => {
          event.stopPropagation()  // Prevent Bubbles from Rising to the Mask Layer
          console.log('Click on the pop-up window content')
        }}
      >
        <h2>Pop-up Title</h2>
        <p>Pop-up Content</p>
        <button onClick={() => console.log('Close')}>Close</button>
      </div>
    </div>
  )
}

// Click "Modal Content" area → only triggers modal onClick
// Click "Overlay" (outside modal) → trigger mask layer onClick


7. 完全な例:To-Doリスト(包括的なイベント処理)

JSX
// ============================================
// Example:Todo List(Comprehensive Incident Handling)
// Features:Add a Task、Marked as complete、Delete Task、Double-click to edit
// ============================================

function TodoApp() {
  const [todos, setTodos] = React.useState([
    { id: 1, text: 'Study React Event', done: false },
    { id: 2, text: 'Complete the homework', done: false }
  ])
  const [input, setInput] = React.useState('')

  // 1. Add a Task(onSubmit + Enter key)
  function handleSubmit(event) {
    event.preventDefault()
    if (!input.trim()) return
    
    setTodos([...todos, {
      id: Date.now(),
      text: input.trim(),
      done: false
    }])
    setInput('')
  }

  // 2. Quick Add with the Enter Key(onKeyDown)
  function handleKeyDown(event) {
    if (event.key === 'Enter' && input.trim()) {
      handleSubmit(event)
    }
  }

  // 3. Switch to "Completed" status(onChange)
  function handleToggle(id) {
    setTodos(todos.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ))
  }

  // 4. Delete Task(onClick + Parameter Passing)
  function handleDelete(id, event) {
    event.stopPropagation()  // Prevent triggering li the incident
    setTodos(todos.filter(t => t.id !== id))
  }

  // 5. Double-click to edit(onDoubleClick)
  function handleEdit(todo) {
    const newText = prompt('Edit Task:', todo.text)
    if (newText && newText.trim()) {
      setTodos(todos.map(t =>
        t.id === todo.id ? { ...t, text: newText.trim() } : t
      ))
    }
  }

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📋 Todo List</h2>

      {/* Input Form */}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder="Input Task,Press Enter to add..."
          style={{
            width: '70%', padding: '8px',
            border: '1px solid #d9d9d9', borderRadius: '4px'
          }}
        />
        <button type="submit" style={{
          padding: '8px 16px', marginLeft: '8px',
          backgroundColor: '#1890ff', color: 'white',
          border: 'none', borderRadius: '4px', cursor: 'pointer'
        }}>
          Add
        </button>
      </form>

      {/* Statistics */}
      <p style={{ color: '#666', fontSize: '14px' }}>
        Total {todos.length} items, Completed {todos.filter(t => t.done).length} items
      </p>

      {/* Task List */}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <li
            key={todo.id}
            onDoubleClick={() => handleEdit(todo)}
            style={{
              display: 'flex', alignItems: 'center',
              padding: '10px', margin: '4px 0',
              backgroundColor: todo.done ? '#f6ffed' : '#fff',
              border: '1px solid #f0f0f0',
              borderRadius: '4px',
              textDecoration: todo.done ? 'line-through' : 'none',
              color: todo.done ? '#999' : '#333',
              cursor: 'pointer'
            }}
          >
            {/* Checkbox */}
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => handleToggle(todo.id)}
              style={{ marginRight: '10px' }}
            />

            {/* Mission Text */}
            <span style={{ flex: 1 }}>{todo.text}</span>

            {/* Delete Button */}
            <button
              onClick={(e) => handleDelete(todo.id, e)}
              style={{
                padding: '2px 8px',
                backgroundColor: 'transparent',
                color: '#ff4d4f',
                border: 'none',
                cursor: 'pointer',
                fontSize: '16px'
              }}
            >
              ✕
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

インタラクションの流れ

  1. テキストボックスにタスク名を入力し、Enter キーを押すか、「追加」ボタンをクリックします → タスクがリストに追加されます
  2. チェックボックスにチェックを入れる → タスクを完了としてマークする(テキストに打ち消し線が入り、灰色で表示される)
  3. ✕ ボタンをクリック → タスクを削除する(ダブルクリックで編集機能が起動しないようにするため、stopPropagation
  4. 任意のタスクをダブルクリック → 編集用のダイアログボックスが表示されます

(1) ▶ サンプル:キーボードショートカットとフォーカスの管理

JSX
// ============================================
// Example:Shortcut Keys Panel——Integrated Use of Keyboard Events and Focus Management
// Features:Usage onKeyDown Implement Keyboard Shortcuts,Usage onFocus/onBlur Management Focus
// ============================================

function ShortcutPanel() {
  const [output, setOutput] = React.useState('')
  const [activeKeys, setActiveKeys] = React.useState(new Set())
  const inputRef = React.useRef(null)

  // Keyboard Event Handling
  function handleKeyDown(event) {
    const { key, ctrlKey, shiftKey, altKey } = event

    // Ctrl+S:Save
    if (ctrlKey && key === 's') {
      event.preventDefault()
      setOutput('💾 Saved(Ctrl+S)')
    }
    // Ctrl+Z:Revoke
    else if (ctrlKey && key === 'z') {
      event.preventDefault()
      setOutput('↩️ Undo(Ctrl+Z)')
    }
    // Escape:Clear Output
    else if (key === 'Escape') {
      setOutput('')
      event.target.blur()
    }
    // Enter:Confirm
    else if (key === 'Enter') {
      setOutput(`✅ Confirm:${event.target.value || '(Empty input)'}`)
    }

    // Display the currently pressed key
    setActiveKeys(prev => new Set([...prev, key]))
  }

  function handleKeyUp(event) {
    setActiveKeys(prev => {
      const next = new Set(prev)
      next.delete(event.key)
      return next
    })
  }

  // Autofocus
  React.useEffect(() => {
    inputRef.current.focus()
  }, [])

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>⌨️ Shortcut Keys Panel</h2>

      <input
        ref={inputRef}
        onKeyDown={handleKeyDown}
        onKeyUp={handleKeyUp}
        onFocus={() => setOutput('The input field is currently focused')}
        onBlur={() => setOutput('The input field has lost focus')}
        placeholder="Type here,Try the shortcut keys..."
        style={{
          width: '100%',
          padding: '10px',
          fontSize: '16px',
          border: '2px solid #1890ff',
          borderRadius: '6px',
          outline: 'none'
        }}
      />

      {/* Keyboard Shortcut Tips */}
      <div style={{ marginTop: '12px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
        {[
          { key: 'Ctrl+S', desc: 'Save' },
          { key: 'Ctrl+Z', desc: 'Revoke' },
          { key: 'Enter', desc: 'Confirm' },
          { key: 'Escape', desc: 'Clear/blur' }
        ].map(shortcut => (
          <span key={shortcut.key} style={{
            padding: '4px 10px',
            backgroundColor: '#f5f5f5',
            border: '1px solid #d9d9d9',
            borderRadius: '4px',
            fontSize: '12px',
            fontFamily: 'monospace'
          }}>
            {shortcut.key} <span style={{ color: '#999' }}>{shortcut.desc}</span>
          </span>
        ))}
      </div>

      {/* Currently Pressed Key */}
      {activeKeys.size > 0 && (
        <p style={{ marginTop: '8px', fontSize: '13px', color: '#666' }}>
          Currently Pressed Key:{[...activeKeys].join(' + ')}
        </p>
      )}

      {/* Operation Output */}
      {output && (
        <div style={{
          marginTop: '12px',
          padding: '10px',
          backgroundColor: '#f6ffed',
          border: '1px solid #b7eb8f',
          borderRadius: '4px',
          color: '#52c41a',
          fontWeight: 'bold'
        }}>
          {output}
        </div>
      )}
    </div>
  )
}
▶ 試してみよう

(2) ▶ サンプル:ドラッグ&ドロップによる並べ替えイベントの処理

JSX
function DragSortList() {
  const [items, setItems] = useState(['Apple', 'Banana', 'Cherry', 'Date'])
  const [dragIdx, setDragIdx] = useState(null)

  function handleDragStart(idx) { setDragIdx(idx) }

  function handleDragOver(e, idx) {
    e.preventDefault()
    if (dragIdx === null || dragIdx === idx) return
    setItems(prev => {
      const updated = [...prev]
      const [moved] = updated.splice(dragIdx, 1)
      updated.splice(idx, 0, moved)
      return updated
    })
    setDragIdx(idx)
  }

  function handleDragEnd() { setDragIdx(null) }

  return (
    <div style={{ maxWidth: 300, margin: '0 auto' }}>
      <h3>Drag to Reorder</h3>
      {items.map((item, idx) => (
        <div key={item} draggable
          onDragStart={() => handleDragStart(idx)}
          onDragOver={e => handleDragOver(e, idx)}
          onDragEnd={handleDragEnd}
          style={{
            padding: '8px 12px', marginBottom: 4, borderRadius: 4, cursor: 'grab',
            background: dragIdx === idx ? '#e6f7ff' : '#f5f5f5',
            border: dragIdx === idx ? '2px solid #1890ff' : '2px solid transparent',
          }}>
          {item}
        </div>
      ))}
    </div>
  )
}
▶ 試してみよう

(3) ▶ サンプル:イベントリスナーをラップするためのカスタムフック

JSX
function useEventListener(event, handler, element = window) {
  useEffect(() => {
    element.addEventListener(event, handler)
    return () => element.removeEventListener(event, handler)
  }, [event, handler, element])
}

function MouseTracker() {
  const [pos, setPos] = useState({ x: 0, y: 0 })
  const handler = useCallback(e => setPos({ x: e.clientX, y: e.clientY }), [])
  useEventListener('mousemove', handler)

  return (
    <div style={{ padding: 20 }}>
      <p>Mouse: ({pos.x}, {pos.y})</p>
      <div style={{
        width: 200, height: 200, border: '1px solid #ddd', position: 'relative', borderRadius: 4,
      }}>
        <div style={{
          width: 10, height: 10, borderRadius: '50%', background: '#1890ff',
          position: 'absolute', left: Math.min(pos.x - 100, 190), top: Math.min(pos.y - 100, 190),
          transition: 'left 0.1s, top 0.1s',
        }} />
      </div>
    </div>
  )
}
▶ 試してみよう

(4) ▶ サンプル:フォーム送信とイベントの組み合わせ

JSX
function SearchForm({ onSearch }) {
  const [query, setQuery] = useState('')
  const [category, setCategory] = useState('all')

  function handleSubmit(e) {
    e.preventDefault()
    onSearch({ query, category })
  }

  function handleReset() {
    setQuery('')
    setCategory('all')
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
        <input value={query} onChange={e => setQuery(e.target.value)}
          placeholder="Search..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
        <select value={category} onChange={e => setCategory(e.target.value)}
          style={{ padding: 8, borderRadius: 4 }}>
          <option value="all">All</option>
          <option value="electronics">Electronics</option>
          <option value="books">Books</option>
        </select>
      </div>
      <div style={{ display: 'flex', gap: 8 }}>
        <button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Search</button>
        <button type="button" onClick={handleReset} style={{ padding: '6px 16px', cursor: 'pointer' }}>Reset</button>
      </div>
    </form>
  )
}
▶ 試してみよう

❓ よくある質問

Q なぜ単に handleClick(id) ではなく、矢印関数 () => handleClick(id) を使うのですか?
A onClick={handleClick(id)} を直接記述すると、React はクリックされるまで待つのではなく、レンダリング中に handleClick(id) を即座に実行してしまいます。これは、(括弧付きの)関数呼び出しであり、(括弧なしの)関数参照ではないためです。正しい方法は、onClick={() => handleClick(id)}(実行を遅延させる矢印関数)または onClick={handleClick}(引数が渡されない場合に関数名を直接参照する)です。
Q 1つの要素に複数の同一のイベントをバインドすることはできますか?
A 2つのonClickを直接バインドすることはできません。解決策:① 同じハンドラー内で複数の関数onClick={() => { fn1(); fn2() }}を呼び出す;② または、その要素を複数のHOCでラップする。通常、要素に必要なイベントハンドラーは1つだけであり、その中に複数のロジックを呼び出すことができます。
Q Reactのシンセティックイベントとネイティブイベントの違いは何ですか?
A ReactのSyntheticEventは、ネイティブイベントをラップしたクロスブラウザ対応のラッパーであり、統一されたAPI(e.preventDefault()e.stopPropagation()など)を提供し、すべてのブラウザで一貫した動作を実現します。合成イベントはイベントデリゲーションの仕組みに基づいて動作します。つまり、すべてのイベントは個々のDOM要素ではなく、ルートノードにアタッチされます。React 17以降では、イベントはdocumentではなくルートノードにデリゲートされるため、複数のバージョンのReactが共存する場合の競合を防ぐことができます。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):ボタンがクリックされるたびに、表示されているカウントを1ずつ増やす ButtonCounter コンポーネントを作成してください。onClick イベントを使用してください。
  2. 上級演習(難易度 ⭐⭐):「デバウンス検索」を実装した SearchInput コンポーネントを作成してください。この検索機能は、ユーザーが入力を停止してから 500 ms 後に自動的にトリガーされます。onChange および useEffect を使用してください。
  3. 課題(難易度:⭐⭐⭐):ドラッグ&ドロップによる並べ替え機能を実装した DragAndDropList コンポーネントを作成してください。onDragStartonDragOver、および onDrop イベントを使用してください。リストの項目を新しい位置にドラッグして移動できるようにしてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%