🌍

【React.js/Next.js/TypeScript】僕のコーディングルール

2022/12/11に公開

背景

先日、大規模開発を行なっていくなかで、その言語に合わせたコーディングルールを持っておくのは大切なことだと痛感しました。

ここの出来・不出来で保守コストが大きく変わります。読みやすいコードは、機能追加担当者・バグ修正担当者が比較的短時間で誤読なく理解できますし、拡張性のあるコードであれば、修正範囲も狭くなるためです。

そのため、開発の中でブラッシュアップし続けているコーディングルールをここにまとめておき、自分自身の明確な基準としたり、一緒に開発する人と合わせる基準にしたいと思い、筆をとることとしました。

※ この記事は、日々ブラッシュアップしていく予定。

※押し付けるつもりは全くありません

割と読みやすいコードは人それぞれのケースもあるので、僕は自分のコーディングルールが絶対だとまったく思っていません。コーディング方針を決めていく中での材料になればくらいに捉えております。

僕のコーディングルール

基本的には リーダブルコードスタイルガイド(コーディング規約) に沿って記載していますが、その中でも特に重要だと考えた部分や、実践の中でテクニック的に効果的だと考えた部分をまとめていきます。

変数

変数① 変数名は省略しない

BAD
var cnt:number  // NG よく見る略し方でも略さない
var pIJSLY:PersonalData // NG 長くても略さない
GOOD
var count:number // OK 誰が見てもわかる単語
var personalInfoJoiningSinceLastYear:PersonalData // OK 長いがわかりやすい

変数名は省略しない派です。

省略した方が変数名が短くなり、見やすくなるかもしれませんが、他の開発者が意味を読み取れない可能性があるため、変数は省略しない方がよいと考えています。

変数② 配列に対して「itemList」は使用しないで、「items」を使用する

BAD
var followerList:User[]  // NG ~Listを使用する
GOOD
var followers:User[]  // OK ~sを使用する

Clean Codeにも記載がありますが、List というのは、プログラムにおいて特別な意味があります。(例:Java の List インターフェースなど)

そのような重要な単語を変数名に使用すると、誤解を生む可能性があります。

そのため、~List ではなく ~s を使用した方がよいと考えています。

変数③ 型定義を行う

BAD
var followers  // NG 型定義を行わない
var followers:any  // NG any型で型定義を行う
GOOD
var followers:User[]  // OK 型定義を行う

TypeScript を使用するメリットの9割は型定義ができることです。型定義は、ランタイムエラー防止に役立つため、必ず行うようにしています。

繰り返し

繰り返し① 配列操作時は基本的に map や filter などの配列用関数を使用する

BAD
var animals = ["monkey", "cat", "dog"]
for(var animal:String of animals){
  // Do Something
}
GOOD
var animals = ["monkey", "cat", "dog"]
animals.map((animal) => {
  // Do Something
})

var animals = ["monkey", "cat", "dog"]
animals.filter((animal) => {
  // Do Something
})

TypeScript には mapfilter など、配列を扱うために便利なメソッドが用意されています。配列のメソッドを利用した方が短く、意味も通じやすくなるケースが多いため、基本的には配列用関数を使用します。

繰り返し② ループカウンタを使用しない場合は拡張 for 文を使用する

BAD
var animals = ["monkey", "cat", "dog"]
for(var i:number; i < animal.length(); i++){
  // Do Something
}
GOOD
var animals = ["monkey", "cat", "dog"]
for(var animal:String of animals){
  // Do Something
}

配列用関数が不適切、かつ、ループカウンタ(よく i で定義するやつ)を使用しない場合、拡張 for 文を利用するのがよいです。

条件分岐

条件分岐① else がない場合は即 return する

BAD
const hoge = () =>{
  if(foo === "bar"){
    // Do Something
  }
}
GOOD
const hoge = () =>{
  if(foo !== "bar") return;  // 条件式を逆にし、即 return する
  // Do Something
}

基本的には、if の中身は短ければ短いほど良いと考えています。

後続のプログラマの気持ちになって考えてみてください。条件を頭の中で覚えておきつつ、中身の処理を追うのは結構しんどくないですか?

なので、else がないならば、逆の条件を条件式に記載し、return するのが、親切だと考えます。

条件分岐② 厳密等価演算子を使用する

BAD
if(hoge == "fuga") {
  // Do Something
}
GOOD
if(hoge === "fuga") {
  // Do Something
}

等価演算子( == )ではなく、厳密等価演算子( === )を使用します。型の一致まで確認してくれるためです。

本来、動的型付け言語にて、重要視されるものですが、TypeScript でもどこからしらの型定義が漏れる可能性がないわけではないので、念のため、厳密等価演算子を利用します。

