未経験からエンジニア 奮闘記

未経験からエンジニアに自由に生きる途中

MENU

単一方向バインディングと双方向バインディングの違いとメリット・デメリットを理解する

背景

最近、UIライブラリのOSS開発者の方と話していて、「双方向バインディングは避けたほうが良い」と聞いて、ちょっと興味が湧きました。そこで、単一方向バインディングと双方向バインディングの違いやメリット・デメリットについてまとめてみました。

単一方向バインディングと双方向バインディングのメリット・デメリット

単一方向バインディング

メリット:

  • シンプルでわかりやすい: データの流れが一方向なので、コードの追跡が簡単。
  • デバッグが容易: データの変更が予測しやすく、バグが発生した際に原因を見つけやすい。
  • パフォーマンス向上: データの変更が少ないため、無駄な再レンダリングが減る。

デメリット:

  • コードが冗長になる: 状態管理が複雑な場合、コード量が増える。
  • 双方向の連携が面倒: 双方向のデータ連携が必要な場合、手動での更新が必要。

双方向バインディング

メリット:

  • 直感的: フォーム入力など、ユーザーのアクションに対して即時に反応できる。
  • コードが短くなる: 自動でデータが同期されるため、コード量が減る。

デメリット:

  • デバッグが難しい: データの流れが複雑になり、バグの原因を特定しにくい。
  • パフォーマンスの低下: 頻繁なデータの変更により、レンダリングが多発しやすい。

ReactとAngularでのサンプルコードを見比べてみます

Reactでの単一方向バインディング

import React, { useState } from 'react';

const SingleWayBinding = () => {
  const [value, setValue] = useState('');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div>
      <input type="text" value={value} onChange={handleChange} />
      <p>{value}</p>
    </div>
  );
};

export default SingleWayBinding;

Reactの場合、valueが変更されると、再度レンダリングして新しい値を用いて、UIを変更してますよね。 これはデータ => UIの一方方向が実現できていると思います。 とても分かりやすいですね。

Angularでの双方向バインディング

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  value: string = '';
}

// app.component.html
<div>
  <input [(ngModel)]="value" type="text" />
  <p>{{ value }}</p>
</div>

一方で、Angularの場合は、value(データ)を用いてUIを表示して、inputからvalueが更新されると、ReactのようにonChangeの関数などのでデータを更新せずともUIからデータを更新できてしまう仕組みになっています。 これは便利な部分もあると思うのですが、データが絡みあっているのでデバックがしずらく可能性がありそうですね。

まとめ

個人的には、Reactの単一方向バインディングの方がデータフローがシンプルでメンテナンスしやすいと感じました。特に大規模なアプリケーションでは、単一方向バインディングの方がバグを避けやすいかもしれません。
単一方向バインディングと双方向バインディングにはそれぞれメリットとデメリットがあります。ユースケースによってどちらが適しているかは異なるため、絶対に単一方向バインディングが良いというわけではありません。自分のプロジェクトに合った方法を選ぶことが大切です。

React Reduceの使い方をマスターしよう!

背景

JavaScriptreduceというメソッドは、存在は知っていても実際にどのような場面で使うのかがわかりにくいことがあります。私も以前はその一人でしたが、最近購入履歴のデータを加工してチャートに反映する機会があり、その時にreduceの便利さを実感しました。具体的には、Rechartsを使ってグラフを描くために、購入履歴データを集計する必要がありました。

jsのreduceの挙動

まず、reduceの基本的な挙動について説明いたします。reduceは配列を一つの値にまとめるための関数ですが、初期値(initialValue)が数字の場合とオブジェクトの場合で少し使い方が変わります。

初期値が数字の場合

例えば、配列内の数字の合計を求める場合は以下のようにします:

const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, current) => acc + current, 0);
console.log(sum); // 10

ここで、accは累積値で、currentが現在の要素です。initialValueとして0を渡しているので、最初のaccは0からスタートします。

初期値がオブジェクトの場合

次に、初期値がオブジェクトの場合を見てみましょう。例えば、購入履歴をカテゴリごとに集計したい場合は以下のようになります:

const purchaseHistories = [
  { id: 1, amount: 100, category: { name: '外食' } },
  { id: 2, amount: 50, category: { name: '交際費' } },
  { id: 3, amount: 100, category: { name: 'Netflix' } },
  { id: 4, amount: 150, category: { name: '外食' } },
];

