20201128のReactに関する記事は20件です。

【React】環境構築から起動まで

はじめに

React を習得するまでの軌跡をメモっていく備忘録的な記事です。
https://qiita.com/u_query/items/51b4140a450ee5d51dcc の続きです

component を表示してみる

create-react-appで作ったデフォルトのファイルは中身は全部消して一から作ります。

index.js
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <p>Hello, World</p>
  )
};

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

表示成功!

スクリーンショット 2020-11-28 23.43.07.png

まとめ

とりあえずファイルの先頭で以下の2行を書けば React が動く。

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.renderメソッドを使って第一引数にコンポーネント、第二引数にdocument.getElementById('root')を入れる。

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

コンポーネントは以下のように定義をする。
returnの中に JSX を書いていき、ブラウザにはこの部分が表示される。

const App = () => {
  return (
    <p>Hello, World</p>
  )
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】component を作る

はじめに

React を習得するまでの軌跡をメモっていく備忘録的な記事です。
https://qiita.com/u_query/items/51b4140a450ee5d51dcc の続きです

component を表示してみる

create-react-appで作ったデフォルトのファイルは中身は全部消して一から作ります。

index.js
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <p>Hello, World</p>
  )
};

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

スクリーンショット 2020-11-28 23.43.07.png

表示成功

まとめ

とりあえずファイルの先頭で以下の2行を書けば React が動く。

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.renderメソッドを使って第一引数にコンポーネント、第二引数にdocument.getElementById('root')を入れる。

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

コンポーネントは以下のように定義をする。
returnの中に JSX を書いていき、ブラウザにはこの部分が表示される。

const App = () => {
  return (
    <p>Hello, World</p>
  )
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】環境構築から起動まで

はじめに

React を習得するまでの軌跡をメモっていく備忘録的な記事です。

環境構築

yarn は installしておく

$ yarn global add create-react-app

React アプリに必要なファイル群を作成

$ create-react-app sample_app

アプリの起動

sample_app $ yarn start

localhost:3000 にアクセスすると...

スクリーンショット 2020-11-28 23.32.40.png

表示成功

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを学んだ(3) ~React Router~

React Router

  • SPAを実現してくれる。
  • <BrowseRouter/><Link/><Route/>を同時に使う。
  • yarn add react-router-domから始める。

<BrowserRouter />

  • こんな感じで導入する。
  • 遷移の履歴がhistoryオブジェクトの中に蓄積されるようだ。
src/index.js
import { BrowserRouter } from 'react-router-dom'
import App from './App';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>, document.getElementById('root')
);

<Link to="/about">About</Link>

  • 公式ドキュメントはこちら
  • stateが肝っぽい。

image.png

  • 現在地を元に次の行き先を決めるためにはcallback関数を渡せば良いらしい。

image.png

<Route />

  • This is a component which is going to decide which components are rendered based on the current URL path.
  • ブラウザが指定するURLがpath=XXXにマッチした場合のみ、指定されたコンポーネントを描画してくれる。
  • 以下のように使用する
import { Link } from 'react-router-dom'

<div>
  <Route exact path='/' render={() => (   // exactを付けないと前方一致は全て当てはまってしまう...
                                          // propsを渡す必要があるならば、render=にcallback関数形式でコンポーネントを指定する
    <ListContacts
      onDeleteContact={this.removeContact}
      contacts={this.state.contacts}
    />
  )} />

  <Route path='/create' component={CreateContact}/>   // propsを渡す必要がなければ、component=XXXで指定できる
</div>

  • また、以下のようにhistory.push('/xxx')により、ページのリダイレクトが可能である。

<Route path='/create' render={({ history }) => (  // 第一引数はhistroyオブジェクトが渡されるらしい(?)
  <CreateContact
    onCreateContact={(contact) => {
      this.createContact(contact)
      history.push('/')              // history.push('/xxx')を指定する
    }}
  />
)}/>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactからLaravelのAPIサーバーを叩く + CORS概説

初めに

以前、LaravelをAPIサーバーとして利用する記事を書いたのですが、その続きとして、ReactとLaravelを使って、CORSとは何かを解説しながら、実際にLaravel側に設定を書くところまでをやってみたいと思います。

※ Laravelでは、CORSの設定は、fruitcake/laravel-corsを使うことが一般的かなと思いますが、本記事では、Middlewareの実装も行っています。

CORSって何?

Cross-Origin Resource Sharingの略で、コルスと呼びます。日本語訳するとオリジン間リソース共有と呼びます。この設定をしていないと、フロントからAPIを叩いて、APIサーバーの値を取得したり、保存したりということが出来ません。

Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。

オリジン間リソース共有 (CORS)

Cross-OriginのOriginってそもそも何?

Originとは、URL中の スキーム ホスト ポートの組み合わせのことです。

例:https://google.com:443

Origin(オリジン)

ブラウザはセキュリティ上の理由で、スクリプトによって開始されるHTTPリクエストを制限しています。これは「同一オリジンポリシー」に従う事とイコールで、APIを叩く際に、CORSの設定をしていない場合、同じオリジンに対してのみしかリクエストを行うことができません。これは以下のような脆弱性を防ぐことを目的としています。

CSRF(Cross-Site RequestForgeries)

直訳のまま、サイト横断的にリクエストを偽装する攻撃で、Web アプリケーションのユーザーが、意図しない処理をWebサーバー上で実行される脆弱性です。

本来はログインしたユーザーしか実行できない記事の書き込みを勝手にされたり、登録情報を強制的に変更されたりしてしまいます。

XSS(Cross Site Scripting)

ユーザーが Web サイトにアクセスすることで不正なスクリプトがWebブラウザ(Client)で実行されてしまう脆弱性です。

利用者のCookieが盗まれ、Cookie内にある利用者のセッション情報がそのまま使用されてしまう「なりすまし」の危険性があります。

繰り返しになりますが、上記の脆弱性を回避するために、APIを叩く際には、同一オリジンポリシーに従って、通常は異なるオリジンに対してリクエストを行うことができません。

CORSって何のためにあるの?

WebクライアントとAPIサーバーで分けて運用する場合など、異なるオリジンの間で通信を行う場合には、CORSの設定が必要です。

CORSは、ブラウザから情報を読み取ることを許可するオリジンを設定することを可能にします。

今回のケースでいうと、Reactアプリケーションから、Laravelで作ったAPIサーバーにリクエストを投げる際に、CORSの設定を行っていないと、エラーが返ってきてしまいます。

CORSの二つのリクエスト

CORSには、リクエストの種類として、単純リクエスト(Simple Request)プリフライトリクエスト(Preflight Request)に分類されています。

単純リクエスト(Simple Request)

以下のメソッドが単純リクエストで許可されています。

  • GET
  • POST
  • HEAD

詳細は以下を参照してみてください。
単純リクエスト

プリフライトリクエスト(Preflight Request)

HTTPメソッドには、サーバー情報に副作用を引き起こすメソッド(MIMEタイプを伴うPOSTやDELETE等)がありますよね。この場合、サーバーから対応するメソッドの一覧を取得し、サーバーの「認可」に基づいてリクエストを送信する必要があります。

