React.useState

お待たせしました!(?)
連載企画(?)の第一弾です。
これは当ブログのUIを作ってReactHooksのAPIを学んでいく企画です!

なぜこれを書くか

私自身もReactを触り始めて数ヶ月ほどの初心者に毛が生えたレベルですが、Reactを一から勉強するに当たって、「このReactの機能は実務でどう使うのか?」がイメージできずに苦労してきました。

Reactの機能を詳しく紹介していて分かりやすい記事はありがたいことにこの世にたくさんあるのですが、実際にUIやサービスを作りながらという記事はあまり見かけません。
私自身エンジニアではないということもあり、同じように実務でなかなかReactに触れる機会があまりない人のために記事を書いていこうと思いました。

とは言いつつも、自分のためでもあります。特にReactのv16.8~にでたReactHooksを用いてUIを作成していく力を身に付けたかったのでアウトプットも兼ねて連載をしていこうと思います。
以下の記事にインスピレーションを受けました。
React Hooksで作るGUI

今回のテーマ

というわけで、今回のテーマはuseStateです。
UIを作成しながら、useStateの使い方について学んでいきましょう。
UIはハンバーガーメニューを作成していきたいと思います。
当ページのスマホページではUIとしてハンバーガーメニューを使用しています。

RyusouBlogのスマホUI

ハンバーガーメニューをクリックすると、アニメーションが発火し、ドロワーが開く。
RyusouBlogのスマホUI(ドロワー開)

このUIは以下のページで紹介されているコードを参考にさせていただいています。
アクセシビリティにも配慮されたハンバーガーメニューになります。
ハンバーガーボタン 何で作ってる?僕なりの作り方を解説してみる。

詳しくは元ページを参照していただければと思いますが、簡単に仕様について説明します(当ブログのスマホページでぜひ実際にお試しください)。
ドロワーの開閉の状態を切り替える方法は様々な方法がありますが、上記のページを参考にして、aria-expandedの状態を切り替えることによってハンバーガーメニューの開閉を行なっています。

//buttonにaria-expanded属性を付与する
//今回作成するハンバーガーメニューのコンポーネント
  return (
    <>
        <button
          type="button"
          className="button hamburger"
          aria-controls="global-nav"
          aria-expanded={open}
          onClick={() => setOpen(!open)}
        >
          <span className="hamburgerLine">
            <span className="visuallyHidden">
              メニューを開閉する
            </span>
          </span>
        </button>
    </>
  );


//nav要素にaria-expanded属性を付与する
//今回作成するハンバーガーメニューを押した時に開くNavコンポーネント
  return (
      <nav aria-expanded={open}>
        <ul>
          <li>
            <Link
              to="/"
              aria-label="HOME"
              onClick={() => setOpen(!open)}
            >
              <ul>
                <li className="NavListIcon">
                  <Img
                    fixed={data.file.childImageSharp.fixed}
                    alt="Logo"
                  />
                </li>
                <li>HOME</li>
              </ul>
            </Link>
          </li>
          <li>
            <Link
              to="/about"
              aria-label="ABOUT"
              onClick={() => setOpen(!open)}
            >
              <ul>
                <li className="NavListIcon">
                  <FaReact size={36} />
                </li>
                <li>ABOUT</li>
              </ul>
            </Link>
          </li>
          <li>
            <Link
              to="/posts"
              aria-label="POSTS"
              onClick={() => setOpen(!open)}
            >
              <ul>
                <li className="NavListIcon">
                  <TiPencil size={36} />
                </li>
                <li>POST</li>
              </ul>
            </Link>
          </li>
          <li>
            <Link
              to="/works"
              aria-label="WORK"
              onClick={() => setOpen(!open)}
            >
              <ul>
                <li className="NavListIcon">
                  <MdWork size={36} />
                </li>
                <li>WORK</li>
              </ul>
            </Link>
          </li>
          <li>
            <Link
              to="/contacts"
              aria-label="CONTACTS"
              onClick={() => setOpen(!open)}
            >
              <ul>
                <li className="NavListIcon">
                  <FiMail size={36} />
                </li>
                <li>CONTACT</li>
              </ul>
            </Link>
          </li>
        </ul>
      </nav>
  );
)


逆に言えば上記のコードを読んで実装をイメージできた人は読む必要はあまりありません(React初心者の人にぜひ宣伝お願いします笑)。
ちなみに私はReactの静的サイトジェネレーターであるGatsbyを使用して実装していますが、Reactでも問題なく動作するかと思います(Navコンポーネント内のページネーションはReact Routerなどの実装が必要になるかと思いますが、当記事ではUIを作成するのが目的なのでそこには触れません)。

準備

それでは実際にやっていきましょう。
プロジェクトをスタートさせます。
create-react-appgatsby newなどで雛形を用意してください。
なお、サンプルコードはTypeScriptで書いています。

フォルダ構成

