Reactのベストプラクティス





Reactで開発していますか、それともこのテクノロジーに興味がありますか?次に、私の新しいプロジェクトであるTotalReactへようこそ



前書き



私はReactを5年間使用していますが、アプリケーションの構造や外観(デザイン)に関しては、普遍的なアプローチに名前を付けることは困難です。



同時に、プロジェクトの長期的なサポートとスケーラビリティを確保できる特定のコーディング手法があります。



この記事は、私と私が協力してきたチームにとって効果的であることが証明されたReactアプリケーションを開発するための一連のルールです。



これらのルールは、コンポーネント、アプリケーション構造、テスト、スタイリング、状態管理、およびデータ取得を対象としています。提供されている例は、特定の実装ではなく一般的な原則に焦点を当てるために、意図的に簡略化されています。



提案されたアプローチは究極の真実ではありません。これは私の意見です。同じタスクを実行するには、さまざまな方法があります。



コンポーネント



機能コンポーネント


機能コンポーネントを優先します-それらはより単純な構文を持っています。それらには、ライフサイクルメソッド、コンストラクター、および定型コードがありません。これらを使用すると、クラスコンポーネントと同じロジックを実装できますが、労力が少なく、より説明的な方法で実装できます(コンポーネントコードが読みやすくなります)。



ヒューズが必要になるまで、機能コンポーネントを使用してください。心に留めておくべきメンタルモデルははるかに単純になります。



//     ""
class Counter extends React.Component {
  state = {
    counter: 0,
  }

  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    this.setState({ counter: this.state.counter + 1 })
  }

  render() {
    return (
      <div>
        <p> : {this.state.counter}</p>
        <button onClick={this.handleClick}></button>
      </div>
    )
  }
}

//       
function Counter() {
  const [counter, setCounter] = useState(0)

  handleClick = () => setCounter(counter + 1)

  return (
    <div>
      <p> : {counter}</p>
      <button onClick={handleClick}></button>
    </div>
  )
}

      
      





一貫性のある(順次)コンポーネント


コンポーネントを作成するときは、同じスタイルに固執します。ヘルパー関数を同じ場所に配置し、同じエクスポート(デフォルトまたは名前)を使用し、コンポーネントに同じ命名規則を使用します。



それぞれのアプローチには、独自の長所と短所があります。



コンポーネントをどのようにエクスポートするかは関係ありません。一番下または定義で、1つのルールに固執するだけです。



コンポーネント名


常にコンポーネントに名前を付けてください。これは、React開発ツールを使用するときにエラースタックトレースを解析するのに役立ちます。



また、現在開発しているコンポーネントを判別するのにも役立ちます。



//    
export default () => <form>...</form>

//    
export default function Form() {
  return <form>...</form>
}

      
      





二次機能


コンポーネントに格納されているデータを必要としない関数は、コンポーネントの外部(外部)にある必要があります。このための理想的な場所は、コンポーネント定義の前であり、コードを上から下に調べることができます。



これにより、コンポーネントの「ノイズ」が減少します。必要なものだけがコンポーネントに残ります。



//    
function Component({ date }) {
  function parseDate(rawDate) {
    ...
  }

  return <div> {parseDate(date)}</div>
}

//      
function parseDate(date) {
  ...
}

function Component({ date }) {
  return <div> {parseDate(date)}</div>
}

      
      





コンポーネント内には最小限の数の補助機能が必要です。それらを外部に配置し、状態からの値を引数として渡します。



「クリーンな」関数を作成するためのルールに従うことで、エラーの追跡とコンポーネントの拡張が容易になります。



//      ""    
export default function Component() {
  const [value, setValue] = useState('')

  function isValid() {
    // ...
  }

  return (
    <>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={validateInput}
      />
      <button
        onClick={() => {
          if (isValid) {
            // ...
          }
        }}
      >
        
      </button>
    </>
  )
}

//          
function isValid(value) {
  // ...
}

export default function Component() {
  const [value, setValue] = useState('')

  return (
    <>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={validateInput}
      />
      <button
        onClick={() => {
          if (isValid(value)) {
            // ...
          }
        }}
      >
        
      </button>
    </>
  )
}

      
      





静的(ハード)マークアップ


ナビゲーション、フィルター、またはリストの静的マークアップを作成しないでください。代わりに、設定を使用してオブジェクトを作成し、ループします。



つまり、必要に応じて、マークアップと要素を1か所で変更するだけで済みます。



