前置き
タイトルに書いてある通り、React + Typescriptでポートフォリオを作ってみました。
レスポンシブ対応とSPA(シングルページアプリケーション)対応を行っています。
この記事では公開までの過程を書いていきます。
完成品
PCサイズ
スマートフォン、タブレットサイズ
開発環境
- Windows 10 Pro
- Visual Studio Code
- Node.js:v8.12.0
- npm: v6.4.1
使ったパッケージ
- react: v16.6.0
- react-icons: v3.2.2
- react-router-dom: v4.3.1
- reactstrap: v6.5.0
- typescript: v3.1.3
- bootstrap: v4.1.3
- gh-pages: v2.0.1
きっかけ
AngularでWebチャットアプリを作ってみたので次はReactを触れてみたいと思っていました。
フロント未学習の大学生が1週間でVue.jsを使ったポートフォリオを作った話や大学生がReactで1日でポートフォリオを作った話を見て、自分もポートフォリオを作ろうと思い立って作りました。
導入編
create-react-app
をインストールする
以下のコマンドで、グローバルにインストールします。
npm install -g create-react-app
create-react-app
コマンドを使うことで、Reactアプリを簡単に作成することが出来ます。
create-react-app
コマンドでReactアプリを作成する
以下のコマンドで、Reactアプリの雛形を作成します。
create-react-app my-app --typescript
--typescript
オプションを付けることで、TypescriptでReactアプリを作成することが出来ます。
my-app
には、作成するアプリ名を入力してください。
コンポーネントやページを作成する際の拡張子はtsxになります。
(2018/12/5 追記)
create-react-appのバージョンアップに伴い、Typescriptを使用する際のオプションが変わりました。
実装編
TOPページを実装する
まずはReactアプリの根幹となるApp.tsx
のコードを以下に示します。
import * as React from 'react';
import './App.css';
import Backdrop from './Components/Backdrop/Backdrop';
import Navbar from './Components/Navbar/Navbar';
import SideDrawer from './Components/SideDrawer/SideDrawer';
interface ISideDrawerState {
isOpen: boolean;
}
class App extends React.Component<{}, ISideDrawerState> {
constructor(props: {}) {
super(props);
this.state = {
isOpen: false,
};
this.drawToggleClickHandler = this.drawToggleClickHandler.bind(this);
this.backdropClickHandler = this.backdropClickHandler.bind(this);
};
public render() {
let backDrop;
if (this.state.isOpen) {
backDrop = <Backdrop backdropClickHandler={this.backdropClickHandler} />;
}
return (
<div className="App">
<Navbar drawToggleClickHandler={this.drawToggleClickHandler} />
<SideDrawer show={this.state.isOpen} drawToggleClickHandler={this.drawToggleClickHandler} />
{backDrop}
</div>
);
}
private drawToggleClickHandler = () => {
this.setState((prevState) => {
return { isOpen: !prevState.isOpen };
});
};
private backdropClickHandler = () => {
this.setState({ isOpen: false });
};
}
export default App;
PropsとState
まず、class App extends React.Component<{}, ISideDrawerState>
の<{}, ISideDrawerState>
でPropsとStateを指定します。
PropsとStateは以下のようなものです。
- Props: 親コンポーネントから受け取る情報
- State: 自コンポーネントの状態
今回は最上位のコンポーネントなのでPropsは空を示す{}
、Stateはサイドバーの表示状態を示すisOpen
となります。
interface ISideDrawerState {
isOpen: boolean;
}
Typescriptで書く際は、PropsやStateを上記のようにインターフェイスとして定義しておくと管理がしやすいと思います。
constructor()
constructor(props: {}) {
super(props);
};
上記で、コンポーネントの初期化を行います。
super(props)
で親コンポーネントから送られた情報を受け取ります。
constructor
を書く際は必ずsuper(props);
を書きましょう。
それと個人的な経験則ですが、constructor
は省略可能ですがコンポーネント作成時に書くべきだと思います。
理由としては、コンポーネント初期化処理を一番意識するのはコンポーネント作成時であると実感したためです。
後から必要になった時、省略していた私は完全に存在を忘れてしばらく悩んでいました。
今回は他にも以下のことを行っています。
this.state = {
isOpen: false,
};
これは、Stateの初期化を行っています。ページを開いた時の初期状態ではサイドバーは閉じているので、false
と設定しています。
this.drawToggleClickHandler = this.drawToggleClickHandler.bind(this);
this.backdropClickHandler = this.backdropClickHandler.bind(this);
これは、App.tsx
の下部で定義していたメソッドのthis
をバインドしています。
バインドを行わなければ、イベントのコールバック関数として使用した時にthis
を使用することが出来ません。
render()
Typesciprtで定義する際は必ずpublic render()
と、アクセス修飾子を付けましょう。書かないとエラーが出ます。
render()
のreturn ()
内にページやコンポーネントの構成を記述していきます。
<SideDrawer show={this.state.isOpen} drawToggleClickHandler={this.drawToggleClickHandler} />
といったように、コンポーネントを追加していくことが出来ます。
show={this.state.isOpen}
とdrawToggleClickHandler={this.drawToggleClickHandler}
は、PropsとしてSideDrawerコンポーネントに値を送っています。
setState()
Steteの値を更新する時に使用します。
private drawToggleClickHandler = () => {
this.setState((prevState) => {
return { isOpen: !prevState.isOpen };
});
};
更新を行う前の状態を用いる際は、prevState
を使用します。
上記では、三本線アイコンがクリックされた時のサイドバーの表示非表示をトグルで処理しています。
SideNavBarを実装する
スマートフォン、タブレットサイズのような、ナビゲーションバーにある三本線アイコンをタップするとサイドバーがスライドする処理を実装します。
レスポンシブ対応を行うのでPCから見る時は、PCサイズのように三本線アイコンを非表示にしてナビゲーションバーにリンクを載せます。
実装に関しては、ReactJS - Build a Responsive Navigation Bar & Side Drawer Tutorialを参考にしました。(※英語です)
ナビゲーションバーを実装する
interface IProps {
drawToggleClickHandler(): void,
}
App.tsx
から送られてきたコールバック関数を受け取るので、インターフェイスでPropsの定義を行います。
定義したインターフェイスを基にconstructor(props: IProps)
で初期化を行います。
public render() {
return (
<header className="navbar" style={{ padding: 0 }}>
<nav className="navbar__navigation">
<div className="navbar__toggle-button" onClick={this.clickHandler}>
<IconContext.Provider value={{ color: "white", size: "1.5em" }}>
<MdMenu />
</IconContext.Provider>
</div>
<div>
<Link to="/" className="navbar__title">Portfolio</Link>
</div>
<div className="spacer" style={{ flex: 1 }} />
<div className="navbar__navigation-items">
<ul>
<Link to="/about">
<li>about</li>
</Link>
<Link to="/works">
<li>Works</li>
</Link>
<Link to="/skills">
<li>Skills</li>
</Link>
</ul>
</div>
</nav>
</header>
);
}
private clickHandler() {
this.props.drawToggleClickHandler();
}
App.tex
から受け取ったコールバック関数を実行するメソッドを作成し、三本線アイコンに対するクリックイベントに適用しています。
TSXでクラスを定義する時は、class
ではなくclassName
で指定します。
TSXでクリックなどのイベントを定義する時は、onClick
のようにキャメルケースで指定します。
style={{}}
で個別にスタイル定義を行っています。
理由はコンポーネントのcssファイルに記載した定義ではなく、Bootstrapのスタイル定義が反映されていたのでTSX内に定義して対応したためです。
レスポンシブ対応を行うため三本線アイコンとナビゲーションバーの各項目は、CSSの@media
で画面サイズに応じて表示非表示を切り替えています。
@media (max-width: 768px) {
.navbar__navigation-items {
display: none;
}
}
@media (min-width: 769px) {
.navbar__toggle-button {
display: none;
}
.navbar__title{
padding: 0 0rem;
}
}
サイドバーを実装する
Navbar.tsx
同様、App.tsx
からPropsを受け取りクリックイベントを設定しています。
public render() {
let drawerClasses = ['side-drawer'];
if (this.props.show) {
drawerClasses = ['side-drawer', 'open'];
}
return (
<nav className={drawerClasses.join(' ')}>
<div className="side-drawer__title-area">
<p className="side-drawer__title">Menu</p>
</div>
<ul>
<Link to="/about">
<li onClick={this.clickHandler}>About</li>
</Link>
<Link to="/works">
<li onClick={this.clickHandler}>Works</li>
</Link>
<Link to="/skills">
<li onClick={this.clickHandler}>Skills</li>
</Link>
</ul>
</nav>
);
}
App.tsx
からサイドバー表示状態を受け取って、サイドバーのクラス定義を切り替えています。
切り替える理由は、サイドバーの表示アニメーションをCSSアニメーションで実装するためです。
.side-drawer {
height: 100%;
background: white;
position: fixed;
top: 0;
left: 0;
width: 70%;
max-width: 300px;
z-index: 200;
transform: translateX(-100%);
transition: transform 0.3s ease-out;
}
.side-drawer.open {
box-shadow: 1px 0px 3px rgba(0, 0, 0, 0.5);
transform: translateX(0);
}
具体的には、z-index
で最前面に設定したサイドバーをtransform
とtransition
で左端から右へ移動させています。
オーバーレイの実装
public render() {
let backDrop;
if (this.state.isOpen) {
backDrop = <Backdrop backdropClickHandler={this.backdropClickHandler} />;
}
return (
<div className="App">
<Navbar drawToggleClickHandler={this.drawToggleClickHandler} />
<SideDrawer show={this.state.isOpen} drawToggleClickHandler={this.drawToggleClickHandler} />
{backDrop}
</div>
);
}
private backdropClickHandler = () => {
this.setState({ isOpen: false });
};
オーバーレイを構成するBackdropコンポーネントをサイドバーの表示状態に合わせて表示非表示を切り替えています。
オーバーレイ部分をタップするとサイドバーとオーバーレイを非表示にするため、クリックイベントを定義しています。
Backdropコンポーネントは以下のようになっています。
class Backdrop extends React.Component<IProps, {}> {
constructor(props: IProps) {
super(props);
this.clickHandler = this.clickHandler.bind(this);
};
public render() {
return (
<div className="backdrop" onClick={this.clickHandler}/>
);
}
private clickHandler() {
this.props.backdropClickHandler();
}
}
今までと同様に、親コンポーネントから受け取ったPropsを基に初期化処理とクリックイベントの定義を行っています。
そして、CSSは以下のようになっています。
.backdrop {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 100;
}
z-index
でページとサイドバーの間に定義したオーバーレイを画面全体に展開しています。
react-icons
ポートフォリオ内の三本線アイコン、GithubアイコンやtwitterアイコンはReact Iconsを使用しています。
導入方法
以下のコマンドで、パッケージをインストールします。
npm install react-icons --save
実装方法
例:Github Octicons iconsのGithub Iconを選んだ場合
import { GoMarkGithub } from 'react-icons/go';
class Example extends React.Component {
public render() {
return (
<GoMarkGithub />
);
}
}
- 公式ドキュメントから実装したいアイコンを選びます。
- 選んだアイコン名と選んだアイコンを含んでいるモジュールを基に、アイコン一覧上部にあるインポート文をコンポーネントに挿入します。
- 選んだアイコンをコンポーネントとして挿入します。
アイコンの色やサイズを変更する場合は以下のようになります。
ただし、React v16.3以上しか対応していません。
import { IconContext } from "react-icons";
import { GoMarkGithub } from 'react-icons/go';
class Example extends React.Component {
public render() {
return (
<IconContext.Provider value={{ color: "black", size: "3em" }}>
<FaTwitterSquare />
</IconContext.Provider>
);
}
}
アイコン用のインポート文に加えてimport { IconContext } from "react-icons";
を挿入し、アイコンコンポーネントをIconContext.Provider
コンポーネントで包みます。
そして上記のように、value={{}}
の{{}}
内に変更内容を記入します。
value={{}}
内に記入出来る内容は、公式のREADMEを参照してください。
react-router-dom
SPAでアプリを作成する際、アクセスしたURLによってどのページを開くかを制御するルーティングが必要があります。
Reactではルーティングをreact-router-domによって行います。
導入方法
以下のコマンドで、パッケージをインストールします。
npm install -S react-router-dom
実装方法
例を以下に示します。
import * as React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css';
import Navbar from './Components/Navbar/Navbar';
import About from './Pages/About/About';
import Home from './Pages/Home/Home';
import Skills from './Pages/Skills/Skills';
import Works from './Pages/Works/Works';
class App extends React.Component {
public render() {
return (
<Router>
<div className="App">
<Navbar />
<Switch>
<Route path="/about" component={About} />
<Route path="/works" component={Works} />
<Route path="/skills" component={Skills} />
<Route path="/" component={Home} />
<Route component={Home} />
</Switch>
</div>
</Router>
);
}
}
export default App;
ページ構成はTOPページとなるHomeと、About、Works、Skillsの4ページです。各々のページはコンポーネントとして作成されています。
<Router>
タグと<Route>
タグで、ページごとに作成したコンポーネントにルーティングを行っています。
ルーティングは<Route>
タグを降順でパスチェックし、マッチしたパスのコンポーネントを表示します。
<Switch>
タグを使用しない場合は降順でパスチェックし、マッチしたページを全て表示してしまうので注意が必要です。
加えて、パスチェックは部分一致で行われるので注意が必要です。
上記の例で言えば<Route path="/" component={Home} />
を一番上に書いていたら、AboutなどのページへアクセスしてもTOPページが表示されてしまいます。
パスチェックを完全一致で行うようにさせるには、exact={true}
を<Route>
タグに記入します。
<Route exact={true} path="/" component={Home} />
次は、各ページへのリンクを載せているナビゲーションバーを以下に示します。
import * as React from 'react';
import { Link } from "react-router-dom";
import './Navbar.css';
class Navbar extends React.Component {
public render() {
return (
<header className="navbar" style={{ padding: 0 }}>
<nav className="navbar__navigation">
<div>
<Link to="/MyPortfolio" className="navbar__title">Portfolio</Link>
</div>
<div className="spacer" style={{ flex: 1 }} />
<div className="navbar__navigation-items">
<ul>
<Link to="/about">
<li>about</li>
</Link>
<Link to="/works">
<li>Works</li>
</Link>
<Link to="/skills">
<li>Skills</li>
</Link>
</ul>
</div>
</nav>
</header>
);
}
}
export default Navbar;
<Route>
タグで指定した各ページへのリンクは<a>
タグではなく、<Link>
タグを使用します。
ReactでBootstrap 4を使用する
ReactでBootstrap 4を使用する記事を見かけたので、記事に書かれていた、reactstrapを導入しました。
導入方法
まずは、Bootstrap 4とreactstrapをインストールします。
npm install --save bootstrap reactstrap
次に、reactstrapの型定義をインストールします。
npm install --save-dev @type/reactstrap
最後に、index.tsx
にbootstrap/dist/css/bootstrap.css
をインポートするインポート文を挿入します。
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
<App />,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();
実装方法
今回は、CardとLayoutを使用しました。
使用した部分のソースを以下に示します。
public render() {
return (
<main className="main">
<h1 className="skills-page__title">Skills</h1>
<Container fluid={true} className="skills-page__container">
<Row>
<Col xs="12" md="4">
<Card className="skills-page__card">
<CardBody>
<CardTitle>HTML & CSS</CardTitle>
<CardText>
フロントエンドの制作で使用しています。
このポートフォリオやOrgaSoundではBootstrap 4を使用しています。
業務ではMaterial Design Liteを使用しています。
</CardText>
</CardBody>
</Card>
</Col>
<Col xs="12" md="4">
<Card className="skills-page__card">
<CardBody>
<CardTitle>SASS</CardTitle>
<CardText>
OrgaSoundの制作で使用しました。
業務で使うかはわかりませんが、使えると便利であると感じたので学習中です。
</CardText>
</CardBody>
</Card>
</Col>
<Col xs="12" md="4">
<Card className="skills-page__card">
<CardBody>
<CardTitle>javascript (jQuery)</CardTitle>
<CardText>
業務で使用しています。
主にES5以前の書式でjQueryと共に使用しています。
恐らく2番目に長く使用しています。
</CardText>
</CardBody>
</Card>
</Col>
</Row>
</Container>
</main>
);
}
<Container fluid={true}>
で画面幅に合わせたコンテナを生成します。
生成したコンテナに対して<Row>
タグを使用することで、コンテナを12分割してレイアウトを制御出来るようになります。
<Col>
タグで分割したレイアウトの配分を設定します。
画面幅によって、xs
やmd
に書かれた値でレイアウトの配分を設定されます。
xs
等については、Bootstrapの公式ドキュメントを参照してください。
Cardの部分に関しては、公式のカードサンプルを基に使わない部分を削って作成しました。
完成
npm start
でサーバを起動して、レイアウトや動作をチェックしたらいよいよ公開作業に入ります。
公開編
作成したポートフォリオは、きっかけに載せたポートフォリオを作成した2つの記事に書かれていたGitHub Pagesに公開することにしました。
Netlifyも候補の1つでしたが、GitHub Pagesの方が簡単に公開出来ると判断したためGitHub Pagesを採用しました。
GitHub Pagesでの公開先をユーザーページ(https://{username}.github.io/
)に設定しています。
前提
Github上にリポジトリを作成します。
ユーザーページを作成する際には、注意しなければならない点が2点あります。
- リポジトリ名は必ず**{username}.github.io**としてください。(
{username}
は自分のGithub ID) - 公開ファイルは必ずmasterブランチに配置しなければならない。
そのため私はsourceブランチを開発ソース用、masterブランチを公開用に分けています。
リモートのmasterブランチを消す
Github上でリポジトリを作成した際に生成されるリモートのmasterは、README.mdなど公開の際に不要なファイルが含まれているので消します。
開発ソースをsourceブランチにプッシュする
次の作業に必要なので、開発ソースをローカルで作ったsourceブランチからリモートのsourceブランチへプッシュします。
git checkout -b source
git push origin source
Default branchをsourceブランチに変更する
Githubのリポジトリを開いて、Settings
からBranches
を開きます。
画像のように、Default branchをsourceブランチに変更します。
リモートのmasterブランチを削除する
以下のコマンドで、リモートのmasterブランチを削除します。
git push -f --delete origin master
gh-pagesをインストールする
以下のコマンドで、gh-pagesをインストールします。
npm install gh-pages --save-dev
gh-pagesは、GitHub Pagesに簡単にデプロイすることが出来るパッケージです。
package.jsonに公開先のURLを指定する
以下の一行をpackage.json
に挿入します。
"homepage": "https://{username}.github.io",
{username}
は自分のGithub IDを入力してください。
package.jsonに「predeploy」「deploy」コマンドを追加
package.json
のscripts
に以下の2行を挿入します。
"predeploy": "npm run build",
"deploy": "gh-pages -d build -b master",
predeploy
コマンドはコンパイルを実行して、公開ファイルを生成します。デフォルトではbuildフォルダに生成されます。
deploy
コマンドは-d
で指定したディレクトリを、-b
で指定したブランチにプッシュします。
上記では-d
で公開ファイルが格納されているbuildフォルダを、-b
で公開対象のmasterブランチを指定しています。
デプロイする
以下のコマンドで、デプロイを実行します。
npm run deploy
デプロイに成功すると、Githubのリポジトリ上にmasterブランチが生成されています。
公開完了
package.json
のhomepage
に指定したURLにアクセスすると、作成したページが表示されます。
表示されない場合は、数分後にもう一度アクセスすると表示されると思います。
今後やりたいこと
- CSSで作ったのでSASSにしたい
PostCSSも候補の1つでしたが、個別に必要なパッケージをインストールするのは手間がかかると感じたのでSASSにしました。 - ナビゲーションバーをreactstrapで作り直したい
今回はチュートリアル動画の通りに自作しましたが、reactstrapでナビゲーションバーを作成出来ることに自作後に気付きました。 -
Steam APIでポートフォリオにSteamで所有しているゲーム一覧を載せたい
Web APIを活用してみたいと考えて私にとっては身近なSteamに目を付けたが、ポートフォリオに載せるものか迷ったので不採用としました。
感想
ポートフォリオを作成するのに約3日、この記事を書くのに約3日、とにかく大変でした。
しかし自分が作りたいものを作った後は、とても大きな達成感を得られました。
Reactに対する知見も得られて、苦労した甲斐がありました。
まとめ
参考資料は少ないですがReactで開発する際にTypescriptを使うと捗ると感じたので、この記事が少しでも皆さんのお役に立てば幸いです。