src
 - components
  - Atom
    - Burger
	  index.tsx(ハンバーガーメニューのコンポーネント)
    - Nav
      index.tsx(Navメニューのコンポーネント)
  - Layout
    layout.tsx(Burger,Navコンポーネントをまとめるコンポーネント)

//Layoutファイルでルートのファイルをラップする
  - App.js(create-react-appの場合)
  - gatsby-browser.js(Gatsbyの場合)

AtomicDesignに一部沿ってフォルダを分割します。

  • Atom(各コンポーネントファイル)
  1. Burger/index.tsx(ハンバーガーメニュー)
  2. Nav/index.tsx(Nav)
  • Layout(AtomicDesignでいうところのtemplates?)


このあたりは適当にファイル分割しています。Reactのコンポーネント指向というやつですが、私はそんなにカチカチにAtomicDesignの原則に添わずに小さいcomponentはAtomにぶっ込んで、ページを構成するコンポーネントはtemplatesに残りはぶっ込むという方針でフォルダを分けています。また、Layoutは別のフォルダにしています。

Layoutコンポーネント

Layoutコンポーネントでアプリのルートをラップして、全てのファイルに適用されるようにしましょう。以下はgatsbyの例になります。

//gatsby-browser.js
import React from 'react';
import { Layout } from './src/components/layout';

export const wrapPageElement = ({ element }) => {
  return <Layout>{element}</Layout>;
};



これで準備は完了です。


useStateとは?

今回使用するuseStateについて紹介したいと思います。
公式:React-ステートフックの利用法

フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。


以下例です(公式を参照しました)。

import React, { useState } from 'react';
 
   function Example() {
     const [count, setCount] = useState(0);
 
     return (
       <div>
         <p>You clicked {count} times</p>
         <button onClick={() => setCount(count + 1)}>
          Click me
        </button>
      </div>
    );
 }



例のように関数コンポーネントにstate(状態)を持たせたい時に使用します。

const [count, setCount] = useState(0);


useStateを宣言すると、2つの値が渡されるので鍵かっこ内に変数を宣言します。例だとカウンターなのでcountという変数を宣言しています。
useStateの引数である(0)は初期値を表します。今回は0を与えています。
呼び出す時は以下のように呼び出します。thisを使うことなく呼び出せるので直感的に記述できるのがポイントです。

<p>You clicked {count} times</p>


新たな値を与えたい時は、2つ目の値であるsetCountを呼び出します。例ではcountに+1を足して値を返しています。

<button onClick={() => setCount(count + 1)}>


このようにReact.useStateを使うと関数コンポーネント内でstateを管理することができるのでとてもスマートに書くことができます。

Layout(親コンポーネント)にStateを宣言

では実際にやっていきましょう。
前章で述べたように関数コンポーネントにStateを宣言していきます。まずは親コンポーネントであるLayout/index.tsxにState(状態)を宣言していきましょう。

//Layout/index.tsx
import React, { useState, ReactNode } from 'react';

import { Nav } from '../Atom';
import { Burger } from '../Atom';

type Props = {
  children: ReactNode;
};

export const Layout: React.FC<Props> = ({ children }) => {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Burger open={open} setOpen={setOpen} />
      <Nav open={open} setOpen={setOpen} />
      <div>
        <main>{children}</main>
      </div>
    </>
  );
};


ReactとTypeScript

今回のサンプルコードはTypeScriptを用いています。ReactでTypeScriptを用いると例のようにPropsに型を与えることでpropsに型が適用されます。

type Props = {
  children: ReactNode;
};