方法としては、リクエストの始めにOPTIONSメソッドによるHTTPリクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうか確かめます。(=アクセスを許可するメソッドをレスポンスヘッダーに含める必要があります

以下のメソッドが、プリフライトリクエストでは許可されています。

  • PUT
  • PATCH
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE

詳細は以下を参照してみてください。
プリフライトリクエスト

CORSの設定がない状態でReactからAPIサーバーを叩いてみる

以下コンポーネントは、マウントされた時に、APIサーバーから値を取得する処理と入力した値をPOSTして保存する処理が書かれたコンポーネントになります。

実際に手元で試したい場合は、以下記事を参考にAPIサーバーも立てて試してみてください。
LaravelでサクッとAPIサーバーを立てる

検証用コンポーネント
export default function Component(props) {

  const headers = {
    "Content-type": "application/json",
  }

  const [articles, setArticles] = useState([]);
  const [newArticle, setNewArticle] = useState({title: "", body: ""});

  useEffect(()=> {
    const get = async () => {
      const res = await axios("http://127.0.0.1:8000/api/articles");
      setArticles(res.data)
    }
    get();
  },[])

  const handleOnCreateNewArticle = async () => {
    const data = {
      title: newArticle.title,
      body: newArticle.body
    }

    await axios.post("http://127.0.0.1:8000/api/articles", data, {
      headers
    })

    const res = await axios("http://127.0.0.1:8000/api/articles");
    setArticles(res.data)
  }

  return (
    <>
      <h1>
        {props.title}
      </h1>
      <h2>ArticleList</h2>
      {articles.map((article) => 
        <div key={article.id}>
        <div>TITLE:{article.title}</div>
        <div>BODY:{article.body}</div>
        <hr />
        </div>
        )}

      <input type="text" name="title" onChange={(e)=>{setNewArticle({...newArticle, title: e.target.value})}} value={newArticle.title} placeholder="New Title?"/>
      <input type="text" name="body" onChange={(e)=>{setNewArticle({...newArticle, body: e.target.value})}} value={newArticle.body} placeholder="New Body?"/>
      <button onClick={ handleOnCreateNewArticle }>Create</button>

    </>
  )
}

実際にReactと、LaravelのAPIのローカルサーバーを起動して、検証してみます。

React:http://localhost:3000
↓リクエスト
Laravel(APIサーバー):http://127.0.0.1:8000/api/articles

すると、以下メッセージがDevToolで確認できるはずです。
「Access-Control-Allow-Originヘッダーが存在しないので、CORSポリシーによってブロックされていますよ」といったメッセージが表示されていますね。

Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

続いて、新規Articleを作成するためにPOSTメソッドでデータを送信してみます。すると、以下エラーが表示されます。プリフライトリクエストのチェックにパスしてませんと表示されています。

Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

以上で、CORSの設定をしていないとAPIを叩いた際にエラーとなることがわかりました。それでは、Access-Control-Allow-Originレスポンスヘッダーを追記して、CORSの設定をしていきます。

LaravelでCORS設定を行う

Middlewareを作成する

設定は簡単です。まずミドルウェアを作成しましょう。

php artisan make:middleware Cors
app/Http/Middleware/Cors.php
class Cors
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request)
        ->header('Access-Control-Allow-Origin', 'http://localhost:3000')
        ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        ->header('Access-Control-Allow-Headers', 'Content-Type');
    }
}

ここで、レスポンスのHTTPヘッダーに3つのフィールドを追加しています。簡単に各フィールドの役割について見ていきます。

Access-Control-Allow-Origin

MDN:Access-Control-Allow-Origin

Access-Control-Allow-Origin レスポンスヘッダーは、指定されたオリジンからのリクエストを行うコードでレスポンスが共有できるかどうかを示します。

MDNの記述の通りですが、APIサーバー(ここではLaravel)へのアクセスを許可するオリジン(ここではReact)を指定しています。
上記の例だと、http://localhost:3000は許可していますが、それ以外は、同一オリジンポリシーに従って、アクセス不可です、という設定になっています。

Access-Control-Allow-Methods

許可するHTTPメソッドを指定します。
Preflightリクエストを送信する場合には、ここに許可するメソッドを追記します。上記の例では、GET POST PUT DELETE OPTIONSを追記しています。

Access-Control-Allow-Headers

許可するHTTPヘッダーを指定します。
上記の例では、Content-Typeヘッダーが指定される値が設定されます。

作成したミドルウェアをカーネルに追加

続いて、作成したミドルウェアをカーネルに追加しましょう。

app/Http/Kernel.php
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'cors' => \App\Http\Middleware\Cors::class,
    ];

ルーティング

最後にルーティングを以下のように書き換えます。

routes/api.php
Route::group(['middleware' => ['api', 'cors']], function(){
    Route::options('articles', function() {
        return response()->json();
    });
    Route::resource('articles', 'Api\ArticlesController');
});

CORS設定後に実際にAPIを叩いてみる

以上で、CORSの設定は完了です。
先ほど、設定した3つのフィールドもヘッダーに含まれていることが確認できます。
スクリーンショット 2020-11-28 19.08.53.png

実際にArticlesのデータが返ってきていることも確認できました。
スクリーンショット 2020-11-28 19.08.01.png

以上で、CORSの設定からAPIを叩くところまで出来るようになると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CORSを理解しながら、ReactからLaravelのAPIサーバーを叩く

初めに

以前、LaravelをAPIサーバーとして利用する記事を書いたのですが、その続きとして、ReactとLaravelを使って、CORSとは何かを解説しながら、実際にLaravel側に設定を書くところまでをやってみたいと思います。

※ Laravelでは、CORSの設定は、fruitcake/laravel-corsを使うことが一般的かなと思いますが、本記事では、Middlewareの実装も行っています。

CORSって何?

Cross-Origin Resource Sharingの略で、コルスと呼びます。日本語訳するとオリジン間リソース共有と呼びます。この設定をしていないと、フロントからAPIを叩いて、APIサーバーの値を取得したり、保存したりということが出来ません。

Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。

オリジン間リソース共有 (CORS)

Cross-OriginのOriginってそもそも何?

Originとは、URL中の スキーム ホスト ポートの組み合わせのことです。

例:https://google.com:443

Origin(オリジン)

ブラウザはセキュリティ上の理由で、スクリプトによって開始されるHTTPリクエストを制限しています。これは「同一オリジンポリシー」に従う事とイコールで、APIを叩く際に、CORSの設定をしていない場合、同じオリジンに対してのみしかリクエストを行うことができません。これは以下のような脆弱性を防ぐことを目的としています。

CSRF(Cross-Site RequestForgeries)

直訳のまま、サイト横断的にリクエストを偽装する攻撃で、Web アプリケーションのユーザーが、意図しない処理をWebサーバー上で実行される脆弱性です。

本来はログインしたユーザーしか実行できない記事の書き込みを勝手にされたり、登録情報を強制的に変更されたりしてしまいます。

XSS(Cross Site Scripting)

ユーザーが Web サイトにアクセスすることで不正なスクリプトがWebブラウザ(Client)で実行されてしまう脆弱性です。

利用者のCookieが盗まれ、Cookie内にある利用者のセッション情報がそのまま使用されてしまう「なりすまし」の危険性があります。

繰り返しになりますが、上記の脆弱性を回避するために、APIを叩く際には、同一オリジンポリシーに従って、通常は異なるオリジンに対してリクエストを行うことができません。

CORSって何のためにあるの?

WebクライアントとAPIサーバーで分けて運用する場合など、異なるオリジンの間で通信を行う場合には、CORSの設定が必要です。

CORSは、ブラウザから情報を読み取ることを許可するオリジンを設定することを可能にします。

今回のケースでいうと、Reactアプリケーションから、Laravelで作ったAPIサーバーにリクエストを投げる際に、CORSの設定を行っていないと、エラーが返ってきてしまいます。

CORSの二つのリクエスト

CORSには、リクエストの種類として、単純リクエスト(Simple Request)プリフライトリクエスト(Preflight Request)に分類されています。

単純リクエスト(Simple Request)

以下のメソッドが単純リクエストで許可されています。

  • GET
  • POST
  • HEAD

詳細は以下を参照してみてください。
単純リクエスト

プリフライトリクエスト(Preflight Request)

HTTPメソッドには、サーバー情報に副作用を引き起こすメソッド(MIMEタイプを伴うPOSTやDELETE等)がありますよね。この場合、サーバーから対応するメソッドの一覧を取得し、サーバーの「認可」に基づいてリクエストを送信する必要があります。

