20210419のReactに関する記事は6件です。

Reactでお手軽にIntersection Observerを使う

はじめに スクロールに連動して要素をふわっとフェードインさせたりしたい場合など、Intersection Observer APIを利用すると実現ができますが、Reactでお手軽に実現したい時には便利なライブラリ、react-intersection-observerがおすすめです。 ライブラリのstorybookサイト https://react-intersection-observer.vercel.app/ Intersection Obserber APIについて https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API インストール $ yarn add react-intersection-observer 今回はアニメーションのcssもanimate.cssを利用して実装します。 $ yarn add animate.css 利用方法 オプションはかなりの数があって使い方も多様ではありますが、スクロールに合わせて要素をふわっと出したい!とうことであれば下記のような方法で簡単に実現ができます。 src/App.tsx import React from 'react'; import { useInView } from 'react-intersection-observer'; import 'animate.css'; const App: React.VFC = () => { const { ref, inView } = useInView({ // オプション rootMargin: '-50px', // ref要素が現れてから50px過ぎたら triggerOnce: true, // 最初の一度だけ実行 }); return ( <div> <div style={{ height: '2000px', backgroundColor: 'blue' }}>contents</div> {/* refを与えた要素がウインドウ内に現れるとinViewがtrueになります */} <div ref={ref} style={{ height: '300px' }}> {inView && ( <div className="animate__animated animate__fadeInUp" style={{ backgroundColor: 'yellow' }} > <p>黄色の要素が出現!</p> </div> )} </div> </div> ); }; export default App; 下にスクロールしていくと、2つ目の白い背景が現れて、50px過ぎたところで黄色の要素がふわっと出現します。 さいごに 用意されているカスタムフックを利用して簡単にスクロール連動のふわっと出現アニメを実装することができました。 他にも多数のオプションがあるのでアニメーションだけでなく様々用途に利用できそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 覚え書き