条件分岐③ (!hoge)は使用しない

BAD
if(!hoge) {
  // Do Something
}
GOOD
if(hoge === false) {
  // Do Something
}

!(hoge) の場合、hogefalse の場合はもちろん、 nullundefined の場合も、条件が一致していると判断してしまいます。

そして、意図せず通過した hoge が処理内で問題を起こします。

そのため、!(hoge) は使用しない方がよいです。

条件分岐④ jsx 内での条件分岐はシンプルな三項演算子のみ

BAD
const hoge = () => {

  return (
    <>
      {hoge === true?
        <ChildA/>:fuga===true?
	  <ChildB/>:<ChildC>
      }
    </>
  )
}
GOOD
const hoge = () => {
  const [child, setChild] = useState(<></>) 
  if(hoge){
    setChild(<ChildA/>);
    if(huga){
      setChild(<ChildB/>);
    }else{
      setChild(<ChildC/>);
    }
  }
  return <>{child}</>
}

三項演算子は条件分岐をシンプルに見せるための方法です。
BAD の例のようにネストにするのはよくないと考えています。

※三項演算子自体はとても好きで、よく使っています。

メソッド

メソッド① 戻り値、引数の型定義を行う

BAD
const hoge = (fuga, foo) => {
  // Do Something
}
GOOD
const hoge = (fuga:string, foo:number) => {
  // Do Something
}

メソッドの引数にも型定義を行います。

メソッド② フラグ引数は避ける

BAD
const action = (foo:boolean) => {
  if(foo){
    // Do ActionA
  } else {
    // Do ActionB
  }
}

const hoge = () => {
  var foo:boolean;
  // Process
  Action(foo);
}
GOOD

const actionA = () => {
  // Do ActionA
}

const actionB = () => {
  // Do ActionB
}

const hoge = () => {
  var foo:boolean;
  // Process
  if(foo){
    actionA();
  } else {
    actionB();
  }
}

メソッド内部の分岐で使用するフラグは フラグ引数 と言われ、アンチパターンにカテゴライズされます。

フラグを渡して、メソッド内で分岐するのではなく、メソッドを分けて、呼び出し元で分岐するようにしましょう。

拡張性も高くなりますし、テストも行やすくなります。

React/Next

React/Next① useEffect 内で複雑な処理を記述しない

BAD
const hoge = () => {

  useEffect(()=>{
   // Do ActionA
   // Do ActionB
   // Do ActionC
 },[])
}
GOOD
const hoge = () => {

const actionA = () => {
  // Do ActionA
}

const actionB = () => {
  // Do ActionB
}

const actionC = () => {
  // Do ActionC
}

  useEffect(()=>{
   actionA();
   actionB();
   actionC();
 },[])
}

useEffect の中に処理を大量に記述する方法は避けたいです。上記の Do ActionAたちは一行なので、目立っていませんが、もし、20行、30行である場合、どこまでが useEffect の範囲かわからなくなってしまいます。

そのため、メソッドとして切り分け、 useEffect 内でそのメソッドを呼び出す形がよいかと考えます。

ケースバイケースですが、 if文でも同じことがいえるかなと思います。

React/Next② model を作成する際は、type を使用する

BAD
interface hoge {
  name:string,
  age:number,
  mobile:string
}
GOOD
type hoge = {
  name:string,
  age:number,
  mobile:string
}

個人的には interface よりも type 派です。

interface のメリットとして、オブジェクトを拡張できるというものがあります。しかし、僕にはどうしてもそれがデメリットにしか見えません。いつの間にオブジェクトが変わっていたなんてことは避けたいです。

そのため、僕は type を使用します。

React/Next③ pages 配下以外は export default は使用しない

pages 配下以外では、コンポーネントを公開する際、export を使用します。(= export default は使用しない)

pages 配下は Next.js の制約から、export default にする必要があります。

export default を使用した場合、呼び出し元は、ファイルパスさえ指定できていれば、その処理を取得できてしまいます(= 呼び出し先のクラス名やメソッド名まで指定する必要はない)。

つまり、呼び出し先のメソッド名が変わっても、インポートによる問題は一切起きません。

もし、メソッド名が変更された場合、変更後のメソッド名に適した処理が付け加えられていったり、修正されていくことが目に見えています。

その場合、呼び出し元の意図から離れていく可能性が高くなります。

そのため、呼び出し先の名前変更を防ぐ意味でも、export default は使用せず、 export を使用するようにしています。

React/Next④ 関数型コンポーネントを使用する

React.js, Next.js のコンポーネント作成において、クラス型と関数型の選択肢がありますが、記述がシンプルになるので、僕は関数型コンポーネント推しです。

参考

Discussion