export const Layout: React.FC<Props> = ({ children }) => {


この例では、childrenにReactNodeという@type/reactが持っている型を渡しています。
このようにReactとTypeScriptの相性はとても良いので導入しない手はありません。

useStateで状態を宣言

  const [open, setOpen] = useState(false);


前章にしたがってuseStateを宣言します。ドロワーが開くので関数名はopenとしました。初期値には真偽値のfalseを入れます。使用通りこのStateがtrueに切り替わるのと同時にアニメーションするようにします。

Stateを子コンポーネントに共有

Layoutコンポーネントからみて子コンポーネントにあたるBurger,NavコンポーネントにStateの値をします。
Burger,Navコンポーネントは後ほど作成します。

<Burger open={open} setOpen={setOpen} />
 <Nav open={open} setOpen={setOpen} />


現在の状態は以下のようにopenとsetOpenという2つのstateが渡されている状態になります。

Layoutのopen,setOpenがNav,Burgerコンポーネントに共有されている


このようにstateを簡単に共有できます。

Burgerコンポーネントを作成する

ハンバーガーメニューを表示するBurgerコンポーネントを作成していきます。button要素でUIを構成していきます。
ポイントはaria-expanded属性を要素に渡すことです。これによって要素が当たっていることが分かりやすくなります。

//Burger/index.tsx
import React from 'react';

type Props = {
  open: boolean;
  setOpen: Function;
};

export const Burger: React.FC<Props> = ({
  open,
  setOpen,
}) => {
  return (
    <>
      <div css={hamburger}>
        <button
          type="button"
          className="button hamburger"
          aria-controls="global-nav"
          aria-expanded={open}
          onClick={() => setOpen(!open)}
        >
          <span className="hamburgerLine">
            <span className="visuallyHidden">
              メニューを開閉する
            </span>
          </span>
        </button>
      </div>
    </>
  );
};


TypeScriptでPropsの型を明示的に

ReactとTypeScriptの相性はとても良いです。このように型をPropsに渡すことによって、どのような値を受け取っているのか明示的に書くことができます。
Layoutコンポーネントから受け取ったopen,setOpenを受け取ります。
openにはboolean,setOpenは関数的な使い方をするのでTypeScriptが持っているFunction型をここでは使用しています。

type Props = {
  open: boolean;
  setOpen: Function;
};

//Layoutコンポーネントのstateを受け取る。
export const Burger: React.FC<Props> = ({
  open,
  setOpen,
}) => {


Stateに応じてイベントを発火させる

button要素のaria-expandedにopenの値を当てることによって、スタイルを変化させます。

     return (
    <>
      <div css={hamburger}>
        <button
          type="button"
          className="button hamburger"
          aria-controls="global-nav"
          aria-expanded={open}
          onClick={() => setOpen(!open)}
        >
          <span className="hamburgerLine">
            <span className="visuallyHidden">
              メニューを開閉する
            </span>
          </span>
        </button>
      </div>
    </>
  );


このようにopenの値を受け取ります。LayoutコンポーネントのStateを受け取っているので、初期値はfalseになります。

//openの値はfalse
aria-expanded={open}


メソッドonClickを当てることによって、ハンバーガーメニューがクリックされた時に真偽値を切り替えます。

//クリックされると真偽値が逆になる。falseの場合trueに切り替わる
onClick={() => setOpen(!open)}


これで状態を切り替えることができるようになりました。
あと以下のようにCSSでアニメーションを設定します。

.hamburger {
	//デフォルトのスタイルを当てる
}
.hamburger[aria-expanded='true'] {
	//aria-expandedがtrueになった際に発火させたいスタイルを当てる
}


詳しいスタイルはCSSinJSのemotionで書いたものになりますが、以下のリポジトリを参照してください。
GitHub | PortfolioSite

これでuseStateを使ってハンバーガーメニューのアニメーションを実装することができました。

Navコンポーネントを作成する

次にハンバーガーメニューをクリックすると展開されるNavコンポーネントを作成していきましょう。
Navコンポーネントが展開される仕組みについてまず説明します。

BurgerのstateがNavに共有される図

先ほどBurgerコンポーネントにて状態を切り替えるonClickイベントを実装しました。Burgerコンポーネントで切り替わった状態は親コンポーネントであるLayoutで共有されているのでNavコンポーネントにも共有されます。
このように少ないコンポーネント間でstateを管理するのにuseStateは適したHooksです。

LayoutからNavコンポーネントにstateを渡す

よって、Layoutコンポーネントから状態を受け取れば実装は完了です。

import React from 'react';

type Props = {
  open: boolean;
  setOpen: Function;
};

export const Nav: React.FC<Props> = ({ open, setOpen }) => {

  return (
    <div>
      <nav aria-expanded={open}>
      	<ul>
        	<li>HOME</li>
			<li>ABOUT</li>
  			<li>POST</li>
 			<li>WORK</li>
     	    <li>CONTACT</li>
        </ul>
      </nav>
    </div>
  );
};


先ほどのBurgerコンポーネント同様に、aria-expandedの状態によってスタイルを切り替えます。

nav {
	//デフォルトのスタイルを当てる
}
nav [aria-expanded='true'] {
	//aria-expandedがtrueになった際に発火させたいスタイルを当てる
}


詳しいスタイルは以下をご覧ください。
GitHub | PortfolioSite

これでハンバーガーメニューをクリックするとstateが切り替わり、ドロワーが開閉するようになりました!

まとめ

いかがでしたでしょうか?
ReactHooksのuseStateを使えば、関数コンポーネントで簡単に状態管理を行うことができます。
HooksとCSSでアニメーションの切り替えを行うと、実装もシンプルにすみます。アニメーションはCSSに責務が分かれますので、コードもみやすいものになるのではないかと思います。

React ハンバーガーメニューで検索するとCSSinJSに状態の切り替えを埋め込んで、実装するコードがヒットしましたが、このようにCSSライクに書くと処理が明確になって良いのではないでしょうか?
ぜひ、試してみてください。

次回はuseEffectを使って何か作ってみたいと思います(ブログで実装できるUIネタを募集しています。)。

最後まで読んでくださり、ありがとうございました。何か表現など誤りがございましたらCONTACTフォームがtwitterまでコメントください。