プロジェクト作成 npx create-react-app react-sample cd react-sample npm start または yarn start コメント jsxの中では、{/* コメント */} を使う。 タグの中は、/* */ // も使える クラスコンポーネント src/App.js import React, { Component } from "react"; class App extends Component { render() { return <p>Hello world {this.props.name}!</p>; } } export default App; 関数コンポーネント シンプルだが、State、ライフサイクルメソッドが使えない Hookを使うことにより、上記問題は解決する import React from "react"; const App = (props) => { return <p>Hello world {props.name}!</p>; }; export default App; props <App name="ichiro"></App> の場合 {this.props.name} = ichiro <App><p>こんにちは</p></App> の場合 {this.props.children} = <p>こんにちは</p> 繰り返し import React from "react"; const App = () => { const users = [{ name: "yamada" }, { name: "suziki" }, { name: "saito" }]; return ( <dl> {users.map((user, index) => ( <ul key={index}>{user.name}</ul> ))} </dl> ); }; export default App; 条件分岐 // 条件演算子 { this.props.isNew ? <NewIcon /> : null } // &&演算子 { this.props.isNew && <NewIcon /> } // 即時関数 {(() => { if (this.props.isNew) { return <NewIcon /> } })} stateを使ったフォーム constructor()の中で、stateの初期化を行う。 this.setState()でstateの更新を行う。 import React, { Component } from "react"; export default class App extends Component { constructor(props) { super(props); this.state = { name: "", prefecture: "", career: "", skill: [""], memo: "", }; this.handleChange = this.handleChange.bind(this); this.handleChangeMulti = this.handleChangeMulti.bind(this); this.file = React.createRef(); // fileを参照できるようにする(Uncontrolled Component) this.show = this.show.bind(this); } handleChange(e) { this.setState({ [e.target.name]: e.target.value, }); } handleChangeMulti(e) { const fs = this.state.skill; if (e.target.checked) { fs.push(e.target.value); } else { fs.splice(fs.indexOf(e.target.value), 1); } this.setState({ [e.target.name]: fs, }); } show() { console.log(`${this.state.name}`); console.log(`${this.state.prefecture}`); console.log(`${this.state.career}`); console.log(`${this.state.skill}`); console.log(`${this.state.memo}`); const f = this.file.current.files[0]; if (f) { console.log(`${f.name} ${f.type} ${f.size}`); } } render() { return ( <form> <label htmlFor="name">名前:</label> <input id="name" name="name" type="text" value={this.state.name} onChange={this.handleChange}></input> <br /> <label htmlFor="prefecture">住所:</label> <select id="prefecture" name="prefecture" value={this.state.prefecture} onChange={this.handleChange}> <option value=""></option> <option value="tokyo">東京都</option> <option value="kanagawa">神奈川県</option> <option value="saitama">埼玉県</option> <option value="chiba">千葉県</option> <option value="ibaraki">茨城県</option> <option value="gunma">群馬県</option> <option value="tochigi">栃木県</option> </select> <fieldset> <legend>経験年数:</legend> <input id="career_zero" name="career" type="radio" value="zero" checked={this.state.career === "zero"} onChange={this.handleChange} /> <label htmlFor="career_zero">未経験</label> <input id="career_one" name="career" type="radio" value="one" checked={this.state.career === "one"} onChange={this.handleChange} /> <label htmlFor="career_one">1年未満</label> <input id="career_three" name="career" type="radio" value="three" checked={this.state.career === "three"} onChange={this.handleChange} /> <label htmlFor="career_three">1~3年</label> <input id="career_five" name="career" type="radio" value="five" checked={this.state.career === "five"} onChange={this.handleChange} /> <label htmlFor="career_five">3年~5年</label> <input id="career_over" name="career" type="radio" value="over" checked={this.state.career === "over"} onChange={this.handleChange} /> <label htmlFor="career_over">5年以上</label> </fieldset> <fieldset> <legend>スキル:</legend> <input id="skill_java" name="skill" type="checkbox" value="java" checked={this.state.skill.includes("java")} onChange={this.handleChangeMulti} /> <label htmlFor="skill_java">Java</label> <input id="skill_php" name="skill" type="checkbox" value="php" checked={this.state.skill.includes("php")} onChange={this.handleChangeMulti} /> <label htmlFor="skill_php">PHP</label> <input id="skill_javascript" name="skill" type="checkbox" value="javascript" checked={this.state.skill.includes("javascript")} onChange={this.handleChangeMulti} /> <label htmlFor="skill_javascript">Javascript</label> <input id="skill_python" name="skill" type="checkbox" value="python" checked={this.state.skill.includes("python")} onChange={this.handleChangeMulti} /> <label htmlFor="skill_python">Python</label> <input id="skill_ruby" name="skill" type="checkbox" value="ruby" checked={this.state.skill.includes("ruby")} onChange={this.handleChangeMulti} /> <label htmlFor="skill_ruby">Ruby</label> </fieldset> <br /> <label htmlFor="memo">メモ:</label> <textarea id="memo" name="memo" value={this.state.memo} onChange={this.handleChange}></textarea> <br /> <label htmlFor="file">ファイル:</label> <input id="file" name="file" type="file" ref={this.file}></input> <br /> <button type="button" onClick={this.show}> 送信 </button> </form> ); } } useStateを使ったフォーム import React, { useState } from "react"; const App = () => { const [name, setName] = useState(""); const [prefecture, setPrefecture] = useState(""); const [career, setCareer] = useState(""); const [skill, setSkill] = useState([]); const [memo, setMemo] = useState(""); const file = React.createRef(); const handleChange = (event) => { switch (event.target.name) { case "name": setName(event.target.value); break; case "prefecture": setPrefecture(event.target.value); break; case "career": setCareer(event.target.value); break; case "memo": setMemo(event.target.value); break; default: console.log("key not found"); } }; const handleChangeMulti = (e) => { if (skill.includes(e.target.value)) { setSkill(skill.filter((item) => item !== e.target.value)); } else { setSkill([...skill, e.target.value]); } }; const show = () => { console.log(name); console.log(prefecture); console.log(career); console.log(skill); console.log(memo); const f = file.current.files[0]; if (f) { console.log(`${f.name} ${f.type} ${f.size}`); } }; return ( <form> <label htmlFor="name">名前:</label> <input id="name" name="name" type="text" value={name} onChange={handleChange}></input> <br /> <label htmlFor="prefecture">住所:</label> <select id="prefecture" name="prefecture" value={prefecture} onChange={handleChange}> <option value=""></option> <option value="tokyo">東京都</option> <option value="kanagawa">神奈川県</option> <option value="saitama">埼玉県</option> <option value="chiba">千葉県</option> <option value="ibaraki">茨城県</option> <option value="gunma">群馬県</option> <option value="tochigi">栃木県</option> </select> <fieldset> <legend>経験年数:</legend> <input id="career_zero" name="career" type="radio" value="zero" checked={career === "zero"} onChange={handleChange} /> <label htmlFor="career_zero">未経験</label> <input id="career_one" name="career" type="radio" value="one" checked={career === "one"} onChange={handleChange} /> <label htmlFor="career_one">1年未満</label> <input id="career_three" name="career" type="radio" value="three" checked={career === "three"} onChange={handleChange} /> <label htmlFor="career_three">1~3年</label> <input id="career_five" name="career" type="radio" value="five" checked={career === "five"} onChange={handleChange} /> <label htmlFor="career_five">3年~5年</label> <input id="career_over" name="career" type="radio" value="over" checked={career === "over"} onChange={handleChange} /> <label htmlFor="career_over">5年以上</label> </fieldset> <fieldset> <legend>スキル:</legend> <input id="skill_java" name="skill" type="checkbox" value="java" checked={skill.includes("java")} onChange={handleChangeMulti} /> <label htmlFor="skill_java">Java</label> <input id="skill_php" name="skill" type="checkbox" value="php" checked={skill.includes("php")} onChange={handleChangeMulti} /> <label htmlFor="skill_php">PHP</label> <input id="skill_javascript" name="skill" type="checkbox" value="javascript" checked={skill.includes("javascript")} onChange={handleChangeMulti} /> <label htmlFor="skill_javascript">Javascript</label> <input id="skill_python" name="skill" type="checkbox" value="python" checked={skill.includes("python")} onChange={handleChangeMulti} /> <label htmlFor="skill_python">Python</label> <input id="skill_ruby" name="skill" type="checkbox" value="ruby" checked={skill.includes("ruby")} onChange={handleChangeMulti} /> <label htmlFor="skill_ruby">Ruby</label> </fieldset> <br /> <label htmlFor="memo">メモ:</label> <textarea id="memo" name="memo" value={memo} onChange={handleChange}></textarea> <br /> <label htmlFor="file">ファイル:</label> <input id="file" name="file" type="file" ref={file}></input> <br /> <button type="button" onClick={show}> 送信 </button> </form> ); }; export default App; ライフサイクルメソッド Mounting Updating Unmounting 用途 constructor(props) 初期化 stateの初期化、thisの固定など render() 描画時 描画時 必須。React要素、文字列値、数値、など componentDidMount() 配置後 リソースの初期化、文書ツリーへのアクセスなど shouldComponentUpdate() 再描画前 再描画前にアクセスしたいとき componentDidUpdate() 再描画後 再描画後にアクセスしたいとき componentWillUnmount() 破棄時 リソースの破棄など ルーティング npm install react-router-dom または yarn add react-router-dom import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; export default function BasicRouter() { return ( <Router> <div> {/* リンク表示部 リンクがここに表示される */} <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/about">About</Link> </li> <li> <Link to="/dashboard">Dashboard</Link> </li> </ul> <hr /> {/* コンポーネント表示部 パスがマッチングしたコンポーネントがここに表示される */} <Switch> <Route exact path="/"> <Home /> </Route> <Route path="/about"> <About /> </Route> <Route path="/dashboard"> <Dashboard /> </Route> </Switch> </div> </Router> ); } function Home() { return ( <div> <h2>Home</h2> </div> ); } function About() { return ( <div> <h2>About</h2> </div> ); } function Dashboard() { return ( <div> <h2>Dashboard</h2> </div> ); } ローカルストレージ ローカルストレージ 値 = localStorade.getItem(キー) localStorade.setItem(キー, 値) JSON⇔オブジェクト変換 オブジェクト = JSON.parse(JSON文字列) JSON文字列 = JSON.stringify(オブジェクト) || [] はnullの場合、[]にする。 // データ取得 componentDidMount() { const todos = JSON.parse(localStorade.getItem('todos')) || []; this.setState({todos: todos}); } // データ1件追加 addTodo(todo) { const todos = this.state.todos; todos.push(todo); this.setState({todos: todos}); localStorade.setItem('todos', JSON.stringify(this.state.todos)); } // データ1件削除 deleteTodo(i) { const todos = this.state.todos; todos.splice(i, 1); this.setState({todos: todos}); localStorade.setItem('todos', JSON.stringify(this.state.todos)); } Fetch API Read List fetch("http://localhost:8080/todos") .then((res) => res.json()) .then((data) => { this.setState({todos: data}) }) .catch((err) => console.log(err)) Read fetch("http://localhost:8080/todos/${id}") .then((res) => res.json()) .then((data) => { this.setState({todo: data}) }) .catch((err) => console.log(err)) Create fetch("http://localhost:8080/todos" ,{ method: "POST", headers: { "Content-type": "application/json" }, body: JSON.stringify(todo) }) .then((res) => res.json()) .then((data) => { // 処理 }) .catch((err) => console.log(err)) Update fetch("http://localhost:8080/todos/${id}" ,{ method: "PUT", headers: { "Content-type": "application/json" }, body: JSON.stringify(todos) }) .then((res) => res.json()) .then((data) => { // 処理 }) .catch((err) => console.log(err)) Delete fetch("http://localhost:8080/todos/${id}" ,{ method: "DELETE" }) .then((res) => res.json()) .then((data) => { // 処理 }) .catch((err) => console.log(err)) 今後追加したいこと Redux Hook Spring Bootなどへの組み込み 参考 速習 React 速習シリーズ Kindle版 https://reactrouter.com/web/guides/quick-start
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js + Rails + Firebase Authでjwt認証を実装しよう(Email編)