//     
function Filters({ onFilterClick }) {
  return (
    <>
      <p> </p>
      <ul>
        <li>
          <div onClick={() => onFilterClick('fiction')}> </div>
        </li>
        <li>
          <div onClick={() => onFilterClick('classics')}>
            
          </div>
        </li>
        <li>
          <div onClick={() => onFilterClick('fantasy')}></div>
        </li>
        <li>
          <div onClick={() => onFilterClick('romance')}></div>
        </li>
      </ul>
    </>
  )
}

//       
const GENRES = [
  {
    identifier: 'fiction',
    name: ' ',
  },
  {
    identifier: 'classics',
    name: '',
  },
  {
    identifier: 'fantasy',
    name: '',
  },
  {
    identifier: 'romance',
    name: '',
  },
]

function Filters({ onFilterClick }) {
  return (
    <>
      <p> </p>
      <ul>
        {GENRES.map(genre => (
          <li>
            <div onClick={() => onFilterClick(genre.identifier)}>
              {genre.name}
            </div>
          </li>
        ))}
      </ul>
    </>
  )
}

      
      





コンポーネントの寸法


コンポーネントは、小道具を受け取り、マークアップを返す関数です。それらは同じ設計原則に従います。



関数が実行するタスクが多すぎる場合は、ロジックの一部を別の関数に移動します。同じことがコンポーネントにも当てはまります。コンポーネントに複雑すぎる機能が含まれている場合は、それをいくつかのコンポーネントに分割します。



マークアップの一部が複雑で、ループや条件が含まれている場合は、それを別のコンポーネントに抽出します。



インタラクションとデータ取得については、小道具とコールバックに依存します。コードの行数は、必ずしもその品質の客観的な基準ではありません。常に応答性と抽象化を忘れないでください。



JSXのコメント


何が起こっているのかを説明する必要がある場合は、コメントブロックを作成し、そこに必要な情報を追加します。マークアップはロジックの一部であるため、一部にコメントする必要があると思われる場合は、そうしてください。



function Component(props) {
  return (
    <>
      {/*    ,       */}
      {user.subscribed ? null : <SubscriptionPlans />}
    </>
  )
}

      
      





サーキットブレーカー


コンポーネントのエラーは、ユーザーインターフェイスを壊してはなりません。重大なエラーによってアプリケーションがクラッシュまたはリダイレクトされることがまれにあります。ほとんどの場合、画面から特定の要素を削除するだけで十分です。



データを要求する関数では、try / catchブロックをいくつでも持つことができます。アプリケーションのトップレベルでヒューズを使用するだけでなく、例外をスローする可能性のあるすべてのコンポーネントをラップアラウンドして、エラーのカスケードを回避します。



function Component() {
  return (
    <Layout>
      <ErrorBoundary>
        <CardWidget />
      </ErrorBoundary>

      <ErrorBoundary>
        <FiltersWidget />
      </ErrorBoundary>

      <div>
        <ErrorBoundary>
          <ProductList />
        </ErrorBoundary>
      </div>
    </Layout>
  )
}

      
      





小道具を破壊する


ほとんどのコンポーネントは、小道具を受け取り、マークアップを返す関数です。通常の関数では、直接渡される引数を使用するため、コンポーネントの場合は、同様のアプローチに従うのが理にかなっています。どこでも「小道具」を繰り返す必要はありません。



デストラクチャリングを使用しない理由は、外部状態と内部状態の違いである可能性があります。ただし、通常の関数では、引数と変数の間に違いはありません。物事を複雑にする必要はありません。



//    "props"   
function Input(props) {
  return <input value={props.value} onChange={props.onChange} />
}

//        
function Component({ value, onChange }) {
  const [state, setState] = useState('')

  return <div>...</div>
}

      
      





小道具の数


小道具の数に関する質問への答えは非常に主観的です。コンポーネントに渡される小道具の数は、コンポーネントによって使用される変数の数と相関しています。コンポーネントに渡される小道具が多いほど、その責任は高くなります(つまり、コンポーネントによって解決されるタスクの数)。



小道具の数が多い場合は、コンポーネントの処理が多すぎることを示している可能性があります。



5つ以上の小道具がコンポーネントに渡される場合、それを分割する必要があると思います。場合によっては、コンポーネントは大量のデータを必要とするだけです。たとえば、テキスト入力フィールドには多くの小道具が必要な場合があります。一方、これは、ロジックの一部を別のコンポーネントに抽出する必要があることを示しています。



