React・実務の道具立て

古いReactのコードを読む

既存の案件に入ると、フック以前の書き方に必ず当たります。クラスコンポーネント・HOC・render props・PropTypes・旧Contextを「書く」のではなく「読む」ために、いまの書き方との対応表として整理します。

ここまでは新しく書く話でした。この回は逆で、既にあるコードを読む話です。

案件に入って最初に開くファイルが、このコースで習った形とは限りません。Reactは2013年からあり、フックが入ったのは2019年です。それ以前の書き方が、いまも大量に動いています。

書けるようになる必要はありません。読めれば十分です。

クラスコンポーネント

いちばんよく出会います。

class BirdCard extends Component {
  state = { count: 0 }

  componentDidMount() {
    document.title = this.props.bird.name
  }

  componentDidUpdate(prevProps) {
    if (prevProps.bird.id !== this.props.bird.id) {
      document.title = this.props.bird.name
    }
  }

  componentWillUnmount() {
    document.title = 'しまちゃん'
  }

  handleClick = () => {
    this.setState((prev) => ({ count: prev.count + 1 }))
  }

  render() {
    return <button onClick={this.handleClick}>{this.props.bird.name} / {this.state.count}</button>
  }
}

対応はこうです。

クラスいまの書き方
this.props.birdpropsの分割代入{ bird }
state = { count: 0 }useState(0)
this.setState({ count: 1 })setCount(1)
this.setState((prev) => …)setCount((prev) => …)同じ形
componentDidMountuseEffect(() => { … }, [])
componentDidUpdateuseEffect(() => { … }, [依存])
componentWillUnmountuseEffectreturnで片づけ
render()関数のreturn

stateが2つの形で書かれているのも見ます。

constructor(props) {
  super(props)
  this.state = { count: 0 }
  this.handleClick = this.handleClick.bind(this)   // ← thisの束縛
}

bindが出てくるのは、メソッドの中のthisが外れるというクラス特有の問題への対処です。フックにはそもそもthisが無いので、この行はまるごと消えます。

HOC(高階コンポーネント)

コンポーネントを受け取って、機能を足したコンポーネントを返す関数です。名前はwith〜で始まるのが慣習でした。

export default withRouter(withTheme(connect(mapStateToProps)(BirdCard)))

見た目はぎょっとしますが、やっていることはカスタムフックと同じです。

HOCいまの書き方
withRouter(Component)useNavigate() / useParams()
connect(mapStateToProps)(Component)useSelector()
withTheme(Component)useContext(ThemeContext)

中身に外から機能を注入するという目的は同じで、包む中で呼ぶかの違いです。フックのほうが読みやすいのは、どのpropsがどこから来たのかが行として見えるからです。

render props

子として関数を渡して、値を受け取る書き方です。

<MouseTracker>
  {({ x, y }) => <p>マウスは {x}, {y} にいます</p>}
</MouseTracker>

「値を持っている部品」と「表示する部品」を分けるための工夫でした。これもカスタムフックで置きかえられます

const { x, y } = useMousePosition()

いまでも残っている場面はあります(グラフのライブラリなど、描画する場所を渡したいとき)。見かけたら「関数を子どもとして渡している」と読めれば十分です。

PropTypes

TypeScript以前の型チェックです。

BirdCard.propTypes = {
  bird: PropTypes.shape({ name: PropTypes.string.isRequired }).isRequired,
  isFavorite: PropTypes.bool,
}
BirdCard.defaultProps = {
  isFavorite: false,
}

React 19では、これは無視されます。React.PropTypesも削除済みです(実際にtypeof React.PropTypesを見るとundefinedが返ってきます)。関数コンポーネントのdefaultPropsも削除されたので、初期値は引数の既定値で書きます。