const categoryTotals = purchaseHistories.reduce((acc, history) => {
  const category = history.category.name;
  if (!acc[category]) {
    acc[category] = 0;
  }
  acc[category] += history.amount;
  return acc;
}, {});

console.log(categoryTotals);
// { '外食': 250, '交際費': 50, 'Netflix': 100 }

ここで、初期値として空のオブジェクト{}を渡しているため、各カテゴリの合計金額を計算することができます。

実際に利用した場面

次に、実際にどのように使ったかを見てみましょう。

上記のreduceを使って集計したデータを元に、Rechartsで画像の様なパイチャートを描きます。 以下のコードをご覧ください:

'use client'
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'

const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']
const RADIAN = Math.PI / 180

interface CustomizedLabelProps {
  cx: number
  cy: number
  midAngle: number
  innerRadius: number
  outerRadius: number
  percent: number
  index: number
  name: string
  value: number
}

const renderCustomizedLabel = ({
  cx,
  cy,
  midAngle,
  innerRadius,
  outerRadius,
  percent,
  index,
  name,
  value,
}: CustomizedLabelProps) => {
  const radius = innerRadius + (outerRadius - innerRadius) * 0.5
  const x = cx + radius * Math.cos(-midAngle * RADIAN)
  const y = cy + radius * Math.sin(-midAngle * RADIAN)

  return (
    <text
      x={x}
      y={y}
      fill="white"
      textAnchor={x > cx ? 'start' : 'end'}
      dominantBaseline="central"
      fontSize="13px"
    >
      {`${name} ${(percent * 100).toFixed(0)}%`}
    </text>
  )
}

const purchaseHistories = [
  {id: 1, amount: 100 ,category: { name: '外食'}},
  {id: 2, amount: 50 ,category: { name: '交際費'}},
  {id: 3, amount: 100 ,category: { name: 'Netflix'}},
  {id: 4, amount: 150 ,category: { name: '外食'}},
]

const RechartPipeChart = () => {
  const groupByCategoryPurchaseHistories = () => {
    const categoryTotals = purchaseHistories.reduce(
      (acc: Record<string, number>, history) => {
        const category = history.category.name
        if (!acc[category]) {
          acc[category] = 0
        }
        acc[category] += history.amount
        return acc
      },
      {},
    )

    return Object.keys(categoryTotals).map((category) => ({
      name: category,
      value: categoryTotals[category],
    }))
  }

  const groupedData = groupByCategoryPurchaseHistories()

  return (
    <>
      <ResponsiveContainer width="100%" height={300}>
        <PieChart width={500} height={500}>
          <Pie
            data={groupedData}
            cx="50%"
            cy="50%"
            labelLine={false}
            label={({
              cx,
              cy,
              midAngle,
              innerRadius,
              outerRadius,
              percent,
              index,
            }) =>
              renderCustomizedLabel({
                cx,
                cy,
                midAngle,
                innerRadius,
                outerRadius,
                percent,
                index,
                name: groupedData[index].name,
                value: groupedData[index].value,
              })
            }
            outerRadius={150}
            fill="#8884d8"
            dataKey="value"
          >
            {groupedData.map((entry, index) => (
              <Cell
                key={`cell-${index}`}
                fill={COLORS[index % COLORS.length]}
              />
            ))}
          </Pie>
        </PieChart>
      </ResponsiveContainer>
    </>
  )
}

export default RechartPipeChart

ここでは、purchaseHistoriesreduceを使ってカテゴリごとに集計し、その結果をmapでRechartsが扱いやすい形に変換しています。これにより、購入履歴のデータを元にしたパイチャートを表示することができます。

まとめ

reduceは一見難しそうに見えますが、使い方を覚えるとデータを集計したり変換したりするのに非常に便利です。特に初期値をうまく設定することで、様々なデータの操作が可能になります。今回の例では、購入履歴のデータをカテゴリごとに集計してチャートに反映する方法をご紹介しましたが、他にも多くの場面で応用できますので、ぜひ試してみてください。

未経験からエンジニアに転職して質問に回答してみた

初めに簡単な自己紹介

最初に自己紹介 こんにちは!私は営業として3年働いた後、2ヶ月程プログラミングスクールに通い、事業会社への転職に成功しました。現在では正社員として2社の事業会社と受託開発で3年半程、フリーランスとして2年半程の経験があります。現在は週4日のフルリモートで月収75万円程で働いています。 週末は勉強、ゲーム、週末ワーケーションしたりしています。

