BETA

React + Typescriptでポートフォリオを作ってみた

投稿日:2018-10-31
最終更新:2018-10-31
※この記事は外部サイト(https://qiita.com/roottool/items/89b4adcea...)からのクロス投稿です

前置き

タイトルに書いてある通り、React + Typescriptでポートフォリオを作ってみました。
レスポンシブ対応とSPA(シングルページアプリケーション)対応を行っています。 この記事では公開までの過程を書いていきます。

完成品

roottool's Portfolio
ソースコード

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 --scripts-version=react-scripts-ts my-app

--scripts-version=react-scripts-tsオプションを付けることで、TypescriptでReactアプリを作成することが出来ます。
my-appには、作成するアプリ名を入力してください。
コンポーネントやページを作成する際の拡張子はtsxになります。

実装編

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で最前面に設定したサイドバーをtransformtransitionで左端から右へ移動させています。

オーバーレイの実装


この画像の右側にある、黒い半透明部分を実装します。

  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 />
        );
    }
}
  1. 公式ドキュメントから実装したいアイコンを選びます。
  2. 選んだアイコン名と選んだアイコンを含んでいるモジュールを基に、アイコン一覧上部にあるインポート文をコンポーネントに挿入します。
  3. 選んだアイコンをコンポーネントとして挿入します。

アイコンの色やサイズを変更する場合は以下のようになります。
ただし、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.tsxbootstrap/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();

実装方法

今回は、CardLayoutを使用しました。 使用した部分のソースを以下に示します。

    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 &amp; 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>タグで分割したレイアウトの配分を設定します。
画面幅によって、xsmdに書かれた値でレイアウトの配分を設定されます。
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.jsonscriptsに以下の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.jsonhomepageに指定したURLにアクセスすると、作成したページが表示されます。
表示されない場合は、数分後にもう一度アクセスすると表示されると思います。

今後やりたいこと

  • CSSで作ったのでSASSにしたい
    PostCSSも候補の1つでしたが、個別に必要なパッケージをインストールするのは手間がかかると感じたのでSASSにしました。
  • ナビゲーションバーをreactstrapで作り直したい
    今回はチュートリアル動画の通りに自作しましたが、reactstrapでナビゲーションバーを作成出来ることに自作後に気付きました。
  • Steam APIでポートフォリオにSteamで所有しているゲーム一覧を載せたい
    Web APIを活用してみたいと考えて私にとっては身近なSteamに目を付けたが、ポートフォリオに載せるものか迷ったので不採用としました。

感想

ポートフォリオを作成するのに約3日、この記事を書くのに約3日、とにかく大変でした。
しかし自分が作りたいものを作った後は、とても大きな達成感を得られました。
Reactに対する知見も得られて、苦労した甲斐がありました。

まとめ

参考資料は少ないですがReactで開発する際にTypescriptを使うと捗ると感じたので、この記事が少しでも皆さんのお役に立てば幸いです。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@roottoolの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!