方法としては、リクエストの始めにOPTIONSメソッドによるHTTPリクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうか確かめます。(=アクセスを許可するメソッドをレスポンスヘッダーに含める必要があります

以下のメソッドが、プリフライトリクエストでは許可されています。

  • PUT
  • PATCH
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE

詳細は以下を参照してみてください。
プリフライトリクエスト

CORSの設定がない状態でReactからAPIサーバーを叩いてみる

以下コンポーネントは、マウントされた時に、APIサーバーから値を取得する処理と入力した値をPOSTして保存する処理が書かれたコンポーネントになります。

実際に手元で試したい場合は、以下記事を参考にAPIサーバーも立てて試してみてください。
LaravelでサクッとAPIサーバーを立てる

検証用コンポーネント
export default function Component(props) {

  const headers = {
    "Content-type": "application/json",
  }

  const [articles, setArticles] = useState([]);
  const [newArticle, setNewArticle] = useState({title: "", body: ""});

  useEffect(()=> {
    const get = async () => {
      const res = await axios("http://127.0.0.1:8000/api/articles");
      setArticles(res.data)
    }
    get();
  },[])

  const handleOnCreateNewArticle = async () => {
    const data = {
      title: newArticle.title,
      body: newArticle.body
    }

    await axios.post("http://127.0.0.1:8000/api/articles", data, {
      headers
    })

    const res = await axios("http://127.0.0.1:8000/api/articles");
    setArticles(res.data)
  }

  return (
    <>
      <h1>
        {props.title}
      </h1>
      <h2>ArticleList</h2>
      {articles.map((article) => 
        <div key={article.id}>
        <div>TITLE:{article.title}</div>
        <div>BODY:{article.body}</div>
        <hr />
        </div>
        )}

      <input type="text" name="title" onChange={(e)=>{setNewArticle({...newArticle, title: e.target.value})}} value={newArticle.title} placeholder="New Title?"/>
      <input type="text" name="body" onChange={(e)=>{setNewArticle({...newArticle, body: e.target.value})}} value={newArticle.body} placeholder="New Body?"/>
      <button onClick={ handleOnCreateNewArticle }>Create</button>

    </>
  )
}

実際にReactと、LaravelのAPIのローカルサーバーを起動して、検証してみます。

React:http://localhost:3000
↓リクエスト
Laravel(APIサーバー):http://127.0.0.1:8000/api/articles

すると、以下メッセージがDevToolで確認できるはずです。
「Access-Control-Allow-Originヘッダーが存在しないので、CORSポリシーによってブロックされていますよ」といったメッセージが表示されていますね。

Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

続いて、新規Articleを作成するためにPOSTメソッドでデータを送信してみます。すると、以下エラーが表示されます。プリフライトリクエストのチェックにパスしてませんと表示されています。

Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

以上で、CORSの設定をしていないとAPIを叩いた際にエラーとなることがわかりました。それでは、Access-Control-Allow-Originレスポンスヘッダーを追記して、CORSの設定をしていきます。

LaravelでCORS設定を行う

Middlewareを作成する

設定は簡単です。まずミドルウェアを作成しましょう。

php artisan make:middleware Cors
app/Http/Middleware/Cors.php
class Cors
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request)
        ->header('Access-Control-Allow-Origin', 'http://localhost:3000')
        ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        ->header('Access-Control-Allow-Headers', 'Content-Type');
    }
}

ここで、レスポンスのHTTPヘッダーに3つのフィールドを追加しています。簡単に各フィールドの役割について見ていきます。

Access-Control-Allow-Origin

MDN:Access-Control-Allow-Origin

Access-Control-Allow-Origin レスポンスヘッダーは、指定されたオリジンからのリクエストを行うコードでレスポンスが共有できるかどうかを示します。

MDNの記述の通りですが、APIサーバー(ここではLaravel)へのアクセスを許可するオリジン(ここではReact)を指定しています。
上記の例だと、http://localhost:3000は許可していますが、それ以外は、同一オリジンポリシーに従って、アクセス不可です、という設定になっています。

Access-Control-Allow-Methods

許可するHTTPメソッドを指定します。
Preflightリクエストを送信する場合には、ここに許可するメソッドを追記します。上記の例では、GET POST PUT DELETE OPTIONSを追記しています。

Access-Control-Allow-Headers

許可するHTTPヘッダーを指定します。
上記の例では、Content-Typeヘッダーが指定される値が設定されます。

作成したミドルウェアをカーネルに追加

続いて、作成したミドルウェアをカーネルに追加しましょう。

app/Http/Kernel.php
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'cors' => \App\Http\Middleware\Cors::class,
    ];

ルーティング

最後にルーティングを以下のように書き換えます。

routes/api.php
Route::group(['middleware' => ['api', 'cors']], function(){
    Route::options('articles', function() {
        return response()->json();
    });
    Route::resource('articles', 'Api\ArticlesController');
});

CORS設定後に実際にAPIを叩いてみる

以上で、CORSの設定は完了です。
先ほど、設定した3つのフィールドもヘッダーに含まれていることが確認できます。
スクリーンショット 2020-11-28 19.08.53.png

実際にArticlesのデータが返ってきていることも確認できました。
スクリーンショット 2020-11-28 19.08.01.png

以上で、CORSの設定からAPIを叩くところまで出来るようになると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでfor文を使う

したかったこと

JSX内で表の列をfor文を用いることで、繰り返し処理する。
理由)デザインを変える時に、一つ一つ変更するのが面倒だった。

