LoginSignup
26
43

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-29

前置き

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

完成品

roottool's Portfolio
ソースコード

PCサイズ

PC_Top.PNG

スマートフォン、タブレットサイズ

SmartPhone_Skills.PNG

開発環境

  • 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のコードを以下に示します。

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となります。

App.tsx
interface ISideDrawerState {
  isOpen: boolean;
}

Typescriptで書く際は、PropsやStateを上記のようにインターフェイスとして定義しておくと管理がしやすいと思います。

constructor()

App.tsx
constructor(props: {}) {
    super(props);
  };

上記で、コンポーネントの初期化を行います。
super(props)で親コンポーネントから送られた情報を受け取ります。
constructorを書く際は必ずsuper(props);を書きましょう。
それと個人的な経験則ですが、constructorは省略可能ですがコンポーネント作成時に書くべきだと思います。
理由としては、コンポーネント初期化処理を一番意識するのはコンポーネント作成時であると実感したためです。
後から必要になった時、省略していた私は完全に存在を忘れてしばらく悩んでいました。

今回は他にも以下のことを行っています。

App.tsx
    this.state = {
      isOpen: false,
    };

これは、Stateの初期化を行っています。ページを開いた時の初期状態ではサイドバーは閉じているので、falseと設定しています。

App.tsx
    this.drawToggleClickHandler = this.drawToggleClickHandler.bind(this);
    this.backdropClickHandler = this.backdropClickHandler.bind(this);

これは、App.tsxの下部で定義していたメソッドのthisをバインドしています。
バインドを行わなければ、イベントのコールバック関数として使用した時にthisを使用することが出来ません。

render()

Typesciprtで定義する際は必ずpublic render()と、アクセス修飾子を付けましょう。書かないとエラーが出ます。
render()return ()内にページやコンポーネントの構成を記述していきます。

App.tsx
<SideDrawer show={this.state.isOpen} drawToggleClickHandler={this.drawToggleClickHandler} />

といったように、コンポーネントを追加していくことが出来ます。
show={this.state.isOpen}drawToggleClickHandler={this.drawToggleClickHandler}は、PropsとしてSideDrawerコンポーネントに値を送っています。

setState()

Steteの値を更新する時に使用します。

App.tsx
  private drawToggleClickHandler = () => {
    this.setState((prevState) => {
      return { isOpen: !prevState.isOpen };
    });
  };

更新を行う前の状態を用いる際は、prevStateを使用します。
上記では、三本線アイコンがクリックされた時のサイドバーの表示非表示をトグルで処理しています。

SideNavBarを実装する

スマートフォン、タブレットサイズのような、ナビゲーションバーにある三本線アイコンをタップするとサイドバーがスライドする処理を実装します。
レスポンシブ対応を行うのでPCから見る時は、PCサイズのように三本線アイコンを非表示にしてナビゲーションバーにリンクを載せます。
実装に関しては、ReactJS - Build a Responsive Navigation Bar & Side Drawer Tutorialを参考にしました。(※英語です)

ナビゲーションバーを実装する

Navbar.tsx
interface IProps {
    drawToggleClickHandler(): void,
}

App.tsxから送られてきたコールバック関数を受け取るので、インターフェイスでPropsの定義を行います。
定義したインターフェイスを基にconstructor(props: IProps)で初期化を行います。

Navbar.tsx
    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で画面サイズに応じて表示非表示を切り替えています。

Navbar.css
@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を受け取りクリックイベントを設定しています。

SideDrawer.tsx
    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アニメーションで実装するためです。

SideDrawer.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で左端から右へ移動させています。

オーバーレイの実装

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

App.tsx
  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コンポーネントは以下のようになっています。

Backdrop.tsx
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.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を選んだ場合

Example.tsx
import { GoMarkGithub } from 'react-icons/go';

class Example extends React.Component {
    public render() {
        return (
            <GoMarkGithub />
        );
    }
}
  1. 公式ドキュメントから実装したいアイコンを選びます。
  2. 選んだアイコン名と選んだアイコンを含んでいるモジュールを基に、アイコン一覧上部にあるインポート文をコンポーネントに挿入します。
  3. 選んだアイコンをコンポーネントとして挿入します。

アイコンの色やサイズを変更する場合は以下のようになります。
ただし、React v16.3以上しか対応していません。

Example.tsx
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

実装方法

例を以下に示します。

App.tsx
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>タグに記入します。

App.tsx
<Route exact={true} path="/" component={Home} />

次は、各ページへのリンクを載せているナビゲーションバーを以下に示します。

Navbar.tsx
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をインポートするインポート文を挿入します。

index.tsx
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を使用しました。
使用した部分のソースを以下に示します。

Skills.tsx
    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を開きます。
BranchesSettings.PNG
画像のように、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に挿入します。

package.json
"homepage": "https://{username}.github.io",

{username}は自分のGithub IDを入力してください。

package.jsonに「predeploy」「deploy」コマンドを追加

package.jsonscriptsに以下の2行を挿入します。

package.json
    "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を使うと捗ると感じたので、この記事が少しでも皆さんのお役に立てば幸いです。

26
43
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
43