よくある転職相談の内容に回答してみました

知り合いや、初めて知り合った人から転職相談をよく受けるので、よくある質問を下記にまとめてみたので、参考にしていただけると嬉しいです!

Q1. エンジニアとして向かなかったらどうだろうか? A. 向き不向きは正直あると思いますが、ロジカルシンキングができる人は転職時のストレスが少ないと思います。ただし、ロジカルシンキングは才能というよりも「ロジカルシンキングを求められる環境にいるか」が大事だと思います。今の仕事でロジカルシンキングが必要ないから身についていないだけの場合も多いです。エンジニアとして働けば、嫌でもロジカルシンキングは身についてきます。別の記事で仕事の進め方のテンプレートについて書く予定ですので、その流れで考える練習をすれば、誰でも一定レベルのロジカルシンキングは身につきますので安心してください。

Q2. 収入は上がるのだろうか? A. 上がると思います。ただし、エンジニアとしてどの業態で働くのか、何の言語を選択するのかによって、年収の上がるスピードには差があります。事前にしっかり調べておくことが大切です。業態としては「SES・受託・事業会社」の3つに大きく分けられます。また、言語はJava, Ruby, Pythonなど様々あり、言語によってプロダクトの規模感が変わるため、経験の浅いエンジニアが関わる領域や得られる経験も異なってきます。

Q3. 年齢の上限はありますか? A. 厳密な上限はないと思います。ただし、年齢が上がるにつれて、事前準備の負荷が増えることは確かです。具体的には、自分のスキルをアピールする作品集(ポートフォリオ)をしっかり作りこむことや、企業の面接対策をする必要があります。IT業界は新しい技術がどんどん出てくるため、新しいものを吸収する姿勢やスキルが求められます。年齢が上がると、新しいことの吸収速度が減少する傾向があるため、それを克服できれば理論上は年齢制限はないと言えます。

まとめ

エンジニアとしてのキャリアは、努力次第で誰でも成功できるものです。私自身も未経験から始めて、多くの挑戦を乗り越えながら成長してきました。エンジニアとして働くことで、自分のスキルを日々アップデートし、収入も上がり、働き方の自由度も高まります。興味がある方は、ぜひ一歩を踏み出してみてください。あなたの未来は、あなたの努力次第で無限に広がっています!

React Ant Design Formの使い方をまとめてみた

Ant Design Formでこれだけ抑えれば何とかなる知識をまとめました

この記事では、Ant Designのフォームコンポーネントの基本的な使い方について、実際のコード例とともに解説します。基本的な観点としては以下のポイントに注目します。

  1. 入力された最終的なformオブジェクトの取得方法
  2. formオブジェクトのカラムの指定方法
  3. バリデーションの制定方法

基本的なフォームの使い方

まずは、Ant Designのフォームを使った簡単なログインフォームの実装例を見てみましょう。このフォームでは、ユーザーの名前とメールアドレスを入力し、送信ボタンを押すことでフォームの値をコンソールに出力しています。 最終的な値をAPIにリクエストするのによく使うコードです。

'use client'
import { Flex, Form, Input, Button, Typography } from 'antd'

const SimpleForm = () => {

  const onFinish = (value) => {
    console.log('value', value)
    // value {name: '山田太郎', email: 'test@example.com'}
  }

  return (
    <Flex style={{ padding: 10, flexDirection: 'column', width: 300 }}>
      <Typography>ログインフォーム</Typography>
      <Form onFinish={onFinish}>
        <Form.Item name='name' rules={[{ required: true, message: '名前を入力してください' }]}>
          <Input placeholder='name'/>
        </Form.Item>
        <Form.Item name='email' rules={[{ required: true, message: 'メールアドレスを入力してください' }, { type: 'email', message: '有効なメールアドレスを入力してください' }]}>
          <Input placeholder='email'/>
        </Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form>
    </Flex>
  )
}

export default SimpleForm

入力された最終的なformオブジェクトの取得方法

フォームが送信されると、onFinish関数が呼び出され、入力された値がvalueオブジェクトとして渡されます。このオブジェクトには、フォーム内の各フィールドの名前をキーとした値が含まれます。

const onFinish = (value) => {
  console.log('value', value)
  // value {name: '山田太郎', email: 'test@example.com'}
}

解説

動きを細かく見ていきましょう