注意:コンポーネントが受け取る小道具が多いほど、再描画される頻度が高くなります。



プリミティブの代わりにオブジェクトを渡す


渡される小道具の数を減らす1つの方法は、プリミティブの代わりにオブジェクトを渡すことです。たとえば、ユーザーの名前やメールアドレスなどを送信する代わりに。一度に1つずつ、グループ化できます。また、新しいデータの追加も簡単になります。



//      
<UserProfile
  bio={user.bio}
  name={user.name}
  email={user.email}
  subscription={user.subscription}
/>

//   ,  
<UserProfile user={user} />

      
      





条件付きレンダリング


場合によっては、条件付きレンダリングに短い計算(論理AND演算子&&)を使用すると、UIに0が表示されることがあります。これを回避するには、三項演算子を使用します。このアプローチの唯一の欠点は、もう少しコードが多いことです。



&&演算子はコードの量を減らします。これは素晴らしいことです。Ternarnikはより「冗長」ですが、常に正しく機能します。さらに、必要に応じて代替を追加するのにかかる時間が短縮されます。



//     
function Component() {
  const count = 0

  return <div>{count && <h1>: {count}</h1>}</div>
}

//   ,   
function Component() {
  const count = 0

  return <div>{count ? <h1>: {count}</h1> : null}</div>
}

      
      





ネストされた三項演算子


三項演算子は、最初のネストレベルの後で読みにくくなります。三元はスペースを節約しますが、明示的かつ明白な方法でその意図を表現するのが最善です。



//     
isSubscribed ? (
  <ArticleRecommendations />
) : isRegistered ? (
  <SubscribeCallToAction />
) : (
  <RegisterCallToAction />
)

//      
function CallToActionWidget({ subscribed, registered }) {
  if (subscribed) {
    return <ArticleRecommendations />
  }

  if (registered) {
    return <SubscribeCallToAction />
  }

  return <RegisterCallToAction />
}

function Component() {
  return (
    <CallToActionWidget
      subscribed={subscribed}
      registered={registered}
    />
  )
}

      
      





リスト


リストの要素をループすることは一般的なタスクであり、通常は「map()」メソッドを使用して実行されます。ただし、多くのマークアップを含むコンポーネントでは、余分なインデントと「map()」構文によって読みやすさが向上しません。



要素を反復処理する必要がある場合は、マークアップが小さくても、要素を別のコンポーネントに抽出します。親コンポーネントは詳細を必要とせず、リストが特定の場所でレンダリングされていることを「知る」必要があるだけです。



反復は、リストを表示することを唯一の目的とするコンポーネントに残すことができます。リストのマークアップが複雑で長い場合は、別のコンポーネントに抽出するのが最善です。



//       
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      {articles.map(article => (
        <div>
          <h3>{article.title}</h3>
          <p>{article.teaser}</p>
          <img src={article.image} />
        </div>
      ))}
      <div>    {page}</div>
      <button onClick={onNextPage}></button>
    </div>
  )
}

//      
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      <ArticlesList articles={articles} />
      <div>    {page}</div>
      <button onClick={onNextPage}></button>
    </div>
  )
}

      
      





デフォルトの小道具


デフォルトの小道具を定義する1つの方法は、「defaultProps」属性をコンポーネントに追加することです。ただし、このアプローチでは、コンポーネント関数とその引数の値は異なる場所にあります。



したがって、小道具を破壊するときは「デフォルト」値を割り当てる方が望ましいです。これにより、コードが上から下に読みやすくなり、定義と値が1か所に保持されます。



//          
function Component({ title, tags, subscribed }) {
  return <div>...</div>
}

Component.defaultProps = {
  title: '',
  tags: [],
  subscribed: false,
}

//      
function Component({ title = '', tags = [], subscribed = false }) {
  return <div>...</div>
}

      
      





ネストされたレンダリング関数


コンポーネントからロジックまたはマークアップを抽出する必要がある場合は、同じコンポーネントの関数に配置しないでください。コンポーネントは関数です。これは、コードの抽出された部分がネストされた関数として表されることを意味します。



これは、ネストされた関数が外部関数の状態とデータにアクセスできることを意味します。これにより、コードが読みにくくなります-この関数は何をしますか(何を担当しますか)?



ネストされた関数を別のコンポーネントに移動し、名前を付けて、クロージャではなく小道具に依存します。



