どんなことに関する記事?
先日、cloudflare docsのデザインをマネしてみよう、という取り組みを行っていました。
実装を始めた段階では、レスポンシブデザイン
を真似するところをメインに始めました。
それ以外の機能も実装してみようということで、
サイドバーのメニュー表示やパンくずリストも実装を行いました。機能を実現することに集中した結果、後で見返したときにこの実装はなんだ??と思うほど、機能だけ実現コード
になった、と思いました。
もう少しエンジニアらしく(というと偉そうですが)改修してみていいのでは?と思ったので、取り組んでみました。(機能を満たすだけでもそこそこ苦労しました。)
※コード情報は記事の最後に記載しています。
実際のページを見ていただくとわかりますが、目をつけるところは色々あります。
※以下リンクの画面左側に見えるメニューに関する話です。
■画面を操作してわかること
・今表示しているページの項目は背景色がオレンジ色の状態で表示される。(左端が濃くなっている部分の実現方法は調査中・・・)
・三角形のアイコンおよび、各項目の文字列にオンマウスすると、背景色がオレンジ色になる
・各項目が表示された行をクリックすると、そのページを表示できる
・三角形のアイコンをクリックすると、展開して、サブ項目が表示される(サブ項目のサブ項目くらいまでは表示されるので、3段階の表示になっており、階層が深いほど、インデントが下がった状態)
・サブ項目が展開される際、いきなり表示されず、少し時間をかけてヌルっとした感じで表示
どんなことに取り組んだのか
今回の上記メニューに関する機能を実現して(実現だけはそこそこはできている)、
かつある程度使いやすい(今後ページを追加したりする場合に、修正が行いやすい・実装量を少なくする←こちらがメイン)ものにすることを目指して作業を進めたつもりです。
以下2つのテーマを持って、修正作業をしてました。
- 再帰処理の実装
- インデントをつけたレイアウトの実現
chatGPTの力もお借りして
こんな見出しを付けましたが、まずは自分で戦いを挑みます。
(ああ、ぐちゃっとしてきた)ってなったら・・・chatgptに聞いてみて、助け船を出してもらう形で、作成を進めました。
特定の機能について教えて!っていう方法で用いる機会もあります。それ以外にも、どういったロジック進めれば、実現できるのかな~と悩んだときに、要件伝えて実現方法の参考になる情報がもらえるのは本当に助かるので、そういったところで活用できると、自分の実装の改善点と回答例をセットで確認できて、開発におけるスキルを向上できる気がしています。
再帰処理の実装
ある処理の中で、その処理を呼び出して、特定の条件に合致した場合は、処理を終了する、といった言い方になるでしょうか。数学の階乗の計算が一つの例ですね。(数学嫌いの方、ごめんなさい!)
5! = 5 * 4 * 3 * 2 * 1
コードに落とし込むと以下のようになります。
def factorial_recursive(num):
if num == 0:
return 1
else:
return num * factorial_recursive(num-1)
factorial_recursive(5)
この再帰処理をどこで使うかというと、
ある項目のサブ項目のサブ項目を表示する、といった画面の機能を実現するために使います。
フロントエンドの実装で画面の要素構成のために、再帰処理を用いたのは初めてですが、やっぱり素晴らしいなああと再認識しました。
画面の機能を整理
- ある項目がトグルか否か見てみる
- トグルでないは、その項目名を表示(←ここはなんてことない)
- トグルの場合は、三角形のアイコンと横並びでその項目名を表示し、三角形のアイコンをクリックすると、サブ項目が確認できるようにする。
- 3で確認できた各サブ項目について、1の作業を実施(←ここで再帰処理が登場する)
修正前の状況
修正してみた感想になりますが、
再帰処理することによって、どれだけ実装量自体が減らせるのか、また今後項目が追加されたときに、どれだけ修正量を抑えられるのか、痛感しました。
この話をしているのは、修正前の実装が以下のような状況だったからです。
// Path更新時
useEffect(() => {
if (props.pathname === '/queues') {
setIsOpenManage({ ...isOpenManage, overview: !isOpenManage.overview })
}
if (props.pathname === '/queues/examples') {
setIsOpenManage({ ...isOpenManage, examples: !isOpenManage.examples })
}
if (props.pathname === '/queues/learning') {
setIsOpenManage({ ...isOpenManage, learning: !isOpenManage.learning })
}
}, [])
// 三角形のアイコンクリック
const toggleOpen = (value: string) => {
if (value === '/queues/examples') {
setIsOpenManage({ ...isOpenManage, examples: !isOpenManage.examples })
}
if (value === '/queues/learning') {
setIsOpenManage({ ...isOpenManage, learning: !isOpenManage.learning })
}
};
えっ???これだけ見ても何の話ですか?ってなると思うので、冒頭に記載した話の以下の内容を実現するためのコードです。
■画面を操作してわかること
・今表示しているページの項目は背景色がオレンジ色の状態で表示される。(左端が濃くなっている部分の実現は帆方法は調査中・・・)
・三角形のアイコンをクリックすると、展開して、サブ項目が表示される(サブ項目のサブ項目くらいまでは表示されるので、3段階の表示になっており、階層が深いほど、インデントが下がった状態)
修正前の実装の場合、保守性が低い、といいますか。パスが追加されるたびに、この部分を修正することになります。。。
・対象のCommit情報
chatGPTへの質問と回答
ある程度、自分でやってみたので、今回の要件をchatGPTに伝えて、実装の手助けをしてもらうことにしました。
以下質問した内容です。
reactとtypescriptで実装中です。以下の要件を満たす実装を教えてください。
必要であれば再帰処理を用いてもいいです。
■トグルを用いたコンテンツ一覧の表示
コンテンツ一覧は縦に並べます。
各コンテンツ、サブコンテンツがある場合とない場合があります。
サブコンテンツがない場合は、そのままそのコンテンツ名を表示します。
サブコンテンツがある場合は、コンテンツ名に対して、トグル機能が付くようにして、
コンテンツ名をクリックすることで、開閉する形でサブコンテンツが確認できるようにする。
またサブコンテンツに対するサブコンテンツに対しても、同様に
サブコンテンツがない場合は、そのままそのコンテンツ名を表示します。
サブコンテンツがある場合は、コンテンツ名に対して、トグル機能が付くようにして、
コンテンツ名をクリックすることで、開閉する形でサブコンテンツが確認できるようにする。
この質問によって出力された結果を元に、実装を修正してみました。
コード内のコメントにもありますが、画面に表示する情報の構造を見直しました。
この見直しによって、求めている画面構成を実現しやすくなりました。
// 変数contentsの構造について見直しを行い、以下の形に収束
また、ToggleableContent
で再帰処理を実現しています。
実装を修正
import React, { useState } from 'react';
interface Content {
name: string;
subContents?: Content[];
}
// ToggleableContent:再帰処理を実装
const ToggleableContent: React.FC<Content> = ({ name, subContents }) => {
const [isOpen, setIsOpen] = useState(false);
const handleToggle = () => {
setIsOpen(!isOpen);
};
return (
<div>
<div onClick={handleToggle}>{name}</div>
{isOpen &&
subContents &&
subContents.map((subContent, index) => (
<ToggleableContent key={index} {...subContent} />
))}
</div>
);
};
const ContentList: React.FC = () => {
// 変数contentsの構造について見直しを行い、以下の形に収束
const contents: Content[] = [
{
name: 'Content 1',
subContents: [
{
name: 'Subcontent 1-1',
subContents: [
{ name: 'Subcontent 1-1-1' },
{ name: 'Subcontent 1-1-2' }
]
},
{ name: 'Subcontent 1-2' }
]
},
{
name: 'Content 2',
subContents: [
{ name: 'Subcontent 2-1' },
{ name: 'Subcontent 2-2' },
{
name: 'Subcontent 2-3',
subContents: [
{ name: 'Subcontent 2-3-1' },
{ name: 'Subcontent 2-3-2' }
]
}
]
},
{ name: 'Content 3' }
];
return (
<div>
{contents.map((content, index) => (
<ToggleableContent key={index} {...content} />
))}
</div>
);
};
export default ContentList;
インデントを整える
サブ項目は、メイン項目よりもインデントが下がった状態になっており、階層が深くなっても同じことが言えます。
サブ項目があるときには、三角形のアイコンの後に、項目名を表示します。
サブ項目がないときは、三角形のアイコン部分は何も表示せずで、項目名だけ表示します。
この形を実現する方法を聞いてみました。
修正前の状況
margin-left:XXXpx
やpadding-center:XXXpx
といった記述をXXX.tsx
ファイル内で行い、
インデントが下がるにつれて、XXXの値を大きくするような仕組みで実装していました。
しかし、インデントが思ったように機能しませんでした。原因はもう少し見てみる必要がありますが、一旦別の方法を検討することとしました。
chatGPTへの質問と回答
以下の質問を投げてみました。
reactとtypescriptで3段階のインデントを実現したいです。
マージンやパディングで実現を予定しています。実装を方法を教えてください。
またマージンやパディング以外で実現できる場合はその実装も教えてください。
回答を整理すると、以下の通りです。
cssファイルに、インデント調整のためのクラス(margin-left:XXXpx
)を定義して、使用したい要素に対して付与する実装方法でした。これをベースに実装すると、最初から整ったレイアウトになります。
以下のクラスをglobals.css
に定義しました。
.indent-level-1 {
margin-left: 20px;
}
.indent-level-2 {
margin-left: 40px;
}
.indent-level-3 {
margin-left: 60px;
}
実際のインデントを適用したい箇所で、以下のように実装します。
<div className="grid-container">
<div className="grid-item indent-level-1">Content</div>
<div className="grid-item indent-level-2">Content</div>
<div className="grid-item indent-level-3">Content</div>
</div>
修正を行ってみて
フロントエンドのコンポーネントを用いた開発における改修作業をこれまであまりやってきませんでした。
どういった実装が、その後も使いやすいコードであるか、これを考えながらやることの大切さを再認識できる機会となりました。
リポジトリ(最新のコミット)
※タイトルに「1週間があった」と少し前のような書き方をしましたが、コードを最後に修正したのは一月ほど前で、記事にまとまったこのタイミングで投稿させていただきました。