formオブジェクトのカラムの指定方法

各フォームフィールドはForm.Itemコンポーネントで定義され、nameプロパティでフィールド名を指定します。これにより、フォームが送信された際に、各フィールドの値がオブジェクトの対応するキーとして設定されます。

<Form.Item name='name'>
  <Input placeholder='name'/>
</Form.Item>
<Form.Item name='email'>
  <Input placeholder='email'/>
</Form.Item>

バリデーションの設定方法

rulesプロパティを使用して、各フィールドに対するバリデーションルールを指定します。ルールはオブジェクトの配列として渡され、各オブジェクトにはrequiredtypeなどのプロパティを設定できます。

<Form.Item 
  name='name' 
  rules={[{ required: true, message: '名前を入力してください' }]}>
  <Input placeholder='name'/>
</Form.Item>
<Form.Item 
  name='email' 
  rules={[
    { required: true, message: 'メールアドレスを入力してください' }, 
    { type: 'email', message: '有効なメールアドレスを入力してください' }
  ]}>
  <Input placeholder='email'/>
</Form.Item>

カスタムバリデーション

Ant Designのフォームでは、カスタムバリデーションを使用して特定の条件に基づいてフォームの入力を検証することができます。以下の例では、名前のフィールドに対して「田中」という名前は無効とするカスタムバリデーションを設定しています。

const SimpleForm = () => {

  const onFinish = (value) => {
    console.log('value', value)
  }

  const validateName = (rule, value) => {
    if (value && value.includes('田中')) {
      return Promise.reject('田中という名前は無効です');
    }
    return Promise.resolve();
  };

  return (
    <Flex style={{ padding: 10, flexDirection: 'column', width: 300 }}>
      <Typography>ログインフォーム</Typography>
      <Form onFinish={onFinish}>
        <Form.Item 
          name='name' 
          rules={[
            { required: true, message: '名前を入力してください' }, 
            { validator: validateName }
          ]}>
          <Input placeholder='name'/>
        </Form.Item>
        <Form.Item 
          name='email' 
          rules={[
            { required: true, message: 'メールアドレスを入力してください' }, 
            { type: 'email', message: '有効なメールアドレスを入力してください' }
          ]}>
          <Input placeholder='email'/>
        </Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form>
    </Flex>
  )
}

実務でよくあるユースケース

実際に開発現場でよくあるユースケースとしては指定の項目を入力すると他のフィールドを操作する事がよくあります。その場合の実装を見ていきましょう。

フォーム項目の依存関係操作

特定のフォーム項目が変更されたときに他のフォーム項目の値を操作するユースケースもよくあります。以下の例では、メールアドレスのフィールドが変更されたときに名前のフィールドの値をクリアします。

const SimpleForm = () => {
  const [form] = Form.useForm();

  const onFinish = (value) => {
    console.log('value', value)
  }

  const onEmailChange = (e) => {
    form.setFieldsValue({ name: '' });
  };

  return (
    <Flex style={{ padding: 10, flexDirection: 'column', width: 300 }}>
      <Typography>ログインフォーム</Typography>
      <Form form={form} onFinish={onFinish}>
        <Form.Item 
          name='name' 
          rules={[{ required: true, message: '名前を入力してください' }]}>
          <Input placeholder='name'/>
        </Form.Item>
        <Form.Item 
          name='email' 
          rules={[
            { required: true, message: 'メールアドレスを入力してください' }, 
            { type: 'email', message: '有効なメールアドレスを入力してください' }
          ]}>
          <Input placeholder='email' onChange={onEmailChange}/>
        </Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form>
    </Flex>
  )
}

export default SimpleForm

解説

  • Form.useForm: フォームインスタンスを作成するためのフックです。これにより、フォーム内の値や状態を操作できます。
  • onEmailChange関数: メールアドレスフィールドのonChangeイベントハンドラーです。このハンドラーで名前フィールドの値をクリアしています。

実際によくあるケースとしては、税別金額を入力したら、税率を掛けて税込金額のフィールドを操作するユースケースなどがよくあると思います

まとめ

これで、Ant Designのフォームコンポーネントを使った基本的な使い方、カスタムバリデーションの設定方法、およびフォーム項目の依存関係を操作する方法について解説しました。これらの基本を押さえておけば、基本的な実装は対応できる様になると思います。是非参考にしていただけると嬉しいです!

React Ant DesignのTableコンポーネントの使い方まとめてみた