function BirdCard({ bird, isFavorite = false }) {

旧Context・その他

たまに見る、それ以外のもの。

見かけるもの何だったか
<Context.Consumer>{(value) => …}</Context.Consumer>Contextの読み方(いまはuseContext
<Context.Provider value={…}>React 19からは<Context value={…}>でよい
this.refs.input文字列ref。React 19で削除済み
ReactDOM.render(…)React 18より前の起動。いまはcreateRoot(...).render(…)
UNSAFE_componentWillMount廃止予定のライフサイクル。名前のとおり触らない
React.createFactoryReact 19で削除済み
PureComponentmemoにあたるもの

案件に入った初日にやること

読み方が分かったうえで、何を先に見るかです。

  1. package.json — Reactのバージョン、ルーター、状態管理、テスト。このプロジェクトの語彙が全部ここにある
  2. src/main.tsx(またはindex.js — 何で包まれているか(Provider・Router・Query)
  3. ルーティングの定義 — 画面の一覧がここに出る
  4. いちばん大きい画面のファイル1つ — 書き方の癖が分かる
  5. READMEと、直近のPR数件 — チームの作法が出る
React画面全体を落とさない(Error BoundaryとSuspense)Error Boundaryの回。いまもクラスで書く、唯一の場所です

公式ドキュメントも見てみる

ja.react.dev外部サイトレガシー React API – ReactReact公式の「レガシーAPI」。いま非推奨のもの・React 19で削除されたものが一覧になっています。読んでいるコードで見つけた名前を引くのに使えます

このレッスンのまとめ

  1. この回は書くためではなく読むため。フックは2019年からで、それ以前が大量に動いている
  2. クラスのcomponentDidMount/DidUpdate/WillUnmountは、useEffect1つに対応する
  3. ただし機械的に置きかえると回数が変わる。理由なく書きかえない
  4. bind(this)はクラス特有のthis対策。フックにはそもそもthisが無い
  5. HOC(with〜)はカスタムフックと同じ目的。包むか、中で呼ぶかの違い
  6. render propsは関数を子として渡す形。いまはカスタムフック
  7. PropTypesはReact 19で無視されるdefaultPropsは引数の既定値へ
  8. Error Boundaryだけは今もクラス。「古い=動かない」ではない
  9. 案件の初日はpackage.jsonmain.tsx → ルーティング → 大きい画面 → READMEの順

次からは、公開の前に踏んでおきたい章です。まずはReactの安全な書き方から。

やってみよう:クラスに書きかえて、戻す

読めるかどうかは、一度書いてみるのが早いです。

  1. BirdCardクラスコンポーネントで書き直すBirdCardClass.jsxとして別に作る)
  2. statethis.propsrender()handleClick = () => {}を使う
  3. componentDidMountconsole.logを出し、いつ呼ばれるかを見る
  4. 一覧で1枚だけ差しかえて、同じように動くことを確認する
  5. 確認できたら削除する(残さない)
class BirdCardClass extends Component {
  handleClick = () => {
    this.props.onToggleFavorite(this.props.bird.id)
  }

  render() {
    const { bird, isFavorite } = this.props
    return <button onClick={this.handleClick}>{isFavorite ? '★' : '☆'} {bird.name}</button>
  }
}

書いてみると、this.がどれだけ多いかが体感できます。フックが歓迎された理由も、同時に分かります。

よくある質問

クラスコンポーネントはもう書かなくてよいのですか?
新しく書く必要はありません。ただしError Boundaryだけは今もクラスでしか書けず、既存のコードには大量に残っているので、読めることは必要です。
componentDidMountはuseEffectと同じですか?
だいたい同じですが、正確には違います。クラスは「マウント時・更新時・アンマウント時」で分かれていて、useEffectは「依存配列が変わったら実行し、次の前に片づける」という別の考え方です。機械的に置きかえると挙動が変わることがあります。
見つけたクラスコンポーネントは書きかえるべきですか?
動いているものを理由なく書きかえるのは、レビューで止まる筆頭です。触る必要が出たときに、その範囲だけ直すのが実務の作法です。
PropTypesは残っていたらどうしますか?
React 19では無視されるので、動作には影響しません。TypeScriptを入れているなら型に寄せていきますが、これも触るついでに、が基本です。