はじめに JWT認証に関して、Next.js Rails Firebase Authenticationという組み合わせでの実装をした記事があまりないとのことなので、今回は実際にやっていこうと思う。今回はEmailでの実装になるため、Google認証で実装をしたいという方は同シリーズの https://qiita.com/satopin/items/fa0c35a0ba69a379683e をご覧いただければ幸いである。 ソースコードはこちらから https://github.com/yumaasato/my-mahjong (まだアプリ自体は作成中なので悪しからず・・・) 環境 Ruby: 2.6.5 Rails 6.0.3.6 TypeScript firebase-auth-rails Redis-server *ちなみに今回、user認証でよく用いられるdeviseは必須ではありません。 今回は使わずに実装を進めていきます。 また、今回redis-serverのインストールからconfig/initializers/firebase_id_token.rbで設定を行うまでは https://qiita.com/satopin/items/fa0c35a0ba69a379683e  と同じのため、3-1までは飛ばしてもらって構わない。 1. Redis-serverのインストール mac版の場合 $ brew install redis-server Linuxでは $ sudo apt install redis-server 内部でfirebase_id_tokenを使っているためこのRedisをインストールする必要があるとのことです。 2. firebase-auth-railsの追加 今回、firebase-auth-railsというgemを利用することでjwt認証を比較的簡単に行うことができます。 Gemfile gem 'firebase-auth-rails' を追加します。 そして、 $ bundle install を実行します。 3-1. 実装(プロジェクトの設定) まずはじめに、Reidsとfirebaseプロジェクトの設定をおこないます。 config/initializers/firebase_id_token.rb FirebaseIdToken.configure do |config| config.redis = Redis.new config.project_ids = [ENV['FIREBASE_PROJECT_ID']] end 3-2.Userテーブルの設計 今回は、Userのnameをクライアント側で登録せずにEmailとPasswordのみの認証を行う。 そのため、Userテーブルの編集を行う必要がある。 以下は一例である。 db/migrate/****_create_users.rb class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :password_digest t.string :name, default: "" # null: falseとnameが必須になるので今回は外す t.string :email, default: "" t.string :uid, null: false, default: "" t.timestamps t.index :uid, unique: true # 同じuidによる登録を防ぐ end end end ここで、uidを null:falseにしておくことで、Firebase Authで認証したUserをRails側で受け取れるようにしておく。 3-3.ユーザー登録用のコントローラを実装する 以下はあくまで一例である app/controllers/api/v1/auth/users_controller.rb require_dependency 'api/v1/application_controller' module Api module V1 module Auth class UsersController < V1::ApplicationController skip_before_action :authenticate_user def index # ユーザー一覧を取り出す users = User.order(created_at: :desc) render json: { status: 'SUCCESS', message: 'Loaded users', data: users } end def create # ユーザーを作成する FirebaseIdToken::Certificates.request raise ArgumentError, 'BadRequest Parameter' if payload.blank? @user = User.find_or_initialize_by(uid: payload['sub']) do |user| user.email = payload['email'] end if @user.save render json: @user, status: :ok else render json: @user.errors, status: :unprocessable_entity end end private def token params[:token] || token_from_request_headers end def payload @payload ||= FirebaseIdToken::Signature.verify token end end end end end Google認証では、 @user = User.find_or_initialize_by(uid: payload['sub']) do |user| user.name = payload['name'] end としたが、これでは、payloadでUserのnameが要求される。今回Userのnameは使わずに、Emailのみの認証を行うため、 @user = User.find_or_initialize_by(uid: payload['sub']) do |user| user.email = payload['email'] end とする。ちなみに user.password= payload['password']とする必要はない。Passwordを暗号化した文字列ならわかるが、データベースにpasswordを保管するのはセキュリティ的に問題があるからだ。 3-4 Routeの追加 Rails.application.routes.draw do namespace 'api' do namespace 'v1' do resources :players resources :games, only: %i(index) namespace 'auth' do post 'users' => 'users#create' #追記箇所 get 'users' => 'users#index' #追記箇所 end end end end 今回の場合、Firebase Authを介してUserの認証を行なっているため、上記のようにする。 ちなみに、get 'users' => 'users#index'は、User一覧をみれるようにするために設定した。 4-1. クライアント側の実装 次にNext.js側の実装を行う。フロント側の実装方法が省かれている記事が多いが、ここではNext側についても実装方法を記述する。あくまで参考(一部省略)だが、 pages/sign_in/Auth.tsx // Eメール認証 const signUpEmail = async () => { await auth.createUserWithEmailAndPassword(email, password).catch((err) => alert(err.message)); router.push('/') }; // 認証後Rails側にリクエストを送る const handleEmailsignUp = () => { const request = async () => { await signUpEmail(); const auth = getAuth(); const currentUser = auth.currentUser; // Firebase Authの認証 if (auth && auth.currentUser) { const token = await currentUser.getIdToken(true); const config = { token }; // Rails側にリクエストを送る try { await axios.post('/api/v1/auth/users', config); } catch (error) { console.log(error); } } }; request(); }; return ( <> <TextField variant="outlined" margin="normal" required fullWidth id="email" label="Email Address" name="email" autoComplete="email" value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value); }} /> <TextField variant="outlined" margin="normal" required fullWidth name="password" label="Password" type="password" id="password" autoComplete="current-password" value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value); }} /> <Button disabled={ isLogin ? !email || password.length < 6 : !email || password.length < 6 } fullWidth variant="contained" color="primary" className={classes.submit} startIcon={<EmailIcon />} onClick={ isLogin ? async () => { try { await handleEmailLogin(); } catch (err) { alert(err.message); } } : async () => { try { await handleEmailsignUp(); } catch (err) { alert(err.message); } } } > {isLogin ? "Login" : "Register"} </Button> </> ) これでButtononClick 5-1. 実装後のオペレーション(重要) 実装後、$ rails cとして、firebase_id_tokenのDownloading Certificates以下に記載してある内容を実行する必要がある。 irb(main):001:0>FirebaseIdToken::Certificates.request ・ ・ ・ irb(main):002:0>FirebaseIdToken::Certificates.present? => true となってはじめて、jwt認証を使うことができる。 注意点 この実装ではnameを使わずに実装をおこなったため、クライアント側でnameを入れて認証を行なった際には、エラーが起こる可能性がある。nameを入れて実装を進めたいという場合には、別の方法を参照してもらいたい。 また、Railsにおいて、.envファイルで環境変数を管理する際には、 gem 'dotenv-rails' をインストールする必要があるため、忘れずに設定しておこう。 (これで認証に時間がかかったので・・・) さらに、認証を行う際には、Railsサーバーとクライアント側のサーバーに加えて、$ redis-serverでRedis-serverを起動する必要があるため、忘れないようにしよう。 終わりに 実装だけでもかなりの時間をかけた上に、Next.js(TypeScript) + Rails + Firebase Authの組み合わせでのjwt認証の記事が少ないということで今回記事を書くことになった。この記事が一人でも多くの人の参考になれば幸いである。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React(hooks)にて子コンポーネントから親コンポーネントのStateを変更する