//       
function Component() {
  function renderHeader() {
    return <header>...</header>
  }
  return <div>{renderHeader()}</div>
}

//      
import Header from '@modules/common/components/Header'

function Component() {
  return (
    <div>
      <Header />
    </div>
  )
}

      
      





状態管理



ギアボックス


「useState()」よりも強力な状態の定義と管理の方法が必要になる場合があります。サードパーティのライブラリを使用する前に、「useReducer()」を使用してみてください。これは、依存関係を必要とせずに複雑な状態を管理するための優れたツールです。



コンテキストとTypeScriptを組み合わせると、useReducer()は非常に強力になります。残念ながら、あまり使用されていません。人々は特別なライブラリを使用することを好みます。



複数の状態が必要な場合は、それらをレデューサーに移動します。



//       
const TYPES = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large'
}

function Component() {
  const [isOpen, setIsOpen] = useState(false)
  const [type, setType] = useState(TYPES.LARGE)
  const [phone, setPhone] = useState('')
  const [email, setEmail] = useState('')
  const [error, setError] = useSatte(null)

  return (
    // ...
  )
}

//      
const TYPES = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large'
}

const initialState = {
  isOpen: false,
  type: TYPES.LARGE,
  phone: '',
  email: '',
  error: null
}

const reducer = (state, action) => {
  switch (action.type) {
    ...
    default:
      return state
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    // ...
  )
}

      
      





フックとHOCおよびレンダリング小道具


場合によっては、コンポーネントを「強化」するか、外部データへのアクセスを提供する必要があります。これを行うには、3つの方法があります。高次コンポーネント(HOC)、小道具とフックを介したレンダリングです。



最も効果的な方法は、フックを使用することです。それらは、コンポーネントは他の機能を使用する機能であるという哲学に完全に準拠しています。フックを使用すると、これらのソース間の競合の脅威なしに、外部機能を含む複数のソースにアクセスできます。フックの数は関係ありません。値をどこから取得したかは常にわかっています。



HOCは小道具として値を受け取ります。値がどこから来たのか、親コンポーネントから来たのか、そのラッパーから来たのかは必ずしも明らかではありません。さらに、複数の小道具を連鎖させることは、よく知られているバグの原因です。



レンダープロップを使用すると、ネストが深くなり、読みやすさが低下します。同じツリーにレンダープロップを使用して複数のコンポーネントを配置すると、状況がさらに悪化します。さらに、マークアップで値のみを使用するため、値を取得するためのロジックをここに記述するか、外部から受け取る必要があります。



フックの場合、追跡が容易でJSXと混同しない単純な値を使用しています。



//    -
function Component() {
  return (
    <>
      <Header />
        <Form>
          {({ values, setValue }) => (
            <input
              value={values.name}
              onChange={e => setValue('name', e.target.value)}
            />
            <input
              value={values.password}
              onChange={e => setValue('password', e.target.value)}
            />
          )}
        </Form>
      <Footer />
    </>
  )
}

//   
function Component() {
  const [values, setValue] = useForm()

  return (
    <>
      <Header />
        <input
          value={values.name}
          onChange={e => setValue('name', e.target.value)}
        />
        <input
          value={values.password}
          onChange={e => setValue('password', e.target.value)}
        />
      <Footer />
    </>
  )
}

      
      





データを取得するためのライブラリ


多くの場合、状態のデータはAPIから「取得」されます。それらをメモリに保存し、更新して、いくつかの場所で受け取る必要があります。 React Queryの



ような最新のライブラリ は、外部データを操作するためのかなりの量のツールを提供します。データをキャッシュして削除し、新しいデータをリクエストできます。これらのツールは、データの送信、別のデータの更新のトリガーなどにも使用できます。 ApolloのようなGraphQLクライアントを使用すると、外部データの操作がさらに簡単になります すぐに使用できるクライアント状態の概念を実装します。







状態管理ライブラリ


ほとんどの場合、アプリケーションの状態を管理するためのライブラリは必要ありません。これらは、非常に複雑な状態の非常に大規模なアプリケーションでのみ必要です。このような状況では、2つのソリューションのいずれかを使用します -Recoilまたは Redux



コンポーネントメンタルモデル



コンテナおよび代表者


通常、コンポーネントを2つのグループ(代表とコンテナ、または「スマート」と「バカ」)に分割するのが通例です。