はじめに

Ant DesignのTableコンポーネントを使えば、データの表示や管理が簡単にできます。今回は、todoリストのデータを表示するTableコンポーネントの基本的な使い方を紹介します。

ゴール

添付しているテーブルはよくあるものだと思います。実際にコードもと合わせて見ていきましょう。

最終的なコードも記載しておきます

"use client";
import moment from "moment";
import { Table, TableProps, GetProp, TablePaginationConfig } from "antd";

const data = [
  {
    id: 1,
    title: "todo1",
    description: "todo内容",
    is_completed: true,
    limite_date: "2024-05-01",
    category: { id: 1, name: "家事" },
  },
  {
    id: 2,
    title: "todo2",
    description: "todo2内容",
    is_completed: false,
    limite_date: "2024-04-25",
    category: { id: 1, name: "買い物" },
  },
  {
    id: 3,
    title: "todo3",
    description: "todo3内容",
    is_completed: true,
    limite_date: "2024-05-04",
    category: { id: 1, name: "家事" },
  },
];

const columns = [
  { title: "タスク名", dataIndex: "title", key: "title" },
  { title: "タスク詳細", dataIndex: "description", key: "description" },
  {
    title: "カテゴリ名",
    dataIndex: ["category", "name"],
    key: "name",
    filters: [
      {
        text: "家事",
        value: "家事",
      },
      {
        text: "買い物",
        value: "買い物",
      },
    ],
    onFilter: (value, record) =>
      record.category.name.indexOf(value as string) === 0,
  },
  {
    title: "完了",
    dataIndex: "is_completed",
    key: "is_completed",
    render: (_, record) => {
      return <>{record.is_completed == true ? "完了" : "未完了"}</>;
    },
    sorter: (a: any, b: any) => a.is_completed - b.is_completed,
  },
  {
    title: "期限日",
    dataIndex: "limite_date",
    key: "limite_date",
    sorter: (a: any, b: any) => moment(a.limite_date) - moment(b.limite_date),
  },
];

interface TableParams {
  pagination?: TablePaginationConfig;
  sortField?: string;
  sortOrder?: string;
  filters?: Parameters<GetProp<TableProps, "onChange">>[1];
}

const AntdDesignTable = () => {
  return <Table dataSource={data} columns={columns} />;
};

export default AntdDesignTable;

データについて

まず、表示するデータについて説明します。以下のように、todoリストのデータが配列に格納されています。各todoにはタイトル、詳細、完了状態、期限日、カテゴリーが含まれています。

const data = [
  {
    id: 1,
    title: "todo1",
    description: "todo内容",
    is_completed: true,
    limite_date: "2024-05-01",
    category: { id: 1, name: "家事" },
  },
  {
    id: 2,
    title: "todo2",
    description: "todo2内容",
    is_completed: false,
    limite_date: "2024-04-25",
    category: { id: 1, name: "買い物" },
  },
  {
    id: 3,
    title: "todo3",
    description: "todo3内容",
    is_completed: true,
    limite_date: "2024-05-04",
    category: { id: 1, name: "家事" },
  },
];

基本的な表示方法

Tableコンポーネントを使ってこのデータを表示します。dataSourceプロパティに表示したいデータを渡し、columnsプロパティに各列の設定を行います。各列は、dataIndexでデータのキーを指定し、keyプロパティで一意のキーを設定します。

カテゴリーに関しては、少し注意が必要でネスト構造になっているので配列形式で書いてあげる必要があります。

const columns = [
  { title: "タスク名", dataIndex: "title", key: "title" },
  { title: "タスク詳細", dataIndex: "description", key: "description" },
  {
    title: "カテゴリ名",
    dataIndex: ["category", "name"],
    key: "name",
    filters: [
      { text: "家事", value: "家事" },
      { text: "買い物", value: "買い物" },
    ],
    onFilter: (value, record) => record.category.name.indexOf(value) === 0,
  },
  {
    title: "完了",
    dataIndex: "is_completed",
    key: "is_completed",
    render: (_, record) => (record.is_completed ? "完了" : "未完了"),
    sorter: (a, b) => a.is_completed - b.is_completed,
  },
  {
    title: "期限日",
    dataIndex: "limite_date",
    key: "limite_date",
    sorter: (a, b) => moment(a.limite_date) - moment(b.limite_date),
  },
];