はじめに Hooksで管理する親コンポーネントのStateを子コンポーネントから変更する方法を模索し解決したので備忘録として 実装 親コンポーネント App.jsx const App = () => { const [count,setCount] = useState(0) const Up = () => { setCount(count+1) } return( <div> <p>Clicked {count} times</p> <button onClick={Up}> Push! </button> <Child count={count} onClick={Up}/> </div> ) } export default App; 子コンポーネント Child.jsx import React from "react"; const Child = (props) => { const Up = () => { return( props.countUp ) } return ( <div> <button onClick={props.onClick}> Push! </button> </div> ); }; export default Child; 結果 親コンポーネントで実装したボタン 子コンポーネントで実装したボタン どちらのボタンを押しても親コンポーネントのStateが変更されカウント回数は連動して表示される 言葉で説明すると、、 親コンポーネントでStateをセットし(hooks)、Stateの変更メソッドを実装 変更メソッドをpropsとして子コンポーネントに渡し、実装 おわりに 公式ドキュメントでHooksを学習中に「関数コンポーネントで子コンポーネントから親コンポーネントのStateを変更するにはどうすればいいのか」と疑問に感じ実装してみた あくまで実装したみた!なので実際にアプリ開発を行う際は非効率的であったり推奨されない実装の可能性は大いにある (Redux使えば悩むまでもない話)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリケーションを途中からReactのSPAに書き換える際に設定したこと