要するに、一部のコンポーネントには状態と機能が含まれていないということです。それらは、いくつかの小道具を使用して親コンポーネントによって呼び出されるだけです。次に、コンテナコンポーネントには、いくつかのビジネスロジックが含まれ、データを受信するための要求を送信し、状態を管理します。



このメンタルモデルは、実際にはサーバー側アプリケーションのMVCデザインパターンを記述しています。彼女はそこで素晴らしい仕事をしています。



しかし、最近のクライアントアプリケーションでは、このアプローチはそれ自体を正当化するものではありません。すべてのロジックを複数のコンポーネントに配置すると、肥大化につながります。これは、1つのコンポーネントがあまりにも多くの問題を解決するという事実につながります。このようなコンポーネントのコードは、保守が困難です。アプリケーションが大きくなるにつれて、コードを適切な状態に維持することはほとんど不可能になります。



ステートフルおよびステートレスコンポーネント


コンポーネントをステートフルコンポーネントとステートレスコンポーネントに分割します。上記のメンタルモデルは、少数のコンポーネントがアプリケーション全体のロジックを駆動する必要があることを示唆しています。このモデルは、ロジックを可能な最大数のコンポーネントに分割することを前提としています。



データは、それが使用されるコンポーネントに可能な限り近いものでなければなりません。 GrapQLクライアントを使用する場合、このデータを表示するコンポーネントでデータを受け取ります。トップレベルのコンポーネントでなくても。コンテナについて考えるのではなく、コンポーネントの責任について考えてください。状態の一部を保持するための最も適切なコンポーネントを決定します



たとえば、<Form />コンポーネントにはフォームデータが含まれている必要があります。 <Input />コンポーネントは、値を受け取り、コールバックを呼び出す必要があります。 <Button />コンポーネントは、処理などのためにデータを送信したいというユーザーの希望についてフォームに通知する必要があります。



フォームの検証は誰が担当しますか?入力フィールド?これは、このコンポーネントがアプリケーションのビジネスロジックを担当することを意味します。エラーについてフォームにどのように通知しますか?エラー状態はどのように更新されますか?フォームはそのような更新について「知っています」か?エラーが発生した場合、データを送信して処理することはできますか?



そのような疑問が生じると、責任の混乱があることが明らかになります。この場合、「入力」はステートレスコンポーネントのままで、フォームからエラーメッセージを受信する方が適切です。



アプリケーション構造



ルート/モジュールによるグループ化


コンテナとコンポーネントによるグループ化は、アプリケーションの学習を困難にします。特定のコンポーネントがアプリケーションのどの部分に属しているかを判断するには、コードベース全体に「精通している」ことを前提としています。



すべてのコンポーネントが同じというわけではありません。グローバルに使用されるコンポーネントもあれば、特定のニーズを満たすように設計されたコンポーネントもあります。この構造は、小規模なプロジェクトに適しています。ただし、中規模から大規模のプロジェクトでは、このような構造は受け入れられません。



//        
├── containers
|   ├── Dashboard.jsx
|   ├── Details.jsx
├── components
|   ├── Table.jsx
|   ├── Form.jsx
|   ├── Button.jsx
|   ├── Input.jsx
|   ├── Sidebar.jsx
|   ├── ItemCard.jsx

//     /
├── modules
|   ├── common
|   |   ├── components
|   |   |   ├── Button.jsx
|   |   |   ├── Input.jsx
|   ├── dashboard
|   |   ├── components
|   |   |   ├── Table.jsx
|   |   |   ├── Sidebar.jsx
|   ├── details
|   |   ├── components
|   |   |   ├── Form.jsx
|   |   |   ├── ItemCard.jsx

      
      





最初から、ルート/モジュールごとにコンポーネントをグループ化します。この構造により、長期的なサポートと拡張が可能になります。これにより、アプリケーションがそのアーキテクチャを超えて成長するのを防ぐことができます。「コンテナコンポーネントアーキテクチャ」に依存している場合、これは非常に迅速に発生します。



モジュールベースのアーキテクチャは非常にスケーラブルです。システムの複雑さを増すことなく、新しいモジュールを追加するだけです。



「コンテナアーキテクチャ」は間違いではありませんが、それほど一般的ではありません(抽象的)。Reactを使用してアプリケーションを開発する以外は、それを学んだ人にはわかりません。



共通モジュール


ボタン、入力フィールド、カードなどのコンポーネントはどこにでもあります。コンポーネントベースのフレームワークを使用していない場合でも、それらを共有コンポーネントに抽出します。