上記の設定を用いて、以下のようにTableコンポーネントを表示します。

import { Table } from "antd";
import moment from "moment";

const AntdDesignTable = () => {
  return <Table dataSource={data} columns={columns} />;
};

export default AntdDesignTable;

オプション機能

Tableコンポーネントには、便利なオプション機能がいくつかあります。

フィルター

filtersプロパティを使うと、特定の列に対してフィルターを設定できます。添付画像の様にフィルターしたい要素を選択して、データを絞り込む事が出来ます。方法は、onFilter関数でフィルターのロジックを定義します。

{
  title: "カテゴリ名",
  dataIndex: ["category", "name"],
  key: "name",
  filters: [
    { text: "家事", value: "家事" },
    { text: "買い物", value: "買い物" },
  ],
  onFilter: (value, record) => record.category.name.indexOf(value) === 0,
}

ソート

sorterプロパティを使うと、列のデータをソートできます。sorter関数でソートのロジックを定義します。

{
  title: "完了",
  dataIndex: "is_completed",
  key: "is_completed",
  sorter: (a, b) => a.is_completed - b.is_completed,
}

レンダリング

renderプロパティを使うと、データの表示方法をカスタマイズできます。例えば、is_completedフィールドを「完了」または「未完了」として表示します。

{
  title: "完了",
  dataIndex: "is_completed",
  key: "is_completed",
  render: (_, record) => (record.is_completed ? "完了" : "未完了"),
}

まとめ

Ant DesignのTableコンポーネントは、データの表示や管理に非常に便利です。基本的な表示方法から、フィルターやソート、カスタムレンダリングまで、様々な機能を活用して、より使いやすいテーブルを作成しましょう。以上、参考になれば幸いです!

Ant Designのレイアウトについて

背景

仕事でAnt Design(antd)のフレームワークを使っていて、共通レイアウトをどう実装すべきか調べてみました。

結論

結論から言うと、layoutディレクトリに共通レイアウトのコンポーネントを作成し、childrenで変更したい内容を追加するのが良いと思いました。これで各ページごとにレイアウトを統一しつつ、内容を柔軟に変えることができます。

ディレクトリ構造の例

/src
  /components
    /layout
      CommonLayout.tsx
  /pages
    index.tsx
    about.tsx

サンプル

この添付したサンプルのレイアウトは良くある構成だと思います ヘッダーがあり、サイドバー、フッターは基本変わらず、ピンクの箇所だけを変更したい場合で考えていきます。

まずは共通レイアウトのコードを作成します。/src/components/layout/CommonLayout.tsxに以下のコードを書いてみました。

'use client'
import { Layout } from "antd"

const { Header, Footer, Sider, Content } = Layout

const styles = {
  header: {
    background: 'yellow',
    height: '50px'
  },
  body:{
    height: 'calc(100vh - 100px)', 
  },
  sider: {
    background: 'blue',
  },
  content: {
    background: 'pink'
  },footer: {
    background: 'green',
    height: '50px',
    padding: 0,
    margin: 0
  }
}

const CommonLayout = ({ children } : { children : React.ReactElement}) => {

  return (
    <>
      <Header style={styles.header}>ヘッダー</Header>
      <Layout style={styles.body}>
        <Sider style={styles.sider} width={50}>
          Sider
        </Sider>
        <Content style={styles.content}>
          {children}
        </Content>
      </Layout>
      <Footer style={styles.footer}>
        Footer
      </Footer>
      
    </>
  )
}

export default CommonLayout

解説

構造について説明すると、CommonLayoutコンポーネントはAnt DesignのLayoutコンポーネントを使用して、以下のように構成されています:

  • Header: ページのヘッダー部分。ここにはサイトのタイトルやナビゲーションメニューを置くことが多い。
  • Sider: サイドバー。ここにはナビゲーションリンクやその他の補助的な情報を配置。
  • Content: メインのコンテンツエリア。ここに各ページごとの内容が入る。
  • Footer: フッター部分。著作権情報やリンクなどを配置するのが一般的。

stylesオブジェクトを使って、それぞれの部分のスタイルを定義している。これにより、レイアウトの見た目を一箇所で管理できるのが便利です。

layout.tsxについて

Next.jsではlayout.tsxというファイルが存在します。このファイルから共通のレイアウトを作成することができるのですが、共通のレイアウトを複数持ちたい場合に分岐などが発生してしまうので、今回のようにlayoutのコンポーネントを用意して、ラップしてあげる方が拡張性があると思いました。

