# React&TypeScriptで動的UIコンポーネントを作ってみよう! ## 1. 環境構築 1. [Sandbox](https://codesandbox.io/)のサイトに行きます。 2. ヘッダーのCreate Sandboxをクリックします。 3. React TypeScriptを選択します。以下の画面に遷移したらokです。 <img src="https://i.imgur.com/uIOdLdu.png" width="500px"/> 4. package.jsonを確認していただけると、ReactとTypeScriptがインストールされていることが確認できます。 <img src="https://i.imgur.com/vuLDKSu.png" width="500px"/> 5. 左メニューバーのAdd Dependencyをクリックして、react-ioniconsと入力します。 <img src="https://i.gyazo.com/417d3aba372787f37c42a4cfda1a9eb0.png" width="500px"/> 6. react-ioniconsをクリックすると、react-ioniconsのインストールが完了します。左メニューバーのDependenciesにreact-ioniconsが追加されていることが確認できます。 <img src="https://i.gyazo.com/f04f4a88381ff77e9f2557fd9f4f00f2.png" width="500px"/> 7. 同様の手順で、styled-componentsをインストールします。 8. 以上で環境構築は終了です。お疲れ様でした! ## 2. 動的UIコンポーネントの作成 1. FCとuseStateをreact.jsから名前付きインポートしてきます。FCとは、Reactの関数コンポーネントの型を表します。 ```tsx= // src/App.tsx import { FC, useState } from "react"; ``` 2. まず、useStateがどんな感じに機能するのかコードを書いて確認してみます。Appコンポーネント内で以下のコードを書きます。 ```tsx= // src/App.tsx import { FC, useState } from "react"; import "./styles.css"; export const App: FC = () => { const [state, setState] = useState(true); console.log(state); // この関数は何も返さないので、戻り値の方はvoid型を指定します。 // stateはtrue or falseのどちらかなので、stateの型はbooleanを指定します。 const handleState = (state: boolean): void => { setState(!state); }; return ( <div className="App"> <h1>Hello CodeSandbox</h1> { state ? <h2>Start editing</h2> : <h2>yarno lock</h2> } <button onClick={() => handleState(state)}>押すとh2が変わるよ</button> </div> ); }; ``` 3. Appコンポーネントを名前付きエクスポートしたので、index.tsxの中でAppコンポーネントを名前付きインポートに変更します。 ```tsx= // src/index.tsx import { render } from "react-dom"; import { App } from "./App"; const rootElement = document.getElementById("root"); render(<App />, rootElement); ``` 4. ボタンを押した時、以下のように変化していればokです。 <img src="https://i.gyazo.com/f6679cfffb8db2ea1c4b63a5f9adb3cc.gif" width="300px" /> 5. useStateの使い方も分かったところで、本題に入ります。まずは背景を作ります。bodyタグにスタイルを当てます。 ```tsx= // src/App.tsx // 省略 return ( <div className="App"> </div> ); // 省略 ``` ```css= /* style.css*/ .App { font-family: sans-serif; text-align: center; } /* bodyにスタイルを当てます。*/ body { /* 1st */ /* どんな端末で見ても、要素の高さが100vhより小さくならないように指定します。*/ min-height: 100vh; background-color: #10131c; /* 2nd */ display: flex; justify-content: center; align-items: center; } ``` 6. 次に黒い四角を作ります。 ```tsx= // src/App.tsx // 省略 return ( <div className="App"> {/* JSXでクラスを付与するなら、classNameというpropsを使います。*/} <div className="navigation"></div> </div> ); // 省略 ``` ```css= /* style.css*/ .navigation { /* 黒い四角の見た目 */ width: 70px; height: 70px; /* このrelativeを書くことで、黒い四角を子要素に指定したabsoluteの基準にすることができる*/ position: relative; background-color: #212532; border-radius: 10px; cursor: pointer; /* 位置 */ display: flex; justify-content: center; align-items: center; /*動き */ /* ある状態に変化する時間を0.5秒に指定します。*/ transition: 0.5s; } ``` ここまでの手順で、画面が以下の画像のようになっていると思います。 <img src="https://i.imgur.com/uQzOcR2.png" width="200px"/> 7. AppコンポーネントにisOpenというstateを持たせます。そして、黒い四角のclassNameに、isOpenがtrueならactiveを付与するというロジックを追加します。さらに、activeのstyleも作ります。 ```tsx= // src/App.tsx // 省略 <div className="App"> {/* JSXでクラスを付与するなら、classNameというpropsを使います。*/} <div className={`${isOpen && "active"} navigation`} onClick={() => (isOpen ? setIsOpen(false) : setIsOpen(true))} ></div> </div> // 省略 ``` ```css= /* style.css*/ /* 省略 */ /* クラスセレクタを連続で指定することもできます。*/ .navigation.active { width: 300px; height: 300px; } ``` ここまでの手順で、画面が以下のgif画像のようになっていると思います。クリックすると大きさが変わります。 <img src="https://i.gyazo.com/dd33a1d1e095be9bd2d2d33a7fcaae8a.gif" width="200px"/> 8. 次に9つの白い丸を作成します。9つの白い丸は黒い四角の中に配置します。その後、白い丸のスタイルも作成します。 ```tsx= // src/App.tsx // 省略 return ( <div className="App"> {/* JSXでクラスを付与するなら、classNameというpropsを使います。*/} <div className={`${isOpen && "active"} navigation`} onClick={() => (isOpen ? setIsOpen(false) : setIsOpen(true))} > <span className="dots aaa"></span> <span className="dots bbb"></span> <span className="dots ccc"></span> <span className="dots ddd"></span> <span className="dots fff"></span> <span className="dots ggg"></span> <span className="dots hhh"></span> <span className="dots iii"></span> </div> </div> ); // 省略 ``` ```css= /* style.css*/ /* 省略 */ /* .navigationというクラスを持つタグの配下にいるspanタグの、dotsというクラスセレクタを表しています。*/ .navigation span.dots { /* 白い丸の見た目 */ width: 7px; height: 7px; background: #fff; border-radius: 50%; /* 位置 */ position: absolute; display: flex; justify-content: center; align-items: center; /* 白い丸の動き */ /* calc()はCSSの関数です。 CSS のプロパティ値を指定する際に計算を行うことができます。*/ /* var()はCSSの関数です。CSS変数の値を参照できます。*/ /* cssのtransformとは、要素を2D変形や3D変形を行う事ができるCSSプロパティです。*/ /* transformプロパティでtranslateを指定すると、HTML要素を元の位置からXYZ軸方向に移動して表示する事ができます。*/ /* 以下の場合、transform: translate(X, Y)で白い丸の位置を移動させてます。*/ /* transitionでトランジションを適用するプロパティとそのプロパティが完全に実行完了になるまでの時間を指定しています。*/ /* transitionを指定することで、ある状態からある状態になるまでのアニメーションの速度を操作できます。*/ /* transition-delayは、トランジション効果が開始されるまでの時間を表します。*/ transform: translate(calc(12px * var(--x)), calc(12px * var(--y))); transition: transform 0.5s, width 0.5s, height 0.5s, background 0.5s; transition-delay: calc(0.1s * var(--i)); } ``` ここまでの手順で、画面が以下の画像のようになっていると思います。css変数を定義していないので、9つの白い丸が全て中心に位置していることが検証ツールから分かります。 <img src="https://i.imgur.com/hElkq5J.png" width="200px"> 9. スタイルシートにcss変数を定義します。定義したことでtranslateが機能して、白い丸が中心から移動します。 ```css= /* style.css*/ /* 省略 */ /* y方向は、下向きが正を表します。*/ /* あるセレクタにおける変数を定義したい場合、以下のように定義します。*/ /* --xのxは変数名です。*/ span.dots.aaa { --i: 0; --x: -1; --y: 0; } span.dots.bbb { --i: 1; --x: 1; --y: 0; } span.dots.ccc { --i: 2; --x: 0; --y: -1; } span.dots.ddd { --i: 3; --x: 0; --y: 1; } span.dots.eee { --i: 4; --x: 1; --y: 1; } span.dots.fff { --i: 5; --x: -1; --y: -1; } span.dots.ggg { --i: 6; --x: 0; --y: 0; } span.dots.hhh { --i: 7; --x: -1; --y: 1; } span.dots.iii { --i: 8; --x: 1; --y: -1; } .navigation span.dots { /* 省略 */ ``` <img src="https://i.imgur.com/Vwe3tk3.png" width="200px" /> 10. onClick時に、白い丸にアニメーションを追加してみます。まず、App.tsxでreact-ioniconsからUIコンポーネントをインポートします。その後、spanタグの中に書きます。さらに、白い丸のアニメーションを制御するような変数をコンポーネント内に定義します。 ```tsx= // src/App.tsx // 省略 // アイコンのUIコンポーネントを名前付きインポートします。 import { AirplaneOutline, AlarmOutline, AnalyticsOutline, AppsOutline, BagAddOutline, BasketOutline, BeerOutline, BuildOutline, CameraOutline } from "react-ionicons"; export const App: FC = () => { const [isOpen, setIsOpen] = useState(false); // 白い丸のアニメーションを制御する値 // isOpenの値によって、変化します。 const hoverWhite: string = `${isOpen ? "#fff" : ""}`; const style: any = { transition: "0.5s" }; const width: string = `${isOpen ? "1.35em" : "0em"}`; const height: string = `${isOpen ? "1.35em" : "0em"}`; return ( <div className="App"> {/* JSXでクラスを付与するなら、classNameというpropsを使います。*/} <div className={`${isOpen && "active"} navigation`} onClick={() => (isOpen ? setIsOpen(false) : setIsOpen(true))} > <span className="dots aaa"> <CameraOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots bbb"> <BuildOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots ccc"> <AirplaneOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots ddd"> <AlarmOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots eee"> <AnalyticsOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots fff"> <BagAddOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots ggg"> <AppsOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots hhh"> <BasketOutline style={style} width={width} height={height} color={hoverWhite} /> </span> <span className="dots iii"> <BeerOutline style={style} width={width} height={height} color={hoverWhite} /> </span> </div> </div> ); // 省略 ``` ここまでの手順で、画面が以下のgif画像のようになっていると思います。クリックするとアイコンが出現します。理由は、widthとheightに0より大きい値が指定されたからです。isOpenがtrueだと、widthとheightに13.5emが指定されています。 <img src="https://i.gyazo.com/bf3b6930f0b530503ba38bce387e1f2d.gif" width="200px" /> 11. isOpenがtrue時の白い丸にスタイルを当てます。 ```css= /* 省略 */ /* activeが付与されている時、dotsのスタイルは、このスタイルで上書きされます。*/ .navigation.active span.dots { /* 見た目 */ width: 45px; height: 45px; background: #333849; /* 動き */ transform: translate(calc(90px * var(--x)), calc(90px * var(--y))); } /* アイコンのUIコンポーネントは、svgタグとして出力されます。*/ /* svgタグのhover時に対してスタイルを当てます。*/ svg:hover { color: #2dfc52; transition: color 1s; /* filterプロパティを使うと、画像の彩度、明度、コントラスなどを変更したり、ぼかしを加えたりすることが可能です。*/ /* filterプロパティを使って、drop-shadowを重ね掛けしています。*/ /* filter:drop-shadow()は表示されている(見えている)ものだけに影を落とします。*/ filter: drop-shadow(0 0 2px #2dfc52) drop-shadow(0 0 5px #2dfc52) drop-shadow(0 0 15px #2dfc52); } ``` アイコンのwidhtとheightはemで指定しているので、各々の白い丸が大きくなると、それに応じてアイコンも大きくなります。 完成です!お疲れ様でした! <img src="https://i.gyazo.com/6dcd89f83a2760a408b1041203ce49b1.gif" width="200px" /> ## 3. リファクタリング Atomic Design(UIコンポーネントの粒度をカテゴリー毎に分ける手法)に沿って、Appコンポーネントを細分化します。細分化することによって、各コンポーネントの責務が明確になります。そして、再利用したり、コードの可読性の向上も期待できます。 以下のSandBoxでリファクタリングをしているので、参考にしていただければと思います。 https://codesandbox.io/s/nine-dots-on-react-ts-gdsq0o?file=/src/App.tsx ## 4. 超細かい知識 ### (Q1)なぜ、useStateをデフォルトインポートではなく、名前付きインポートをしてるのか? 以下の記事が参考になりました。 [HooksのuseStateがどのように実現されているか調べた話 ](https://qiita.com/yamazaki_sensei/items/c5dc2dbf148ffa4dab52) react/packages/react/src/React.jsファイル内で、useStateが名前付きエクスポートされています。そのため、他のファイルでuseStateを利用するときは名前付きインポートをします。 - [React.jsの95行目](https://github.com/facebook/react/blob/v17.0.1/packages/react/src/React.js#L95) ```js= // react/packages/react/src/React.js export { // 省略 useState, // 省略 }; ``` useState自体は、react/packages/react/src/ReactHooks.jsで定義されています。 - [ReactHooks.js](https://github.com/facebook/react/blob/v17.0.1/packages/react/src/ReactHooks.js#L80) ```js= // react/packages/react/src/ReactHooks.js export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } ``` ReactHooks.jsでエクスポートされたuseStateは、React.jsでインポートされています。 ```js= // react/packages/react/src/React.js import { // 省略 useState, // 省略 } from './ReactHooks'; ``` 以上のことをまとめると、useStateはReactHooks.jsで定義、名前付きエクスポートされており、React.jsでuseStateを名前付きインポートして、また名前付きエクスポートしていることが分かりました。 ### (Q2)なぜexport default constと書くとエラーになる? 以下は[ECMAScript](https://262.ecma-international.org/6.0/#sec-exports-static-semantics-boundnames)に書いてあるexport defaultの仕様です。 <img src="https://i.imgur.com/qQTnR0i.png" width="500px"> 以上の内容から、defaultの後に続くキーワードは、function宣言、クラス宣言、代入式であることが分かります。したがって、defaultの後ろにconstを書くとシンタックスエラーが起こることが理解できました。 ## 5. 参考記事 - [React.FC使う必要ある?](https://qiita.com/islandryu/items/6477ff6a3a9d74a7cc8c) - [Why Is `Export Default Const` invalid?](https://stackoverflow.com/questions/36261225/why-is-export-default-const-invalid) - [ES6 Module export default syntax](https://stackoverflow.com/questions/21344720/es6-module-export-default-syntax) - [JavaScriptに密かに存在する“無名関数宣言”](https://zenn.dev/uhyo/articles/anonymous-function-declaration#%E4%BB%95%E6%A7%98%E6%9B%B8%E3%81%A7%E7%A2%BA%E3%81%8B%E3%82%81%E3%82%8B) - [代入式 (assignment expression)](https://e-words.jp/w/%E4%BB%A3%E5%85%A5%E6%BC%94%E7%AE%97%E5%AD%90.html#:~:text=%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E5%A4%9A%E3%81%84%E3%80%82-,%E4%BB%A3%E5%85%A5%E5%BC%8F%20(assignment%20expression),%E3%81%A0%E3%82%8A%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%82) - [Reactでの命名を考える](https://zenn.dev/kodai/articles/5fe454d436bf3e) - [関数](https://future-architect.github.io/typescript-guide/function.html) - [セレクタ(要素)を繋げて記述](https://wp-p.info/tpl_rep.php?cat=css-biginner&fl=r10) - [このエラーの解決方法 Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>](https://qiita.com/k_kazukiiiiii/items/e1c45c532b8baeb0c888) - [【CSS】max-widthとmin-widthの使い方まとめ](https://saruwakakun.com/html-css/basic/max-min-width#section3) - [CSS文法の基礎 classセレクタ](https://wp-p.info/tpl_rep.php?cat=css-biginner&fl=r10) - [transitionを使ってエフェクトをつけてみる](http://markup.webcrew.co.jp/art_20150318.html) - [calc()](https://developer.mozilla.org/ja/docs/Web/CSS/calc()) - [var()](https://developer.mozilla.org/ja/docs/Web/CSS/var()) - [【CSS】transition-delayの使い方、効果の開始時間を適用させる](https://shu-naka-blog.com/css/transition-delay/) - [className](https://reactjs.org/docs/faq-styling.html) - [CSS トランジションの使用](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) - [CSS transformプロパティとは?意味や使い方について紹介!](https://owl-view.com/css/2426/) - [filter](https://code-kitchen.dev/css/filter/) - [filter:drop-shadow()を複数適用する](http://doshiroutonike.com/web/css-web/4033) - [CSS filterで複数の効果](https://bitto.jp/posts/%E6%8A%80%E8%A1%93/css/filter/multi-effect/) - [CSSの「box-shadow」と「filter:drop-shadow」の違いを覚えたらちょっとテンション上がった](https://kuzlog.com/2017/03/09/1255/) - [Atomic Designを使ってReactコンポーネントを再設計した話](https://blog.spacemarket.com/code/atomic-design%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6react%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E5%86%8D%E8%A8%AD%E8%A8%88%E3%81%97%E3%81%9F%E8%A9%B1/)