このようにして、ストーリーブックを使用しなくても、アプリケーションで使用されている一般的なコンポーネントを確認できます これにより、コードの重複が回避されます。チームのすべてのメンバーが独自のバージョンのボタンをデザインすることを望まないのですか?残念ながら、これは多くの場合、アプリケーションアーキテクチャが不十分なことが原因です。



絶対パス


アプリケーションの個々の部分は、できるだけ簡単に変更する必要があります。これは、コンポーネントコードだけでなく、その場所にも当てはまります。絶対パスは、インポートされたコンポーネントを別の場所に移動するときに何も変更する必要がないことを意味します。また、コンポーネントを見つけやすくなります。



//     
import Input from '../../../modules/common/components/Input'

//      
import Input from '@modules/common/components/Input'

      
      





内部モジュールのインジケータとして「@」プレフィックスを使用しますが、「〜」文字の使用例も確認しました。



外付け部品のラッピング


あまりにも多くのサードパーティコンポーネントを直接インポートしないようにしてください。このようなコンポーネント用のアダプターを作成することで、必要に応じてAPIを変更できます。使用するライブラリを1か所で変更することもできます。



これは、セマンティックUIやユーティリティなどのコンポーネントライブラリの両方に適用されます。最も簡単な方法は、そのようなコンポーネントを共有モジュールから再エクスポートすることです。



コンポーネントは、使用している特定のライブラリを知る必要はありません。



//     
import { Button } from 'semantic-ui-react'
import DatePicker from 'react-datepicker'

//       
import { Button, DatePicker } from '@modules/common/components'

      
      





1つのコンポーネント-1つのディレクトリ


アプリケーションのモジュールごとにコンポーネントディレクトリを作成します。まず、コンポーネントを作成します。次に、スタイルやテストなど、コンポーネントに関連する追加のファイルが必要な場合は、コンポーネントのディレクトリを作成し、その中にすべてのファイルを配置します。



コンポーネントを再エクスポートするには、「index.js」ファイルを作成することをお勧めします。これにより、インポートパスを変更せず、コンポーネント名の重複を回避できます-「importForm from'components / UserForm / UserForm '」。ただし、コンポーネントコードを「index.js」ファイルに配置しないでください。コードエディタでタブの名前でコンポーネントを見つけることができなくなります。



//        
├── components
    ├── Header.jsx
    ├── Header.scss
    ├── Header.test.jsx
    ├── Footer.jsx
    ├── Footer.scss
    ├── Footer.test.jsx

//      
├── components
    ├── Header
        ├── index.js
        ├── Header.jsx
        ├── Header.scss
        ├── Header.test.jsx
    ├── Footer
        ├── index.js
        ├── Footer.jsx
        ├── Footer.scss
        ├── Footer.test.jsx

      
      





パフォーマンス



時期尚早の最適化


最適化を開始する前に、これには理由があることを確認してください。アプリケーションに影響がない場合、ベストプラクティスに盲目的に従うことは時間の無駄です。



もちろん、最適化などについて考える必要がありますが、読み取り可能で保守可能なコンポーネントを開発することをお勧めします。適切に記述されたコードは、改善が容易です。



アプリケーションのパフォーマンスの問題に気付いた場合は、それを測定して原因を特定してください。バンドルサイズが大きい場合、再レンダリングの数を減らすことは意味がありません。



問題を特定したら、パフォーマンスへの影響の順に修正します。



ビルドサイズ


ブラウザに送信されるJavaScriptの量は、アプリケーションのパフォーマンスにおける重要な要素です。アプリケーション自体は非常に高速ですが、実行するために4MBのJavaScriptをプリロードする必要があるかどうかは誰にもわかりません。



1つのバンドルを目指しないでください。ルートレベルなどでアプリケーションを分割します。最小限のコードをブラウザに送信してください。



バックグラウンドでロードするか、ユーザーがアプリケーションの別の部分を取得しようとしているときにロードします。ボタンをクリックするとPDFのダウンロードが開始され、ボタンにカーソルを合わせるとすぐに対応するライブラリのダウンロードを開始できます。



再レンダリング-コールバック、配列、オブジェクト


コンポーネントの再レンダリングの数を減らすように努力する必要があります。これを覚えておいてください。ただし、不必要な再レンダリングがアプリケーションに重大な影響を与えることはめったにないことにも注意してください。