個人的な経験としては、layout.tsxを修正するケースとして多いのは認証のチェックなどの場合に利用する印象が多いです。

まとめ

Ant DesignのLayoutコンポーネントを使って共通レイアウトを作成し、各ページごとに柔軟に内容を変えることができます。この方法を使うと、コードの再利用性が高まり、メンテナンスも楽になります。ぜひ参考にして、自分のプロジェクトにも取り入れてみてください!

Next.jsのディレクトリ構成についてまとめてみた

Next.jsのディレクトリ構成についてまとめてみた

はじめに

  • Next.js バージョン: 12.0.0 以上
  • App Routerを利用していること

背景

いくつかのプロジェクトを通じて、ディレクトリ構成が作業スピードやバグの発生率に与える影響を実感しました。分かりやすく整理された構成は、コードの読みやすさを向上させ、バグの発見と修正を迅速に行う手助けをしてくれます。そのため、今回はNext.jsプロジェクトに適したディレクトリ構成について紹介したいと思います。

ディレクトリ構成の種類

まずは、Reactプロジェクトでよく見られるディレクトリ構成を紹介します。この情報は、Luis Falconの記事から引用しています。

機能ベースの組織 (Feature-Based Organization)

/src
├── /auth
│   ├── AuthForm.tsx
│   ├── AuthAPI.ts
│   └── ...
├── /dashboard
│   ├── DashboardLayout.tsx
│   ├── DashboardWidgets.tsx
│   └── ...
├── /settings
│   ├── SettingsForm.tsx
│   ├── SettingsAPI.ts
│   └── ...
└── App.tsx

階層型(垂直型)組織 (Layered (Vertical) Organization)

/src
├── /components
│   ├── Header.tsx
│   ├── Sidebar.tsx
│   └── ...
├── /containers
│   ├── HomeContainer.tsx
│   ├── DashboardContainer.tsx
│   └── ...
├── /services
│   ├── ApiService.ts
│   ├── AuthService.ts
│   └── ...
└── App.tsx

ドメイン駆動設計 (Domain-Driven Design, DDD)

/src
├── /sales
│   ├── SalesDashboard.tsx
│   ├── SalesAPI.ts
│   └── ...
├── /inventory
│   ├── InventoryList.tsx
│   ├── InventoryService.ts
│   └── ...
└── App.tsx

アトミックデザイン (Atomic Design)

/src
├── /atoms
│   ├── Button.tsx
│   ├── Input.tsx
│   └── ...
├── /molecules
│   ├── LoginForm.tsx
│   ├── Navbar.tsx
│   └── ...
├── /organisms
│   ├── DashboardLayout.tsx
│   ├── UserProfile.tsx
│   └── ...
└── App.tsx

直感的にわかりづらい?

上記のディレクトリ構成の種類を紹介しましたが、これらのアプローチは少し極端で直感的に分かりにくいかもしれません。それぞれの方法に利点がありますが、実際のプロジェクトに適用するとなると少し混乱することもあります。

自分の意見:おすすめのディレクトリ構成

私が関わったプロジェクトでは、機能ベース、階層型、コンポーネントベースの組み合わせが最も効果的でした。具体的には、以下のような構成です。

/src
├── /types
│   ├── todo.ts
├── /context
│   ├── todoContext.tsx
├── /components
│   ├── /todos
│   │   ├── TodoForm.tsx
├── /hooks
│   ├── useTodoService.tsx
├── /todos
│   ├── /[id] 
│   │   ├── page.tsx
│   ├── page.tsx
└── App.tsx

この構成の理由

  • 機能ベースで整理: 機能ごとにディレクトリを分けることで、特定の機能に関連するファイルをまとめて管理でき、見つけやすくなります。
  • 階層型での整理: コンポーネントやフック、コンテキストなどを階層ごとに整理することで、既存の実装に習いやすく、コードの一貫性を保ちやすくなります。
  • コンポーネントベースの整理: 各コンポーネントを独立させることで、再利用性が高まり、テストやスタイルも一緒に管理できるようになります。

まとめ

最適なディレクトリ構成は、チームの開発方針やプロジェクトの特性によって変わります。一概に正解はありませんが、分かりやすく整理された構成は、作業効率の向上とバグの減少に寄与することは間違いありません。チームで話し合い、自分たちに最適な構成を見つけてください。