この記事の内容 既存Railsアプリを徐々にSPAに置き換えていく、という課題を研修でやりました。 Reactの設定ファイルは先輩が書いてくださったので、その内容を読み解きながら、RailsアプリにReactを導入していく流れを確認しようと思います。 なお、実行環境は以下の通りです。 Rails 6.0.3 React 17.0.2 Rails6系なのでWebpackerはデフォルトで入っているものとします。 ディレクトリ構成 今回の(最終的な)ディレクトリ構成は下記の通りです。関連のあるところだけ記載しています。なお、ここに記載のファイル以外に、config/routes.rbも編集しています。 . ├── controllers │   └── spa │      └── spa_roots_controller.rb └── javascript    ├── App.jsx    ├── Routes.jsx    ├── components    │   └── Bar.jsx    ├── packs    │   ├── application.js    │   ├── root_application.jsx    │   └── server_rendering.js    └── pages    └── Foo.jsx ゴール画面 今回のゴール像は以下のイメージです。 Fooページの中にBarコンポーネントを表示したいと思います。 Reactをインストール まず、gem react-railsをインストールします。 Gemgile gem 'react-rails' このGemを利用する以外の方法も調べはしたのですが、今回はコードの読み解きに留めることにしました。 参考:ReactをRailsと共に使う方法 なお、このgemは先に紹介した記事によると、「RailsのAsset Pipelineを利用してJSXをRailsが認識できる形に処理し」てくれるそうです。 公式サイトの記載通りに、コマンドを実行します。 $ bundle install $ rails webpacker:install $ rails webpacker:install:react $ rails generate react:install その結果、以下のファイルが生成・追記されます。 app/javascript/components/ # Reactのコンポーネント用に生成 app/javascript/packs/application.js # ReactRailsUJSの記載が追記 app/javascript/packs/server_rendering.js # サーバーサイドのレンダリング用に生成 ここで、application.jsとserver_rendering.jsには同じ内容が記載されていましたが...。 var componentRequireContext = require.context("components", true); var ReactRailsUJS = require("react_ujs"); ReactRailsUJS.useContext(componentRequireContext); require.context("components", true);は、/componentディレクトリ下の全てのファイルを拾うという設定、useContext(設定)は、設定に書かれた要素がグローバルな要素として使われるという記載のようです。 それぞれ、以下を参考にしました。 Webpackでフォルダ内の全ファイルを一気にrequireする React hooksを基礎から理解する (useContext編) RailsのルーティングとControllerの編集 次にRails側の、設定ファイルを書いていきます。 まずはconfig/routes.rbに下記のように記載します。 config/routes.rb scope 'spa' do get '*path', to: 'spa_roots#show' end こちらの記述によって、/spa/*で始まるリクエストが来たらなんでも、spa_rootsコンロトーラーのshowアクションに飛ぶようにします。 (namespaceではなくてscopeである理由は、先輩が参考記事を貼ってくれていました。) Railsのroutingにおけるscope / namespace / module の違い そして、spa_routes_controllerには、ほぼ何も書かず...。 app/controllers/spa_roots_controller.rb class SpaRootsController < ApplicationController def show; end end 代わりに、spa_roots#showのViewの方にエントリーポイントとなるファイルを読み込む記載を入れます。 app/views/spa/spa_roots/show.slim = javascript_pack_tag 'root_application' この時読み込んでいるroot_applicationには何が書かれているかというと、以下のとおりです。 app/javascript/packs/root_application.jsx import { App } from '../App' import React from 'react' import ReactDOM from 'react-dom' document.addEventListener('DOMContentLoaded', () => { ReactDOM.render( <App />, document.body.appendChild(document.createElement('div')) ) }) ReactDOM.renderは以下のような形で使われ、React公式サイトによると、 ReactDOM.render(element, container[, callback]) 渡された container の DOM に React 要素をレンダーし、コンポーネントへの参照(ステートレスコンポーネントの場合は null)を返します。 とのことですので、 document.body.appendChild(document.createElement('div') の部分で、bodyの末尾に作られた<div>タグに<App />をレンダーさせます。 参照:ReactDOM#リファレンス Routerの導入 では、先程記載したこの<APP />には何が書かれているのかというと、以下の内容です。 app/javascript/App.jsx import React from 'react' import { BrowserRouter as Router } from 'react-router-dom' import { Routes } from './Routes' export const App = () => { return ( <Router> <Routes /> </Router> ) } Routerが出てきましたね。実は、順番が前後してしまいましたが、この少し前にreact-router-domをインストールしました。 yarn add react-router-dom そして、app/javascript/App.jsxではBrowserRouter(Routerという名前で使用)とRoutesというライブラリを読み込んでいます。 それぞれのライブラリに関して深入りはしませんが、 BrowserRouter ... ルーティングの切り替え機能を提供 Routes ... それぞれのルーティングを提供 しているようです。 参考:【React】ルーティング設定方法 試しにページを表示 そして、この<Routes />には次のように書いてありました。<Switch />を使って、リクエストごとに表示する内容を切り替えていきます。 APIドキュメント:Switch app/javascript/Routes.jsx import React from 'react' import { Switch, Route } from 'react-router-dom' import { Foo } from '~/pages/Foo' export const Routes = () => { return ( <Switch> <Route path="/spa/foo"> <Foo /> </Route> </Switch > ) } ここまでで、Fooコンポーネントは表示されます。 この、Fooコンポーネント内で、さらにBarコンポーネントを呼び出せば、完成です。 app/javascript/pages/Foo.jsx import React from 'react' import { Bar } from '~/components/Bar' export const Foo = () => { return ( <div> <h1>これはfooページです。</h1> <Bar /> </div> ) } 完成! 以上で、RailsにReactを途中から導入する設定が完了です。 いやー、、、先輩いなかったら完全に彷徨っていたと思います。。。 まだまだJSはわからないことが多いので、これからも地道に調べながら理解を深めていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + react-dropzone: ファイルをページにドラッグ&ドロップする