小道具としてコールバックを送信しないでください。このアプローチでは、関数は毎回再作成され、再レンダリングがトリガーされます。



クロージャが原因でパフォーマンスの問題が発生した場合は、それらを取り除きます。ただし、コードを読みにくくしたり、冗長にしすぎたりしないでください。



配列またはオブジェクトの受け渡しは、同じカテゴリの問題に明示的に分類されます。それらは参照によって比較されるため、表面的なチェックに合格せず、再レンダリングをトリガーしません。静的配列を渡す必要がある場合は、コンポーネントを定義する前に定数として作成してください。これにより、毎回同じインスタンスを渡すことができます。



テスト



スナップショットテスト


かつて、スナップショットテストの実行中に興味深い問題が発生しました。引数なしの「新しい日付()」を現在の日付と比較すると、常に「false」が返されます。



さらに、スナップショットでは、コンポーネントが変更された場合にのみアセンブリが失敗します。一般的なワークフローは次のとおりです。コンポーネントに変更を加え、テストに失敗し、スナップショットを更新して、続行します。



スナップショットはコンポーネントレベルのテストに置き換わるものではないことを理解することが重要です。個人的には、このタイプのテストはもう使用していません。



正しいレンダリングのテスト


テストの主な目的は、コンポーネントが期待どおりに機能していることを確認することです。コンポーネントがデフォルトの小道具と渡された小道具の両方で正しいマークアップを返すことを確認してください。



また、関数が特定の入力に対して常に正しい結果を返すことを確認してください。必要なものがすべて画面に正しく表示されていることを確認してください。



状態とイベントのテスト


ステートフルコンポーネントは通常、イベントに応じて変化します。イベントモックを作成し、コンポーネントがそれに正しく応答することを確認します。



ハンドラーが呼び出され、正しい引数が渡されていることを確認してください。内部状態の正しい設定を確認してください。



エッジケースのテスト


コードを基本的なテストでカバーした後、特別な場合をチェックするためにいくつかのテストを追加します。



これは、空の配列を渡して、チェックせずにインデックスにアクセスしないようにすることを意味する場合があります。また、コンポーネント(APIリクエストなど)でエラーを呼び出して、正しく処理されたかどうかを確認することも意味します。



統合テスト


統合テストとは、ページ全体または大きなコンポーネントをテストすることを意味します。このタイプのテストは、特定の抽象化のパフォーマンスをテストすることを意味します。これにより、アプリケーションが期待どおりに実行されているという、より説得力のある結果が得られます。



個々のコンポーネントは単体テストに合格できますが、コンポーネント間の相互作用によって問題が発生する可能性があります。



様式化



CSSからJSへ


これは非常に物議を醸す問題です。私は個人的に、JavaScriptでスタイルを記述できるStyledComponentsやEmotionなどのライブラリを使用することを好みます。1ファイル少ない。クラス名のようなことは考えないでください。



Reactの構成要素はコンポーネントであるため、CSS-in-JS手法、より正確にはall-in-JS手法が、私の意見では好ましい手法です。



他のスタイリングアプローチ(SCSS、CSSモジュール、Tailwindのようなスタイルのライブラリ)は間違っていないことに注意してください。ただし、CSS-in-JSを使用することをお勧めします。



様式化されたコンポーネント


通常、スタイル設定されたコンポーネントとそれらを使用するコンポーネントを同じファイルに保持しようとします。



ただし、スタイル設定されたコンポーネントが多数ある場合は、それらを別のファイルに移動するのが理にかなっています。このアプローチがSpectrumのようないくつかのオープンソースプロジェクトで使用されているのを見てきました。



データ受信中



データを操作するためのライブラリ


Reactは、データを取得または更新するための特別なツールを提供していません。各チームは独自の実装を作成します。これには通常、APIと相互作用する非同期関数のサービスが含まれます。



このアプローチを取るということは、ダウンロードステータスを追跡し、HTTPエラーを処理するのは私たちの責任であることを意味します。これは、冗長性と多くの定型コードにつながります。



代わりに、ReactQuerySWRなどのライブラリを使用すること をお 勧めします。それらは、フックを使用して、サーバーの相互作用を慣用的な方法でコンポーネントライフサイクルの有機的な部分にします。



キャッシュ、負荷状態管理、およびエラー処理のサポートが組み込まれています。また、このデータを処理するための状態管理ライブラリも不要です。



ご清聴ありがとうございました。



All Articles