解決前のコード
<tr>
  <th style={{fontWeight: 'bold',fontSize:40}}>1set</th>
  <th><input style={{height:52, width:80,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}>kg</span></th>
  <th><input style={{height:52, width:70,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}></span></th>
</tr>
<tr>
  <th style={{fontWeight: 'bold',fontSize:40}}>2set</th>
  <th><input style={{height:52, width:80,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}>kg</span></th>
  <th><input style={{height:52, width:70,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}></span></th>
</tr>
<tr>
  <th style={{fontWeight: 'bold',fontSize:40}}>3set</th>
  <th><input style={{height:52, width:80,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}>kg</span></th>
  <th><input style={{height:52, width:70,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}></span></th>
</tr>

解決策

・即時関数を使う。
・for文のループで出力された列を作成したリストにプッシュし、最後にリストを返す。(これをしないでfor文内で値を返すと、1ループ目で値を返してしまいました。)

解決後のコード
{(() =>{
  const item=[];
  for(let i=1; i <5; i++) {
    item.push(
    <tr>
      <th style={{fontWeight: 'bold',fontSize:40}}>{i}set</th>
      <th><input style={{height:52, width:80,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}>kg</span></th>
      <th><input style={{height:52, width:70,borderStyle:"none", borderRadius:4}}/><span style={{fontSize:40}}></span></th>
    </tr>
    )
  }
  return(item)
})()}

結果

スクリーンショット 2020-11-28 17.44.47.png

参考にした記事 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

firebase google認証でログインユーザーを選べない問題

概要

とある日、React.js x firebase でgoogle認の実装をしておりました。
documentを参照しながら、実装をしていたら、以下の問題がおきました。

一度google認証でサインインをすると、次回のサインインを別のユーザー(別のgoogleアカウント)で行うことができない。別のgoogleアカウントでもサインインしたいのに。。。

問題のコード

const signInByGoogle = () => {
   try {
      const googleProvider = new firebase.auth.GoogleAuthProvider();
      firebase
        .auth()
        .signInWithPopup(googleProvider)
        .then((res) => {
          const user = res.user;
          if (user) {
            console.log(ok!!!)
          }
        });
    } catch (e) {
      console.log('error', e.code);
    }

}

こんな感じのコードが公式のドキュメントにも書いてあります。このコードで通常にログインはできますし特に問題はありません。
しかし、他のgoogle アカウントを選択してログインすることができません。

ではどうするのか。以下解決方法です。

解決方法

googleProviderのインスタンスのoptionに追加指定します。

const signInByGoogle = () => {
   try {
      const googleProvider = new firebase.auth.GoogleAuthProvider();
      googleProvider.setCustomParameters({
        prompt: 'select_account', // 追加
      });

      firebase
        .auth()
        .signInWithPopup(googleProvider)
        .then((res) => {
          const user = res.user;
          if (user) {
            console.log(ok!!!)
          }
        });
    } catch (e) {
      console.log('error', e.code);
    }

}

どうやらgoogle側の仕様のようです。1つのアカウントでサインインし続けるのがまあ普通なんですけどね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを学んだ(2) ~Lifecycle Events~

Lifecycle Events

  • 以下のようなものたちがある。
  • componentDidMount is only called once in the lifecycle of any component, re-render will not reinitialize the component. componentDidUpdate is triggered after re-rendering DOM.

image.png

  • API Callをしたい時には、componentDidMount()ライフサイクルメソッド内で行い、それによって得られたデータの変更をstateに反映させる。
  • これによって、render()メソッドはレンダリングだけが責務になるし、まずstateは最新ではないものがコンテンツの枠組みを最初に表示することができる

  • 以下のような感じ。

import React, { Component } from 'react';
import fetchUser from '../utils/UserAPI';

class User extends Component {
 constructor(props) {
   super(props);

   this.state = {
     name: '',
     age: ''
   };
 }

 componentDidMount() {
   fetchUser().then((user) => this.setState({
     name: user.name,
     age: user.age
   }));
 }

 render() {
   return (
     <div>
       <p>Name: {this.state.name}</p>
       <p>Age: {this.state.age}</p>
     </div>
   );
   }
}

export default User;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScriptでコネクト4を作った(クラスコンポーネント)

はじめに

ブラウザゲームを作る経験がしたくて、「遊び大全」の中で一番好きなゲームがコネクト4を自分で作ってみました。
リアクティブに画面の状態が変わり、ゲームが進行するので、
初学者向けのReact + TypeScriptの練習としては良い題材だったと思います。

この記事では作成の大まかな流れと学んだ知識などを書いています。
最初はチュートリアル記事にしようかと思ったのですが、いちいち説明しなくてもgithubにコメントを入れたので、それで伝わるかなーと判断しました。

また最近Reactを触り始めたこともあり、関数コンポーネントでしか今まで制作物を作っていないので、今回はあえてクラスコンポーネントで作りました。

作った物

コネクト4です。
日本語でいう四目並べですね。
縦横斜めのいずれかで4つ連続でコマを先に並べた方が勝ちとなります。

実際のゲームはこんな感じです。https://www.mathsisfun.com/games/connect4.html
※今回はオンライン機能の実装はしません。

で、今回作ったものがこんな感じ。
image.png

動いている所が見たい人は技術ブログに埋め込んでいるので、確認してください。

Githubのリポジトリ: https://github.com/gunners6518/connect4

作り方

仕様を決める

最初の必要機能などからざっくりしたアウトラインを作りました。

・UI:6×7のセルを作る 
・セル:クリックしたら、その列の一番下が選択される 相手のプレイヤーの番になる
・ゲーム:縦、横、斜めで4つ揃ったら勝利

UI:6×7のセルを作る

6×7のセル作成ですね。
セル1つ1つにplayer,indexというデータを持たせて、indexで配置が決まり、playerでどちらかに既に選択されているセルなら色が変わるようにしました。

セル:クリック時のアクション

セルクリック時のアクションでは以下の機能をつけました。
・クリックしたセルのindexから列を判定して、その列で空いている一番下のセルが選択される処理。
・セル選択後にplayerTurnが変わる処理

ゲーム:縦、横、斜めで4つ揃ったら勝利

ここは理論の部分でどうすれば良いのか苦労しました。

あーだこーだ悩んだ結果、

横:連続する4つのindexを持つセルが並んでいる
縦:index、index + 7、index + 7 * 2、index + 7 * 3が並んでいる
斜め:index、index + 7 – 1、index +7 * 2 – 2、index + 7 * 3 – 3が並んでいる

で配列を切り出し、それらPlayerが全て一致しているかどうかをboolean型で返す関数を作り判定しました。

完成まで諸々

デザインを修正したり、文言を入れたり、コメントで簡単な説明を入れたりして完成です。
作成自体は3~4時間ほどかかりました。

感想

ブラウザゲームを作る経験ができたのが嬉しかったです。
いつか遊び大全にあるゲームを全て作りたいな!!(ピアノはなしで)

クラスコンポーネント縛りをしたのですが、好みは関数コンポーネントだなと改めて感じました。
現在はこのコネクト4をクラス→関数コンポーネントのリファクタリングをしているので、関数コンポーネントでの作り方、リファクタリングの方法などは今後まとめていく予定です。

最後まで読んでいただきありがとうございました。
Twitter技術ブログの方が高頻度でアウトプットしているので、興味ある方は是非。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsで「document is not defined」と怒られたときの対処法

Next.jsを使っていると様々なエラーに遭遇します。その中でも

  • document is not defined
  • window is not defined

これらのエラーは良く見るので対処方法を解説します。
エラー画面は以下のようなものです。
WS000006.JPG

documentの未定義エラーの対処方法

「document is not defined」の対処方法は2つあります。

1.documentの定義判定

1つ目はグローバル変数にdocumentがあるのかの存在判定です。
documentが定義されている場合のみプログラムを実行するのでif (typeof document !== 'undefined') {}でくくります。今回のプログラムではdocumentオブジェクトを使って、クッキーに値をセットしています。

index.jsx
export default function Index() {
  //document変数が定義されているときのみ
  if (typeof document !== 'undefined') {
    //クッキーに値をセット
    document.cookie = "クッキー";
  }

  return (
    <div style={{textAlign: "center", marginTop: "50px"}}>
      {/* 値の表示 */}
      <h1>値:{typeof document !== 'undefined'?document.cookie:""}</h1>
    </div>
  )
}

WS000001.JPG

2.ブラウザ実行判定

2つ目の方法はブラウザで動いているかの判定です。ブラウザで動いているときのみ、documentを使った処理を実行します。process.browserがtrueの場合はブラウザで動いている証拠です。一方、undefinedの場合はサーバーでの実行になります。

index.jsx
export default function Index() {
  //ブラウザ実行時のみ
  if (process.browser) {
    //クッキーに値をセット
    document.cookie = "クッキー";
  }

  return (
    <div style={{textAlign: "center", marginTop: "50px"}}>
      {/* 値の表示 */}
      <h1>値:{process.browser?document.cookie:""}</h1>
    </div>
  )
}

なぜ「documentが定義されていない!」と怒られるのか?

なぜdocumentwindowが定義されていないと怒られているのかというと、サーバーサイドでブラウザ用のグローバル変数を使おうとしているためです。

Next.jsはサーバーサイド、クライアントサイド両方で動くフレームワーク。そのため、定義したソースはサーバー、ブラウザ両方の環境で実行されます。

そして、documentwindowはクライアントだけで定義されているグローバル変数です。サーバー環境で動かそうとすると「そんなグローバル変数は定義されていない!」とエラーが発生します。

なので、if (process.browser)でブラウザのみの判定を入れればサーバー環境での実行時には無視され、クライアント環境だけで動くのです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SIerエンジニアからWeb系フロントエンドエンジニアに転身するために今やっていること

こんにちは!SIerでJavaプログラマをしているゆうきデザイン(@yuki_design_gr)と言います。

Qiita初投稿として、自己紹介も兼ねて
"SIerエンジニアからWeb系フロントエンドエンジニアに転身するために今やっていること"
というテーマで書いてみようと思います。

同じような境遇にいる人の道しるべの1つになりますように!

目次

1. 自己紹介
2. なぜWeb系を目指すのか
3. SIerエンジニアからWeb系フロントエンドエンジニアに転身するために今やっていること

1. 自己紹介

東京在住の20代半ば。

学歴

東京外大韓国語専攻卒業

職歴

新卒で大手SIerに入社し、アカウント営業を担当(10ヶ月)
→SE(現職。Java・.NET・Oracleのコーディング実務1年半)
→Web系企業への転職準備中

モットー

技術とデザインのことをポジティブに共有すること

目標

・世の中をポジティブにするWebサービスを作ること
・ビジネスを始めたい・サービスを作りたい友だちをIT・デザイン面でサポートすること

趣味など

韓国語と英語は日常会話レベル
持久系のスポーツが好き(陸上・水泳。社会人になってからもたまにやってる)
実写の動画編集
映画・音楽・コーヒー
ミスチルが生まれた時から好きで、人生ピンチの時に助けてくれる存在(←いま)

さあ、本題に入ります!

2. なぜWeb系を目指すのか

①新しいものを追いかけるのが好きだから

音楽や映画などのエンタメや好きで、
SI業界よりもトレンドの移り変わりが激しいWeb業界が楽しそうに見えるため。

②Webサービスを作りたいという目標があるから

自己紹介でも書いたように
世の中をポジティブにするWebサービスを作る
という目標があり、
SI業界に身を置くよりも目標実現への近道だと思っています。

③システムの裏側の処理よりも見た目に魅かれるから

Javaエンジニアをしていてプログラミングは基本的に全般楽しいですが、
JSPやCSS等のシステムの見た目の開発が楽しく、
また他のメンバーが気にかけないレイアウトのズレなどに何度も気づくことができました。

そのため、まずはWebデザイナーやUI・UXデザイナーに興味を持ち、
デザイナーのための勉強会などに参加してきました。

しかし、自分のプログラマとしての経験を活かす×システムの見た目に寄与できる
というフロントエンドの技術が一番しっくりくるなあと今は思っています。

3. SIerエンジニアからWeb系フロントエンドエンジニアに転身するために今やっていること

①フロントエンド技術に触れること

結局はどの言語がベスト!とかはなさそうなので
今は色々触ってみてます。

フロント: react, vue, rails
バックエンドやインフラ等: node, ruby, docker, laravel
その他: TypeScript, Sass, Bootstrap

色々触れる今の時期が一番楽しいですね。
何かを極めた方が勉強になる気もしますが。

個人的にはnode + react(またはvue・angular) + TypeScriptがアツい気がします!
全部jsで書けるなんて!

②フロントエンド技術を用いたWebアプリを作ってみること

上記のそれぞれの言語を使って
簡単なSNSやTodoリストをチュートリアルを見ながら作成中です。
ネット上に公開するところまでやりたいです。

③SNSやGitHub、Qiita等でのアウトプット

個人的に苦手であまりできていないアウトプット。
でも見てる人との交流が生まれたり、自分の特性や技術力を客観視できる機会と思って、
定期的に取り組んでいきたいです。


この投稿の内容は以上です。

ここまで読んでいただきありがとうございます。
これからも有益な情報をポジティブに発信していきたいです。

ぜひ、Twitter(@yuki_design_gr)のフォローもよろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebase Emulator Suiteをインストールして使ってみた

メイン( masalibの日記 )で書いているブログにはあまりアクセスがないのでこちらにも記載います。あまりQiitaでは書かないので文化が違っていたらすいません

f:id:masalib:20201128105429p:image:w400

目次

  1. Firebase Emulator Suiteとは
  2. 事前準備
    1. javaのインストール
    2. Firebase CLIのインストールと設定
  3. Firebase Emulatorのインストール
  4. Firebase Emulatorの起動
  5. Firebase Emulator Suiteを開く
  6. ローカルのReactで使ってみる
  7. Reactで確認する
  8. 感想

Firebase Emulator Suiteとは

ローカルでFirebaseをエミュレートしてくれる機能です。

今まではAuth機能の開発をおこなう時はプロジェクト+ dev みたいな形でプロジェクトを作って検証していました。それを今後はローカルのみで完結できるみたいです。テストのclも回せるみたいなのでテストツールを導入している所だと重宝されそう。

事前準備

Firebase Emulator Suiteは以下の条件がそろっていないと動きません

  • Node.js バージョン 8.0 以降。
  • Java バージョン 1.8 以降。
  • Firebase CLI 7.8.0 以降

NodeはReactを作っているのでバージョンは余裕でクリアできるのですがjavaやFirebase CLIは新しいマシンだと全然使っていなかったのでインストールしないといけなかった。

javaのインストール

本編とは関係ないのでjavaをインストールしている人はスキップしてください。

  1. 本家からバイナリーをダウンロードする
    https://jdk.java.net/archive/
    f:id:masalib:20201128111033p:image:w400
  2. ダウンロードしたファイルをローカルのPCにダウンロードする。私の場合は
    「C:\java\jdk-15」です
  3. 「Windowsキー」+「R」キーでファイル名を指定して実行するの画面を表示して「control sysdm.cpl」を入力する。システムのプロパティが開く
    f:id:masalib:20201128112005p:image:w400
  4. 詳細設定のタブをクリックして環境変数のボタンを押す
    f:id:masalib:20201128112310p:image:w400
  5. JAVA_HOMEを設定
    f:id:masalib:20201128112310p:image:w400
  6. pathの編集をおこなう
    f:id:masalib:20201128112747p:image:w400
  7. コマンドラインを開いていたら閉じてから開く。
  8. 確認コマンドでjavaが認識している事を確認する
javac -version
# => javac 15
java -version
# => openjdk version "15" 2020-09-15
# => OpenJDK Runtime Environment (build 15+36-1562)
# => OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing)

どうでもいいけどPathの編集画面がかわったのね。

この記事を参考にjavaをインストールしました

https://www.javadrive.jp/start/install/index1.html

https://www.javadrive.jp/start/install/index4.html

Firebase CLIのインストールと設定

  1. 管理者権限でコマンドラインを開く。
    f:id:masalib:20201128113744p:image:w400
  2. npmでインストールする

    npm install -g firebase-tools

  3. firebaseにloginにする。(ブラウザが立ち上がるので認証を許可する)

     $  firebase login
     i  Firebase optionally collects CLI usage and error reporting information to help improve our products.         Data is # collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not         used to identify you.
    
     ? Allow Firebase to collect CLI usage and error reporting information? Yes
     i  To change your data collection preference at any time, run `firebase logout` and log in again.
    
     Visit this URL on this device to log in:
     https://accounts.google.com/o/oauth2/auth?client_id=xxxxxxxxx・・・・
     Waiting for authentication...
     +  Success! Logged in as masalib@gmail.com
    
  4. 確認のためにプロジェクトリストを表示する

    √ Preparing the list of your Firebase projects
    ┌────────────────────────┬
    │ Project Display Name   │ Project ID             
    ├────────────────────────┼
    │ angular-study-chat     │ angular-study-chat     
    ├────────────────────────┼
    │ Learn-Firebase-masalib │ learn-firebase-masalib 
    ├────────────────────────┼
    │ masalib-hosting        │ masalib-hosting        
    ├────────────────────────┼
    │ testpwa                │ testpwa-32138          
    └────────────────────────┴
    

Firebase Emulatorのインストール

自分のプロジェクトに移動して初期化のコマンドを実行する

firebase init

進む準備はできていますか?と聞かれるのでyを入力してEnterキーを押す

Are you ready to proceed  Y

このフォルダに設定したいFirebase CLIの機能をSpaceキーを押して機能を選択し、Enterキーを押して設定します。Emulatorsが*になるようにします

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confi
 rm your choices. Emulators: Set up local emulators for Firebase features
>( ) Database: Deploy Firebase Realtime Database Rules
 ( ) Firestore: Deploy rules and create indexes for Firestore
 ( ) Functions: Configure and deploy Cloud Functions
 ( ) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 (*) Emulators: Set up local emulators for Firebase features
 ( ) Remote Config: Get, deploy, and rollback configurations for Remote Config

プロジェクトのオプションを聞かれるの既存のプロジェクトを選択します。自分の場合は「learn-firebase-masalib」でした

```
=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add,
but for now we'll just set up a default project.

? Please select an option:

Use an existing project
Create a new project
Add Firebase to an existing Google Cloud Platform project
Don't set up a default project

? Select a default Firebase project for this directory: (Use arrow keys)

angular-study-chat (angular-study-chat)
learn-firebase-masalib (Learn-Firebase-masalib)
masalib-hosting (masalib-hosting)
testpwa-32138 (testpwa)
```

どのエミュレータをインストールするのかを聞かれるのでめんどくさいの全部選択します

=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. (
Press <space> to select, <a> to toggle all, <i> to invert selection)
>( ) Authentication Emulator
 ( ) Functions Emulator
 ( ) Firestore Emulator
 ( ) Database Emulator
 ( ) Hosting Emulator
 ( ) Pub/Sub Emulator

エミュレータのport番号を聞かれるので答えていく

? Which port do you want to use for the auth emulator? 9099
? Which port do you want to use for the functions emulator? 5001
? Which port do you want to use for the firestore emulator? 8080
? Which port do you want to use for the database emulator? 9000
? Which port do you want to use for the hosting emulator? 5000
? Which port do you want to use for the pubsub emulator? 8085
? Would you like to enable the Emulator UI? (Y/n) y
? Which port do you want to use for the Emulator UI (leave empty to use any available port)? 8000
? Would you like to download the emulators now? (y/N) y 

「Which port do you want to use for the Emulator UI (leave empty to use any available port)?」は8000と入力したgoogle先生のマニュアルだと4000だった。

Firebase Emulatorの起動

起動コマンドで起動する

firebase emulators:start

問題がなけば下記のような画面が表示される

f:id:masalib:20201128122040p:image:w400

Firebase Emulator Suiteを開く

エミュレーターにはUIが用意されているのでブラウザで http://localhost:8000 を開く
(google先生のマニュアルだと http://localhost:4000 です)

f:id:masalib:20201128122539p:image:w400

Authの画面

f:id:masalib:20201128122710p:image:w400

ローカルのReactで使ってみる

Firebaseの設定ファイルの部分を変更する

import firebase from "firebase/app"
import "firebase/auth"
import "firebase/storage";
import 'firebase/firestore';

const app = firebase.initializeApp({
    apiKey: process.env.REACT_APP_APIKEY,
    authDomain: process.env.REACT_APP_AUTHDOMAIN,
    databaseURL: process.env.REACT_APP_DATABASEURL,
    projectId: process.env.REACT_APP_PROJECT_ID,
    storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_APP_ID,
    measurementId: process.env.REACT_APP_MEASUREMENT_ID
});

var db_obj = firebase.firestore();
if (process.env.REACT_APP_HOST === "localhost") {
  db_obj.useEmulator("localhost", 8080);
  console.log("useEmulator:firestore")
}

var auth_obj = firebase.auth();
if (process.env.REACT_APP_HOST === "localhost") {
    console.log("useEmulator:auth")
    auth_obj.useEmulator("http://localhost:9099")
} 

var storage_obj = firebase.storage();
/* storage_obj.useEmulator is not a functionというエラーがでたのでコメントアウト
if (process.env.REACT_APP_HOST === "localhost") {
    console.log("useEmulator:storage")
    storage_obj.useEmulator("http://localhost:9099")
}
*/

export const db = db_obj;
export const auth = auth_obj;
export const storage = storage_obj;
export default app

Reactで確認する

起動するとログにエミュレートのログが表示される

f:id:masalib:20201128124339p:image:w400

SingUPしてみると通信先がlocalhostになっている

f:id:masalib:20201128124809p:image:w400

Firebase Emulator Suiteで確認もできました

f:id:masalib:20201128125010p:image:w400

f:id:masalib:20201128125143p:image:w400

入力した内容も確認できます。

なおメールアドレスの有効化は2020/11/28の時点はできない

https://github.com/firebase/firebase-tools/issues/2872

感想

  • メールアドレスの有効化ができないとちょっと困るので早く直してほしい
  • 切り替えも楽なのでいいかも。アプリでも多分できると思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

- Cyber Muse - 音楽投稿アプリをリリースしました!(個人開発)

初めに

音楽投稿アプリを開発しました!

ウェブサイト: https://cyber-muse.com
Apple Store: https://apps.apple.com/app/id1511131064
Google Play: https://play.google.com/store/apps/details?id=com.front_music

開発言語はReactReact Nativeを使用しています。

開発から運用まで全て個人で行っています。開発から運用まで大変だったことなど色々書いていきたいと思います。

React & React Nativeについての所感

気づけばReactを書き始めてから1年ぐらいがたってしまいました。Reactを学ぶきっかけとなったのはアルバイトです。それまではフロントエンドについてはあまり興味がなく、Pythonをよく書いていました。バイト先ではReact(JS)がメインだったので面接に受かってから学び始めました。バイトの頻度は週二回1日8時間くらいでした。

React

Reactは個人的にすごく書きやすい。個人的にhtml内のscriptの中やUIとは独立したjsファイルにjsを書くのが嫌いだったので。見にくくないですか?
Reactでつまづくとすれば、それはstateという概念だ。hooks周辺を理解するのは難しい。
useEffectについてひたすら調べまくりました。正直日本語でこれ以上に分かりやすい記事は他にないと思う。
https://iqkui.com/ja/a-complete-guide-to-useeffect/
あとは例外的ではあるがファイルアップロード関係かな。バイト先ではGraphQLというバックエンドとフロントを繋ぐものを使用していたのだが、これに使えるファイルアップローダーの無さといったらもう。そもそもGraphQLを通してファイルアップロードするのもどうかと思うんだが。バックエンドでPrismaとかDockerとか使ってたからしゃあなしか?
今回バックエンドについては特にかくつもりないので割愛します。
とまぁこんな感じでReactに慣れてきたらReact Nativeも書くようになりました。

React Native

React Nativeで一番大変なのはコードを書く以前の準備だと思います。モバイルアプリの開発はSimulatorをたてた状態で行わないといけませんが、慣れないうちはこれが大変です。iOSで言えばpodあたりやXcodeの設定をしっかり把握していないとビルドが失敗します。バイト先はアプリの運用をしていたので、既に完成されているものをビルドしなければいけません。ビルド時間は5~10分くらいかかっていました。ですので、慣れていないと後少し!!というところでビルドが失敗したりしてドツボにハマったりします。
 という感じで僕のモバイルアプリ開発の始めは苦しむことが多かったですが、今はiOSもAndroidもできているのでいいかなという感じです。
 そう、これがReact Nativeの利点だと僕は思う。ネイティブ言語でiOSとAndroidの開発をしようと思ったら、swiftやjavaの両方を学ばなければいけないからだ。React Nativeを使えば、多少の差があることは確かだがUIでいじる必要があることはほとんどない。ビルドの仕方とリリースの仕方を学べばそれでいい。多少の差に言及しておくとそれはアプリ内でファイル操作をする時だ。写真を選択したり、アップロードしたり。知らないとバグって、は?ってなる。 

最後にReact NativeのUIについて述べる。UIはオープンソースのコンポーネントが色々あって便利だと思う。が、React Nativeのバージョンの違いとかで役に立たなかったりするやつがあるから注意。githubのReleasesのlatestが2年前とかのやつには手を出さないのが無難だ。あとは、cssのflexの有り難みがすごくわかる。キーワードはflexDirection, justifyContent, alignItemsだ。この3つがあれば画面が綺麗に整う。

所感の割りには色々書きすぎたと思うが以上がReact Nativeに関する正直な感想だ。

ウェブサイトのアニメーションについて

実は最近ウェブサイトをアップデートしてアニメーションを取り入れた。
https://cyber-muse.com
このウェブサイトのアニメーションは全てreact-springというモジュールを使用している。
https://www.react-spring.io/
このモジュールにスクロールの制御を加えると僕が作ったウェブサイトぐらいは作れるという感じで紹介しておこう。

終わりに

初めの文章に取り消しせんが付いている通りです。
ReactとReact Nativeについて述べただけになってしまいました。

ReactReact Nativeを使えばこういうウェブサイトやアプリが作れる!!という参考になればいいと思います。

ウェブサイト: https://cyber-muse.com
Apple Store: https://apps.apple.com/app/id1511131064
Google Play: https://play.google.com/store/apps/details?id=com.front_music

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js v10でTypeScriptを使いつつEmotion v11を使おうとしたら5時間も解決しなかった話

これは何

  • タイトルにあるように、5時間もハマってしまいました
  • 一応解決出来たので備忘録として残しています

使用しているパッケージの情報

名前 バージョン
next 10.0.1
react 17.0.1
typescript 4.0.5
@emotion/react 11.1.1
@emotion/babel-plugin 11.0.0

JSX Pragmaが動かない

事象

  • インストールの記事などには/** @jsx jsx */という記述が紹介されている
  • しかしそのままだとコンソールエラーが出た

解決策

  • /** @jsxImportSource @emotion/react */という記述にする
    • CSS Propの詳細な説明のページに記載されている
    • Reactのバージョンが新しくて(v17)JSXのランタイムの方式が違っていたため、従来の記述からは変える必要があった

Babelの設定が上手く働かない

前提

  • 新しいJSX Pragmaが動くようにはなったものの、毎回書くのは面倒
  • Babelの設定をしてJSX Pragmaの記載無しで動くようにしたい

事象

@emotion/babel-preset-css-propを使ってもJSX Pragma無しだと動かなかった

解決策

  • @emotion/babel-preset-css-propの代わりに@emotion/babel-pluginを入れつつ、.babelrcを以下のようにしたら動いた
.babelrc
{
  "presets": [
    [
      "next/babel",
      {
        "preset-react": {
          "runtime": "automatic",
          "importSource": "@emotion/react"
        }
      }
    ]
  ],
  "plugins": ["@emotion/babel-plugin"]
}

動くは動くけど、型定義がなくて怒られる

事象

  • スタイル自体は当てられるけどエディタ上でエラーが出ている
    • インストールしただけだとEmotionの型定義ファイルが無かった

解決策

next-env.d.ts
/// <reference types="@emotion/react/types/css-prop" />

まとめ

  • 過去にEmotionを適用したリポジトリを見ながら、ただ真似るだけで設定していたのが良くなかった
  • 公式ドキュメントをしっかり読むのはやっぱり大事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Native (Expo) + unstatedで状態管理するアプリを実際に作り始める時に参考にしたリポジトリ

はじめに

React Nativeの状態管理で最近はReduxよりunstatedを使うと便利という記事を見て、
使いたいとは思いつつも、どうやって実装するのか全体のイメージが湧きませんでした。

その時にいくつかリポジトリを参考にしたのでメモ。

unstated-demo

一番最初の入門としてよくあるカウンターアプリの実装の参考にしたリポジトリ。
あとunstatedで取得したデータを一覧表示する所も参考になりました。

https://github.com/dabit3/unstated-demo

react-native-social-app-sample

unstated+react-navigationの連携の実装で参考にしました。

unstated-demoよりも実用に近いかたちになっているので、
入門が終わってからはこちらのリポジトリを主に参考にしていました。

Subscribe以外の場所でContainerにアクセスしたくなった時の、
HOC化の実装もされているので、イメージが湧いて助かりました。

https://github.com/kdenz/react-native-social-app-sample

おわりに

unstated使えば、Reduxで分けていたStore, Reducer, Actionという処理が一つのファイルにまとまるので見通しがいいし、実装しやすくて良かったです。

ただReact Nativeと組み合わせた実装例が少ないので、今回の記事が参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-hook-formでサーバーサイドで行ったバリデーション結果がコントロール操作時にクリアされる

react-hook-form v6.10 ~ 6.11.5 までで確認した挙動です。

やろうとしたこと

react-hook-formを使用して情報登録用のフォームを開発していたが、
仕様上フォームの入力内容バリデーションはAPIを呼び出してサーバーサイドで実施しなければいけなかった。

理由は単純で、フォームの入力内容がバリデーションを通った場合は入力項目を表示する確認画面を表示し、
バリデーションエラーがあった場合にはフォームの再入力を促すという画面構成だったからだった。

  +------+      
  | Form |<---+
  +------+    |
     | Submit |
     +--------+  フォームが submit されたらバリデーションAPIで検証。
     |           検証OKなら確認画面へ、バリデーションエラーがあった場合は
     |           バリデーションエラーメッセージを表示したフォーム画面を再表示する。
     v
+---------+
| Confirm |
+---------+
    | Submit  確認画面で submit されたら登録完了。
    |
    ~

react-hook-formのuseForm()が返すsetError()でサーバー側のエラーをセットできるので利用していたが、
エラーがあったコントロールを操作しようとすると対応するバリデーションエラーメッセージが消えてしまった。

バリデーションエラーメッセージはフォームの入力中に何度でも確認できるよう、
submit されない限り表示し続けるという要件だったので、入力を再開した時点でメッセージが消えるのは喜ばしい挙動ではなかった。

対応

「コントロールの入力が開始された時点で対応するバリデーションエラーをリセットするべきである」というのが
react-hook-formの考えの様だった: https://github.com/react-hook-form/react-hook-form/issues/1881

react-hook-formにはフロントエンドサイドでフォーム入力中にインタラクティブなバリデーションを行う機能があり、
APIはこの機能をベースに設計されている。

入力を再開してバリデーションエラーが無くなったらエラーメッセージは非表示にするという方針のようだ。

『サーバーでバリデーションした結果を表示し続けたい』というような時は、
バリデーションエラーメッセージをsetError()などで管理しないで独自のstateとして管理するしかないようだったので
以下のようなerrorssetErrorに近いAPIを自作した。

import { useCallback, useState } from "react"
import type { UseFormMethods } from "react-hook-form"

// react-hook-form の setError はジェネリクスに渡された型に
// Array, object が含まれていると setError の第1引数にあらゆる型を受け入れるようになっている。
// あらゆる型を受け入れると Typescript の型チェックが働かなくなる。
// 強制的に型チェックを有効にするために与えられた型のプロパティ名に対して 
// unknown を持つダミータイプを提供する事で setError の型推論を強制的に復活させる。
type ForwardPropertyName<T> = { [K in keyof T]: unknown };

type SetPersistError<T> = UseFormMethods<ForwardPropertyName<T>>["setError"];

type ErrorsType<T> = UseFormMethods<T>["errors"];

type UsePersistErrorResult<T> = {
  persistErrors: ErrorsType<T>;

  setPersistError: SetPersistError<T>;
};

/**
 * コンポーネントがアンマウントされるまではエラー情報を保持する
 * react-hook-form の{@code useForm}が返す{@code errors}, {@code setErrors}と
 * 同等のオブジェクトと関数を返す。
 *
 * react-hook-form v6 の{@code errors}はフォームコントロールが変化すると
 * 全てのエラー情報を消去してしまう。
 * バックエンドでバリデーションした結果を永続的に表示し続けたい場合は、独自のエラー管理を行わなければいけない。
 * この関数はコンポーネントがアンマウントされるまではエラー情報を保持する
 * {@code persistErrors}と{@code setPersistError}を返す。
 * この2つは{@code errors}と{@code setErrors}と同じように使用できる。
 *
 * See: https://github.com/react-hook-form/react-hook-form/issues/1881
 */
const usePersistError = <T>(): UsePersistErrorResult<T> => {
  const [persistError, setPersistError] = useState<ErrorsType<T>>({});

  const setPersistErrorTyped: SetPersistError<T> = useCallback(
    (name, error) => setPersistError((prev) => ({ ...prev, [name]: error })),
    [setPersistError]
  );

  return { persistErrors: persistError, setPersistError: setPersistErrorTyped };
};

export { usePersistError };
export type { SetPersistError, ErrorsType };

使い方はusePersistError(){persistErrors, setPersistError}を取り出し、
サーバーサイドバリデーションの結果はsetPersistError()を呼び出して追加する。
persistErrorsのプロパティにセットされているバリデーションエラー情報を画面に表示するという形になった。

react-hook-formのドキュメントに記載されているサンプルコード元にすると以下のような使い方になる。

バリデーションエラーメッセージはhandleSubmitのコールバックなどで手動でクリアすることになる。

codesandbox

import React from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import { usePersistError } from "./usePersistError";

import "./styles.css";

interface IFormInputs {
  firstName: string;
  lastName: string;
  age: string;
  website: string;
}

function App() {
  const { register, handleSubmit } = useForm<IFormInputs>();
  const { persistErrors, setPersistError } = usePersistError<IFormInputs>();

  const onSubmit = (data: IFormInputs) => {
    // Clear error
    setPersistError("firstName", {});
    setPersistError("lastName", {});
    setPersistError("age", {});
    setPersistError("website", {});

    alert(JSON.stringify(data));

    if (!data.firstName) {
      setPersistError("firstName", { message: "firstName is missing." });
    }
    if (!data.lastName) {
      setPersistError("lastName", { message: "lastName is missing." });
    }
    if (!data.age) {
      setPersistError("age", { message: "age is missing." });
    }
    if (!data.website) {
      setPersistError("website", { message: "website is missing." });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>First Name</label>
        <input type="text" name="firstName" ref={register} />
        {persistErrors.firstName?.message && (
          <p>{persistErrors.firstName.message}</p>
        )}
      </div>
      <div style={{ marginBottom: 10 }}>
        <label>Last Name</label>
        <input type="text" name="lastName" ref={register} />
        {persistErrors.lastName?.message && (
          <p>{persistErrors.lastName.message}</p>
        )}
      </div>
      <div>
        <label>Age</label>
        <input type="text" name="age" ref={register} />
        {persistErrors.age?.message && <p>{persistErrors.age.message}</p>}
      </div>
      <div>
        <label>Website</label>
        <input type="text" name="website" ref={register} />
        {persistErrors.website?.message && (
          <p>{persistErrors.website.message}</p>
        )}
      </div>
      <input type="submit" />
    </form>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを学んだ(1) ~StateとProps~

Reactの特徴

  • Compositional
  • Declarative code

    • いちいちいちいち処理の詳細まで書かない
    • (e.g.) const longNames = people.filter(name => name.length > 6)
    • (c.f.) Imperative code ではない
  • Unidirectional Dataflowを使う

    • dataflowはparent elementからchildrenの一方向に伝搬される
    • 子は親の持つデータを参照できる。子は親のデータを変更しようと思ったらその旨を親に伝え、親がそれをデータの変更を行う
    • Data-Binding ではない
    • Data-Bindingを使うと、appのどこからでも保持していたデータを変更できる

Reactでelementを作る

  • React.createElement( <tag name>, <propsのhash(=オブジェクト)>, <content in tag> );で、仮想DOMを初期化。
    • e.g. const element = React.createElement('div', null, 'My name is Tyler, and I love porcupines.');
    • creteElementしただけでは、描画されない。これを、ReactDOM.render(element), document.getElementById('root')とかして、DOMに反映する。
    • propsにはhashを渡す。有効なキーはclassNameとかhtmlForとか。
      • DOM要素では、ある要素のclassにアクセスするには、cName = elementNodeReference.className;とするから。

Reactでネストしたelementを作る方法

import React from 'react'
import ReactDOM from 'react-dom'

const people = [
  { name: "John", id: 0 },
  { name: "Paul", id: 1 }
]

const element = React.createElement('ol', null,
  people.map((person) => (
    // 子要素を配列にする時は、propsがそれぞれの要素でユニークなkeyを持っていないと怒られる
    React.createElement('li', {key: people.id}, person.name)
  ))
)

ReactDOM.render(
  element,
  document.getElementById('root')
)
  • JSX(=syntax extention for JS)という拡張構文を使うことで、上記のコードもHTMLチックに、楽に書ける。
// 上記のcreateElementの箇所をJSXで書き換えると、
const element = <ol>
  {people.map((person) => (
    <li key={person.id}>{person.name}</li>
  ))}
</ol>

Class ComponentとStateless Functional Component

image.png
image.png

作ったReact componentクラスをexportする方法

export default ListContacts

初期化時にstateをプロパティーとしてComponentに持たせる方法

class User extends React.Component {
 constructor(props) {
   super(props);
   this.state = {
     username: 'Tyler'
   };
 }
}

Babelでトランスコンパイルするなら以下でもいける。

class User extends React.Component {
 state = {
   username: 'Tyler'
 } 
}
  • Avoid initializing that state with props! (e.g. this.state = {user: props.user})

stateの更新

  • this.state.hoge = new_hogeという風に更新しても、re-renderingされない。
  • 引数の関数の返り値が元のstateにマージされる。
this.setState((prevState) => (
  {count: prevState.count + 1}
)
  • なお、元のstateの値を使う必要がないなら、引数にハッシュを渡せばいい。
this.setState({ count: 2 })

stateの型チェック

  • yarn add prop-types でprop-typesというパッケージをインストールして以下のように使う。
ListContacts.propTypes = {
  contacts: PropTypes.array.isRequired,
  onDeleteContact: PropTypes.func.isRequired,
}
  • Componentを定義したクラス内で定義するには、
static propTypes = {
  contacts: PropTypes.array.isRequired,
  onDeleteContact: PropTypes.func.isRequired,
}

form component

  • フォームに打ち込んだ値をそのままフォームのvalueにするのではなく、一旦stateに収納して、それをsource of truthとして使う。
const { query } = this.state  // state.queryをqueryに取り出して、

updateQuery(query) {
  this.setState(() => ({
    query: query.trim()     // valueがqueryというstateに収納される
  }))
}

render() {(
  <input
    className='search-contacts'
    type='text'
    placeholder='Search Contacts!'
    value={query}
    onChange={(event) => this.updateQuery(event.target.value)}  // evnt.target.valueで入力値が取れる
  />
)}
  • 以下にちょっとハマった。
<form onSubmit={(event) => {                //////  onSubmit()はeventを引数にとる関数を渡す
    event.preventDefault();                 ////// 書かなかいと、画面がrefreshされてstateが初期値に戻った...
    this.props.addItem(this.state.value)
}}>
</form>

yarnコマンド

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを学んだ(1)

Reactの特徴

  • Compositional
  • Declarative code

    • いちいちいちいち処理の詳細まで書かない
    • (e.g.) const longNames = people.filter(name => name.length > 6)
    • (c.f.) Imperative code ではない
  • Unidirectional Dataflowを使う

    • dataflowはparent elementからchildrenの一方向に伝搬される
    • 子は親の持つデータを参照できる。子は親のデータを変更しようと思ったらその旨を親に伝え、親がそれをデータの変更を行う
    • Data-Binding ではない
    • Data-Bindingを使うと、appのどこからでも保持していたデータを変更できる

Reactでelementを作る

  • React.createElement( <tag name>, <propsのhash(=オブジェクト)>, <content in tag> );で、仮想DOMを初期化。
    • e.g. const element = React.createElement('div', null, 'My name is Tyler, and I love porcupines.');
    • creteElementしただけでは、描画されない。これを、ReactDOM.render(element), document.getElementById('root')とかして、DOMに反映する。
    • propsにはhashを渡す。有効なキーはclassNameとかhtmlForとか。
      • DOM要素では、ある要素のclassにアクセスするには、cName = elementNodeReference.className;とするから。

Reactでネストしたelementを作る方法

import React from 'react'
import ReactDOM from 'react-dom'

const people = [
  { name: "John", id: 0 },
  { name: "Paul", id: 1 }
]

const element = React.createElement('ol', null,
  people.map((person) => (
    // 子要素を配列にする時は、propsがそれぞれの要素でユニークなkeyを持っていないと怒られる
    React.createElement('li', {key: people.id}, person.name)
  ))
)

ReactDOM.render(
  element,
  document.getElementById('root')
)
  • JSX(=syntax extention for JS)という拡張構文を使うことで、上記のコードもHTMLチックに、楽に書ける。
// 上記のcreateElementの箇所をJSXで書き換えると、
const element = <ol>
  {people.map((person) => (
    <li key={person.id}>{person.name}</li>
  ))}
</ol>

Class ComponentとStateless Functional Component

image.png
image.png

作ったReact componentクラスをexportする方法

export default ListContacts

初期化時にstateをプロパティーとしてComponentに持たせる方法

class User extends React.Component {
 constructor(props) {
   super(props);
   this.state = {
     username: 'Tyler'
   };
 }
}

Babelでトランスコンパイルするなら以下でもいける。

class User extends React.Component {
 state = {
   username: 'Tyler'
 } 
}
  • Avoid initializing that state with props! (e.g. this.state = {user: props.user})

stateの更新

  • this.state.hoge = new_hogeという風に更新しても、re-renderingされない。
  • 引数の関数の返り値が元のstateにマージされる。
this.setState((prevState) => (
  {count: prevState.count + 1}
)
  • なお、元のstateの値を使う必要がないなら、引数にハッシュを渡せばいい。
this.setState({ count: 2 })

stateの型チェック

  • yarn add prop-types でprop-typesというパッケージをインストールして以下のように使う。
ListContacts.propTypes = {
  contacts: PropTypes.array.isRequired,
  onDeleteContact: PropTypes.func.isRequired,
}
  • Componentを定義したクラス内で定義するには、
static propTypes = {
  contacts: PropTypes.array.isRequired,
  onDeleteContact: PropTypes.func.isRequired,
}

form component

  • フォームに打ち込んだ値をそのままフォームのvalueにするのではなく、一旦stateに収納して、それをsource of truthとして使う。
const { query } = this.state  // state.queryをqueryに取り出して、

updateQuery(query) {
  this.setState(() => ({
    query: query.trim()     // valueがqueryというstateに収納される
  }))
}

render() {(
  <input
    className='search-contacts'
    type='text'
    placeholder='Search Contacts!'
    value={query}
    onChange={(event) => this.updateQuery(event.target.value)}  // evnt.target.valueで入力値が取れる
  />
)}
  • 以下にちょっとハマった。
<form onSubmit={(event) => {                //////  onSubmit()はeventを引数にとる関数を渡す
    event.preventDefault();                 ////// 書かなかいと、画面がrefreshされてstateが初期値に戻った...
    this.props.addItem(this.state.value)
}}>
</form>

yarnコマンド

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React classコンポーネントから関数コンポーネントにModalを変換する

今回使用するModalはこちら。

デモサイトはこちら

クラスコンポーネントでの書き方

modal.jsx
import React from 'react';
import Rodal from 'rodal';
// include styles
import 'rodal/lib/rodal.css';
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { visible: false };
  }
  show() {
    this.setState({ visible: true });
  }
  hide() {
    this.setState({ visible: false });
  }
  render() {
    return (
      <div>
        <button onClick={this.show.bind(this)}>show</button>
        <Rodal visible={this.state.visible} onClose={this.hide.bind(this)}>
          <div>Content</div>
        </Rodal>
      </div>
    );
  }
}

関数コンポーネントでは、thisは使用できない。
パーツに分けて変換していこう!

①Stateパーツ

【classComponent】this.state === 【functionComponent】 State

classComponent.jsx
 constructor(props) {
    super(props);
    this.state = { visible: false };
  }
functinComponent.jsx
 const [visible,setVisible] = useState(false);

②handleパーツ

【classComponent】 this.setState === 【functionComponent】 handle

classComponent.jsx
show() {
    this.setState({ visible: true });
  }
  hide() {
    this.setState({ visible: false });
  }

functinComponent.jsx
   const modelHandleOpen = () => {
    setVisible(true);
  }
  const modelHandleClose = () => {
    setVisible(false);
  }

③Contentsパーツ

classComponent.jsx
 <button onClick={this.show.bind(this)}>show</button>
   <Rodal visible={this.state.visible} onClose={this.hide.bind(this)}>
     <div>Content</div>
   </Rodal>
functinComponent.jsx
  <button className="navItemPopUp" onClick={modelHandleOpen}>アバウト<br/>
      <span><InsertEmoticonIcon/></span></button>
      <Rodal visible={visible} onClose={modelHandleClose}>
          <div>プロフィールの内容</div>
      </Rodal>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む