react-dropzoneとは react-dropzoneは、ローカルのファイルをドラッグ&ドロップやダイアログで選んで扱うためのライブラリです。ファイルをアップロードするユーザーインタフェースなどに使えます。アップデートは小まめで、本稿執筆時のバージョンはv11.3.2です。最小限のコード例はとても短く、APIにはフックも用いられています(サンプル001)。 サンプル001■最小限のコード例 >> CodeSandboxへ インストール コマンドラインツールで、npmやyarnを使ってインストールしてください。 npm install --save react-dropzone yarn add react-dropzone 本稿の作例は、Create React Appのひな形アプリケーションをもとにつくっています。ひな形のつくり方については「Reactアプリケーションのひな形をつくる」をお読みください。 コード例を試す 使い方(Usage)に掲げられているコード例は、わずか20行足らずです。それでも、コードをコピー&ペーストすれば、最小限のユーザーインタフェースができ上がります。 それに少しだけ、スタイルなどの手を加えたのがつぎのコード001です。コードはたしかに短い。でも、ちょっと何やってるかわからないですね。まずは、冒頭のサンプル001のCodeSandboxコード例で、どういう動きになるのかをご覧ください。 コード001■最小限のコード例 src/App.js import { useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; const style = { width: 200, height: 150, border: "1px dotted #888" }; function App() { const onDrop = useCallback((acceptedFiles) => { // Do something with the files console.log('acceptedFiles:', acceptedFiles); }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); return ( <div {...getRootProps()} style={style}> <input {...getInputProps()} /> { isDragActive ? <p>Drop the files here ...</p> : <p>Drag 'n' drop some files here, or click to select files</p> } </div> ); } export default App; フックuseDropzoneに引数として{ onDrop }が与えられています。すると、ページ内の領域(<div>要素)にファイルをドロップしたとき、コンポーネントのonDropに定めたコールバックが呼び出されます。コールバックに渡される引数は、ドロップしたFileオブジェクトが収められたFileListです。ここでは、コンソールに出力して中身を確かめています。 useDropzoneから取り出したisDragActiveは、領域にファイルがドラッグされているかどうかを調べる論理値です。前掲コード001では、ドラッグすると表示されるテキスト(p要素)が切り替わります。 getRootProps()の戻り値をコンソールに出力してみました。中身にはつぎのようなイベントハンドラが含まれています。これらがページの領域に加えられ、その中のひとつonDropがコンポーネントに定めたコールバックを呼び出したのです。 onBlur: ƒ (event) onClick: ƒ (event) onDragEnter: ƒ (event) onDragLeave: ƒ (event) onDragOver: ƒ (event) onDrop: ƒ (event) onFocus: ƒ (event) onKeyDown: ƒ (event) ref: {current: null} tabIndex: 0 getInputProps()の戻り値は、つぎのとおりでした。<input>要素の属性とイベントハンドラが含まれているようです。 accept: undefined autoComplete: "off" multiple: true onChange: ƒ (event) onClick: ƒ (event) ref: {current: null} style: {display: "none"} tabIndex: -1 type: "file" スタイルを定める 前掲コード001(サンプル001)では、ページの領域(<div>要素)のスタイルをオブジェクトにしてReactおなじみのstyleプロパティに与えました。同じことは、getRootProps()の引数オブジェクトにstyleプロパティとして渡しても実現できます。 src/App.js function App() { return ( // <div {...getRootProps()} style={style}> <div {...getRootProps({ style })}> </div> ); } ページ内の領域のスタイルは、ドラッグしたとき動的に変えてみましょう。ドラッグしているかどうかは、isDragActiveで調べられました。領域にドラッグしたときのスタイル(borderDragStyle)には、軽くアニメーション(transition)も加えています。なお、useMemoを用いたメモ化については、「Create React App 入門 08: useMemoフックで無駄な再計算を省く」をお読みください。 src/App.js // import { useCallback } from 'react'; import { useCallback, useMemo } from 'react'; // const style = { const baseStyle = { // border: "1px dotted #888" }; const borderNormalStyle = { border: "1px dotted #888" }; const borderDragStyle = { border: "1px solid #00f", transition: 'border .5s ease-in-out' }; function App() { const style = useMemo(() => ( { ...baseStyle, ...(isDragActive ? borderDragStyle : borderNormalStyle)} ), [isDragActive]); } これで、ファイルをドラッグすると領域の枠線スタイルが動的に変わります(サンプル002)。 サンプル002■ドラッグした領域のスタイルが動的に変わる >> CodeSandboxへ ダイアログはボタンで開く ドラッグ&ドロップはいいとして、ダイアログを開くのはボタンの方がわかりやすそうです。useDropzone()の引数に{ noClick: true }を渡すことにより領域クリックでダイアログが開くのは止め、開くための関数openを取り出します。そして、ボタン(<button>要素)のonClickハンドラからopenを呼び出すようにしたのがつぎのコードです。 src/App.js function App() { // const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ noClick: true }); return ( <div {...getRootProps({ style })}> <button type="button" onClick={open}>Select files</button> </div> ); } ドロップしたファイルの情報を得る ドロップしたファイルのFileListオブジェクトは、useDropzoneフックからacceptedFilesとして得られます。この中から取り出したFileオブジェクトそれぞれのファイル名(path)とサイズ(size)をページに差し込んだのがつぎのコードです。 src/App.js function App() { // const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ const { getRootProps, getInputProps, isDragActive, open, acceptedFiles } = useDropzone({ }); const files = useMemo(() => acceptedFiles.map(file => ( <li key={file.path}> {file.path} - {file.size} bytes </li> ) ), [acceptedFiles]); return ( <div className="container"> <div {...getRootProps({ style })}> </div> <aside> <h4>Files</h4> <ul>{files}</ul> </aside> </div> ); } 書き上がったルートモジュールsrc/App.jsの記述全体をまとめたのが、以下のコード002です。コードの動きは、CodeSandboxに掲げたサンプル003でお確かめください。 サンプル003■ドロップしたファイルの情報がページに示される >> CodeSandboxへ コード002■領域のスタイルやボタンを加えたファイル選択のインタフェース src/App.js import { useCallback, useMemo } from 'react'; import { useDropzone } from 'react-dropzone'; const baseStyle = { display: "flex", flexDirection: "column", width: 200, height: 150, }; const borderNormalStyle = { border: "1px dotted #888" }; const borderDragStyle = { border: "1px solid #00f", transition: 'border .5s ease-in-out' }; function App() { const onDrop = useCallback((acceptedFiles) => { // Do something with the files console.log('acceptedFiles:', acceptedFiles); }, []); const { getRootProps, getInputProps, isDragActive, open, acceptedFiles } = useDropzone({ onDrop, noClick: true }); const style = useMemo(() => ( { ...baseStyle, ...(isDragActive ? borderDragStyle : borderNormalStyle)} ), [isDragActive]); const files = useMemo(() => acceptedFiles.map((file) => ( <li key={file.path}> {file.path} - {file.size} bytes </li> ) ), [acceptedFiles]); return ( <div className="container"> <div {...getRootProps({ style })}> <input {...getInputProps()} /> { isDragActive ? <p>Drop the files here ...</p> : <p>Drag 'n' drop some files here</p> } <button type="button" onClick={open} className="btn btn-primary align-self-center">Select files</button> </div> <aside className="mt-1"> <h4 className="mb-0">Files</h4> <ul>{files}</ul> </aside> </div> ); } export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む