- 投稿日:2019-12-21T23:55:56+09:00
1年業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- Javascript (業務ではTypeScriptを使っています)
- React
- Redux
- axious
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
ChromeやFirefox, Safariなどのブラウザに対応
ブラウザからXMLHttpsRequestsを作成
リクエストとレスポンスデータを変換
Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッドを定義しています。componentDidMount
メソッドは、Reactのライフサイクルメソッドの一種で、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なっています。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、完結なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
todo_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
半年間業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
ChromeやFirefox, Safariなどのブラウザに対応
ブラウザからXMLHttpsRequestsを作成
リクエストとレスポンスデータを変換
Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッドを定義しています。componentDidMount
メソッドは、Reactのライフサイクルメソッドの一種で、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
todo_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
- ChromeやFirefox, Safariなどのブラウザに対応
- リクエストとレスポンスデータを変換
- Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種です。出力がcomponentDidMount
メソッドは、DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
todo_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
React+Railsでモダン()なTODOアプリを作ってみた ~ 備忘録も兼ねて ~
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
- ChromeやFirefox, Safariなどのブラウザに対応
- リクエストとレスポンスデータを変換
- Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種です。componentDidMount
メソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルのレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
controllers/todos_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
config/routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
React+Railsでモダン()なTODOアプリを作ってみた 〜備忘録も兼ねて〜
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったSPAの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
- ChromeやFirefox, Safariなどのブラウザに対応
- リクエストとレスポンスデータを変換
- Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種です。componentDidMount
メソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルのレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
controllers/todos_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
config/routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:44:14+09:00
react-trackedを使ってreduxのような機能を作ってみる
はじめに
多くの方が既にuseContextやuseReducerを使い、Reduxのようなprops-drillingを回避するようなHooksを作っています。そこで今回はreact-trackedを使いReduxが解決した機能の一つであるpropsdrilling回避を実現したいと思います。
react-trackedの利点については@daishiさんのreact-trackedの紹介という記事をご参照ください。react-trackedを使ってみる
今回はカウンターアプリを作っていきたいと思います。
ファイル構成
今回のファイル構成は以下です。store.jsにglobalに置きたい変数、reducerを置きます。
src |-- index.js |-- App.js |-- store.js |-- Components |-- Counter.js |-- Display.jsstore.jsに初期値を設定&Reducerを置き、Providerを定義する
今回はカウンターアプリなので、以下のように設定します。
src/store.jsimport { useReducer } from "react"; import { createContainer } from "react-tracked"; const initialState = { count: 0 }; const reducer = (state, action) => { switch (action.type) { case "INCREMENT_COUNT": return { ...state, count: state.count + 1 }; case "DECREMENT_COUNT": return { ...state, count: state.count - 1 }; default: throw new Error(); } }; const useValue = () => useReducer(reducer, initialState); export const { Provider, useTrackedState, useUpdate: useDispatch } = createContainer(useValue);ProviderをApp.jsに適用し、変数、reducerにアクセスできるようにする
Providerでコンポーネント全体をラップします。各コンポーネントに関しては下で説明します。
src/App.jsimport React from "react"; import Display from "./Components/Display"; import Counter from "./Components/Counter"; import { Provider } from "./store"; const App = () => { return ( <Provider> <Display /> <hr /> <Counter /> </Provider> ); }; export default App;Count機能を作るCounterコンポーネントとCountの値表示機能を作るDisplayコンポーネントを作成。
今回の作ったコンポーネントはComponentsディレクトリにまとめておきます。
まずはCount機能src/Components/Counter.jsimport React from "react"; import { useDispatch } from "../store"; const Counter = () => { const dispatch = useDispatch(); return ( <div> <button onClick={() => dispatch({ type: "INCREMENT_COUNT" })}> Plus </button> <button onClick={() => dispatch({ type: "DECREMENT_COUNT" })}> Minus </button> </div> ); }; export default Counter;次に値表示機能
src/Components/Display.jsimport React from "react"; import { useTrackedState } from "../store"; const Display = () => { const state = useTrackedState(); return <div>count : {state.count}</div>; }; export default Display;完成品
完成品についてはこのCodeSandboxのリンクから見ることができるので、動作を確認してみてください。
最後に
カウンターアプリだと結構簡単に作ることができましたが、正直これだけだとreact-truckedの「不要なrenderがパフォーマンス低下を引き起こすのを防ぐ」という面は紹介できなかったかなと思います。
詳細については上で紹介した@daishiさんのreact-trackedの紹介という記事やreact-trackedのチュートリアルを見るとよくわかると思います。参考文献
今回カウンターアプリを作るにあたり以下の記事を参考にさせていただきました。ありがとうございました。
- 投稿日:2019-12-21T23:43:03+09:00
Firebase + Firestore + React でWebアプリのユーザーグループ機能の土台を考える
はじめに
この記事はFirebase #2 Advent Calendar 2019の21日目の記事です。
タイトル通り,Webアプリにおけるユーザーグループ機能について考えた内容を書いていきます.この記事を書いたモチベーションは,自分が開発するWebアプリでユーザーグループ機能を実装する必要が出てきたものの,ユーザー情報がダイレクトに関わるものなので,自分の中でちゃんと整理しながらやらないと不安極まりなく,頭の整理も兼ねて書きました.また,FirebaseとFirestoreのみでどう実装できるかを試したかったというのもあります.(機能はまだ検討中)
しかし,予想以上に検討すべき事項が多かったことから(当たり前だが),機能と設計の全体像については複数記事に分けて書くことにしました.
本記事はとりあえず土台を考えてみた程度に止まっており,下記の様な内容をカバーできていません.
- 登録時のメールアドレスの所有確認
- 各ユーザーの役割や管理者権限の変更
- 複数グループへの所属管理
- Firestoreルールのあるべき姿
- etc... (サンプルコードのanyなどは追って書き直します...)
使ったサービス+技術
- Firebase Hosting
- Firebase Authentication
- Firestore
- React.js
- material-ui
どんな機能を作るか?
土台なので,まずはユーザーグループを管理する上で最低限必要となりそうな機能のみ考えています.
それらの機能の大枠とフローは下記の通りです.この記事でやっていること
ユーザーグループの作成から新規ユーザーの追加まで,です.
今回のユーザーグループ作成の機能では,誰でもユーザーグループを作成できるものの,ユーザーグループを作ったユーザー=管理者という形になっています.また,グループに新しいユーザーを追加できるのもグループを作った管理者のみとなっています.
新規ユーザーグループの作成・管理者登録
0. Firebaseの設定
公式のクイックスタートと変わりないですが,とりあえずこれがなければ始まらないので.
Google Providerやfunctionsは追々使うので入ってますが,本記事では使ってません.configFirebase.tsximport firebase from "firebase/app"; import "firebase/firestore"; import "firebase/functions"; import "firebase/auth"; firebase.initializeApp({ apiKey: process.env.REACT_APP_FIREBASE_APIKEY, authDomain: process.env.REACT_APP_FIREBASE_AUTHDOMAIN, projectId: process.env.REACT_APP_GCP_PROJECTID, }); export default firebase; export const providerGoogle = new firebase.auth.GoogleAuthProvider(); export const firestore = firebase.firestore(); export const functions = firebase.functions(); export const { FieldValue } = firebase.firestore;1. トップページ
これはソースいらないと思いますが一応.
TopPage.tsximport React from "react"; import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; import { Button } from "@material-ui/core"; /* 省略 */ const TopPage: React.FC = () => { const classes = useStyles(); return ( <div> <Button className={classes.button} href="/signUp"> ユーザーグループの作成 </Button> <Button className={classes.button} href="/login"> ログイン </Button> </div> ); };2. ユーザーグループ・管理者アカウントの作成
とりあえず必要最低限の情報を取得し,
firebase.auth().createUserWithEmailAndPassword()
に投げてユーザーグループの管理者となるアカウントの登録を行っています.
なので,ここではユーザーグループという箱を作ってる訳ではなく,グループを作れるアカウントを作成してるだけです.注)
本来はここでメールアドレスの所有確認を前段に挟みますが,私の都合で省略しています.
メールアドレスの本人確認メールを投げ,継続URLから登録に進む,,,という流れは追って修正するか,別記事書いてこちらにリンク貼ります.
(公式だとこの辺の機能ですね.https://firebase.google.com/docs/auth/web/passing-state-in-email-actions?hl=ja)
ここはGoogleやFacebookのProviderを使っても良いですが,個人都合でメアド&パスワードにしています.ソースは各機能に関する部分を抜粋して載せていきます.
本筋に関係のない部分は割愛しています.SignUpForm.tsxconst SignUpForm: React.FC = () => { const classes = useStyles(); const [email, setEmail] = useState<string>(""); const [emailError, setEmailError] = useState<string>(""); const [emailCheck, setEmailCheck] = useState<string>(""); const [emailCheckError, setEmailCheckError] = useState<string>(""); const [password, setPassword] = useState<string>(""); const [complete, setComplete] = useState<boolean>(false); const signUp = (): void => { firebase .auth() .createUserWithEmailAndPassword(email, password) .then(() => { console.log("Success create user."); }) .catch((error: any) => { console.log(error.code); console.log(error.message); if (`${error.message}`.indexOf("already") !== -1) { alert("既に登録済のメールアドレスです."); } }); }; const handleEmailChange = (event: any) => { setEmail(event.target.value); const error = event.target.validationMessage; if (error) { setEmailError(error); } else { setEmailError(""); } }; const handleEmailCheckChange = (event: any) => { setEmailCheck(event.target.value); }; useEffect(() => { if (email.length > 2) { if (email !== emailCheck) { setEmailCheckError("* メールアドレスが一致していません"); } else { setEmailCheckError("* メールアドレスが一致しました"); } } }, [email, emailCheck]); const handlePasswordChange = (event: any) => { setPassword(event.target.value); }; const handleSubmit = (event: any) => { if (complete) { event.preventDefault(); signUp(); } }; useEffect(() => { const regex = RegExp( /^(?=.*?[a-z])(?=.*?\d)(?=.*?[!-\/:-@[-`{-~])[!-~]{8,100}$/i ); console.log(`EmailError: ${emailError}`); if ( emailError.length === 0 && email === emailCheck && regex.test(password) ) { console.log("the email and password is good."); setComplete(true); } else { console.log("the email and password is bad."); setComplete(false); } }, [email, emailCheck, password]); return ( <div> <Card className={classes.mainCard}> <Typography variant="h5"> ユーザーグループの管理者アカウントを作成 </Typography> <form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off" > <TableContainer> <Table className={classes.tableRoot}> <TableHead> <TableRow> <TableCell>入力項目</TableCell> <TableCell /> </TableRow> </TableHead> <TableBody> <TableRow> <TableCell> <TextField required className={classes.textField} id="standard-email" label="メールアドレス" onChange={handleEmailChange} type="email" value={email} /> </TableCell> <TableCell /> </TableRow> <TableRow> <TableCell> <TextField required className={classes.textField} id="check-email" label="メールアドレス(確認用)" onChange={handleEmailCheckChange} type="email" value={emailCheck} /> </TableCell> <TableCell> <Typography className={classes.errorCaption} variant="caption" style={ email === emailCheck ? { color: "green" } : { color: "red" } } > {emailCheckError} </Typography> </TableCell> </TableRow> <TableRow> <TableCell> <TextField required className={classes.passwordField} id="standard-password" label="Password" type="password" autoComplete="current-password" onChange={handlePasswordChange} /> </TableCell> <TableCell> <Typography className={classes.caption} variant="caption"> <p>* 半角英数字と記号をそれぞれ1つ以上含めてください</p> <p>* 8文字以上100文字以下</p> </Typography> </TableCell> </TableRow> </TableBody> </Table> </TableContainer> <p> <Button className={classes.button} type="submit" variant="contained" disabled={!complete} > 管理者アカウントを作成 </Button> </p> </form> </Card> </div> ); };入力フォームに必要な情報を入力してボタンを押せば,firebase authenticationのページにて,入力したアドレスでユーザーが作成されています.
3. 管理者登録後の遷移
ユーザーグループの作成(というより管理者登録)を終えたら,
firebase.auth().onAuthStateChanged()
で認証状態の変化を検知し,ユーザーグループの管理画面へ遷移させます.
(前述のソースコードではルーティング部分の処理を省略してるので,自動で遷移はしません)管理者の初回ログインでは,グループの識別に使うグループUIDの作成と,管理者権限の管理をfirestoreで行えるよう役割を付与しています.本記事ではグループUID生成をカバーできていないので,これまた追々更新したいところ...
2回目以降のログインや,管理者権限を持たないユーザーのログイン時は,firestoreからユーザーが所属するグループや役割を取得する形になっています.
App.tsximport React, { useState, useEffect } from "react"; import firebase, { firestore, FieldValue } from "./configFirebase"; import SignUpForm from ".SignUpForm"; import TopPage from "./TopPage"; import GroupPage from "./GroupPage"; import LoginPage from "./LoginPage"; import AddUserPage from "./AddUserPage"; const App: React.FC = () => { const [loginUser, setUser] = useState<any | null>(null); const [isLoading, setLoading] = useState<boolean>(false); // 初回ロード時 useEffect(() => { setLoading(true); // firebase authenticationの機能で認証 firebase.auth().onAuthStateChanged((user: any) => { // 登録済みのユーザーで認証できた場合 if (user) { // firestoreにアクセス // 初回ログインではここで管理者のユーザー情報をfirestoreにsetする(管理者ログイン以外は登録時にユーザー情報をもつ) // 2回目以降のログインでは,ここでユーザー情報を取得する Promise.resolve(50) .then(() => { firestore .collection("users") .doc(`${user.uid}`) .get() .then((doc: any) => { if (doc.data()) { console.log("already registed User Logined."); const { userName, role, groupUID } = doc.data(); setUser({ userName, role, groupUID }); } else { const docRef = firestore .collection("users") .doc(`${user.uid}`); docRef.set({ email: user.email, userName: "Admin User", role: "admin", timestamp: FieldValue.serverTimestamp(), groupUID: "test" // UIDを生成(後日追記,,,), }); } }); }) .then(() => setLoading(false)); } else { setLoading(false); // signUpPageへリダイレクト } }); }, []); return ( <Router> <Switch> <Route exact path="/top" component={() => <TopPage />} /> <Route exact path="/login" component={() => <LoginPage />} /> {loginUser ? ( <Switch> <Route exact path="/groupPage" component={() => <GroupPage user={loginUser} />} /> <Route exact path="/addUser" component={() => <AddUserPage userRole={loginUser.role} groupUID={loginUser.groupUID} />} /> <Redirect to="groupPage" /> </Switch> ) : ( <Route exact path="/signUp" component={() => <SignUpForm />} /> )} </Switch> </Router> ); };GroupPage.tsxconst GroupPage = (props: GroupPageTypes) => { const classes = useStyles(props); const [memo, setMemo] = useState<string>(""); const { user } = props; const { userName } = user; const { role } = user; const handleMemo = (event: any) => { console.log(event.target.value); setMemo(event.target.value); }; return ( <div> <Card className={classes.mainCard}> <Typography variant="h5">ユーザーグループのページ</Typography> <br /> {role === "admin" ? ( <> 管理者としてログインしています. <Button className={classes.button} variant="contained" href="/addUser" > 新規ユーザーを追加する </Button> </> ) : ( "" )} <br /> <TableContainer> <Table className={classes.tableRoot}> <TableHead> <TableRow> <TableCell>ユーザー名</TableCell> <TableCell>役割</TableCell> <TableCell>メモ</TableCell> </TableRow> </TableHead> <TableBody> <TableRow> <TableCell> {userName} </TableCell> <TableCell> {role} </TableCell> <TableCell> <TextField required className={classes.textField} id="memo" label="メモ" onChange={handleMemo} type="email" value={memo} /> </TableCell> </TableRow> </TableBody> </Table> </TableContainer> </Card> </div> ); };ユーザーグループに新規ユーザーを追加する
グループへ新規ユーザーを追加する際は,Slackのワークスペースなどの様に招待メールで継続URLを送信し,そのURLからパスワード登録などを行ってもらうのが良いと考えています.
が,前述の通り本記事ではメールアドレス確認部分を端折ってるので,管理者がグループの長としてメールアドレスと共通の初期パスワードの登録を行う形になっています.ここも後日(ryここでグループへ追加ボタンを押すと,先ほどのFirebase Authenticationのページへ新規ユーザーが作成されます.
AddUserPage.tsxtype AddUserTypes = { userRole: string; groupUID: string; }; const AddUserPage = (props: AddUserTypes) => { const classes = useStyles(); const { userRole } = props; const [email, setEmail] = useState<string>(""); const [emailError, setEmailError] = useState<string>("error"); const [complete, setComplete] = useState<boolean>(false); const createNewUser = (): void => { const initialPassword = "firebaseAdvent2019@"; firebase .auth() .createUserWithEmailAndPassword(email, initialPassword) .then(() => { console.log("Success create new user."); }) .catch((error: any) => { console.log(error.code); console.log(error.message); if (`${error.message}`.indexOf("already") !== -1) { alert("既に登録済のメールアドレスです."); } }); }; const handleEmailChange = (event: any) => { setEmail(event.target.value); const error = event.target.validationMessage; if (error) { setEmailError(error); } else { setEmailError(""); } }; useEffect(() => { if (emailError.length === 0) { setComplete(true); } else { setComplete(false); } }, [emailError]); const handleSubmit = (event: any) => { event.preventDefault(); if (emailError.length === 0) { createNewUser(); } }; return ( <div> <Card className={classes.mainCard}> <Typography variant="h5"> ユーザーグループへ新規ユーザーを追加 </Typography> {userRole === "admin" ? ( <form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off" > <TableContainer> <Table className={classes.tableRoot}> <TableHead> <TableRow> <TableCell>入力項目</TableCell> <TableCell /> </TableRow> </TableHead> <TableBody> <TableRow> <TableCell> <TextField required className={classes.textField} id="standard-email" label="メールアドレス" onChange={handleEmailChange} type="email" value={email} /> </TableCell> </TableRow> </TableBody> </Table> </TableContainer> <p> <Button className={classes.button} type="submit" variant="contained" disabled={!complete} > 新規ユーザーをグループへ追加 </Button> </p> </form> ) : ( "新規ユーザーの追加を行えるのは管理者のみです." )} </Card> </div> ); };おわりに
本記事ではここまでです.
ユーザーグループを管理するアカウントの作成から,新規ユーザー追加までを行いました.
とりあえず作ってみるかと勢いで始めましたが,書いてると色々足りない部分が出てきますね...
自分は書き始めないと詳細な設計部分などが見えてこないタイプですが,色々甘すぎました.
必要な機能の全体像や設計は改めて整理したいと思います.(次は肝心の新規ユーザーを対象グループへ紐付ける部分,新規ユーザーが初回ログインした時にどんな処理が走るかを書いていきます.)
- 投稿日:2019-12-21T20:34:43+09:00
【React練習問題】(回答)②カスタムhooksをつかって表示制御をしよう
課題の目的
カスタムhooksの基礎的な使い方を理解してもらいたかった。
回答
実装方法はそれぞれ違うと思うので、一つの例だと思って見てほしいです。
import React, { useState, useCallback, memo} from "react"; import ReactDOM from "react-dom"; import "./styles.css"; function App() { const [isOpen, open, close] = useOpenComment(); const [isOpen2, open2, close2] = useOpenComment(); return ( <div> <button onClick={open}>open!!</button> <button onClick={open2}>open2!!</button> <Comment isOpen={isOpen} close={close} comment="openしました!"/> <Comment isOpen={isOpen2} close={close2} comment="openしました!2"/> </div> ); }; const useOpenComment = () => { const [isOpen, setIsOpen] = useState(false); const open = useCallback(() => setIsOpen(true), []); const close = useCallback(() => setIsOpen(false), []); return [isOpen, open, close]; }; const Comment = memo(({isOpen, close, comment}) => ( <> {isOpen ? <div className="comment"> <p>{comment}</p> <button onClick={close}>close</button> </div> : null } </> )); const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);仕様の実装
・useStateを1回だけ使う
・useCallbackを2回だけ使う
これをカスタムhooksをつかって解決している。const useOpenComment = () => { const [isOpen, setIsOpen] = useState(false); const open = useCallback(() => setIsOpen(true), []); const close = useCallback(() => setIsOpen(false), []); return [isOpen, open, close]; };これをつくっておくことで、ボタンで利用する関数を毎回定義しなくても、useOpenCommentを呼び出すだけで必要な関数を呼び出すことだできるようになる。
- 投稿日:2019-12-21T20:28:22+09:00
【React練習問題】②カスタムhooksをつかって表示制御をしよう
概要
これはReactを学ぶ上で、実際の現場で利用できる実装を取得するためのプログラム。
可読性、パフォーマンスなども求める。課題仕様
コードサンドボックスを利用する
https://codesandbox.io/s/画面
・commentを表示するためのボタンを2つ作る
・表示ボタンを押すと、コメントとその表示を消すボタンがコメントと同じ要素内に用事される
以下、私の実装結果の画面ルール
・FCでつくる
・useStateを1回だけ使う
・useCallbackを2回だけ使う
・memoを使うヒント
・useStateを1回だけ使う
・useCallbackを2回だけ使う
これはカスタムフックスを使うことで解決できる回答
- 投稿日:2019-12-21T19:43:18+09:00
睡眠時間記録webアプリを作る際に便利だったライブラリなど
作ったもの
起きた時間と寝た時間を記録するだけのwebアプリケーション「sleepkun」を作りました。
データはIndexedDBに保存するようになっています。
firebaseの方が複数端末で使えるかなと思ったりしましたが、やる気の関係で見送りました。リポジトリ
リポジトリはこちら。
https://github.com/lisp719/sleepkun作業風景
30分くらいの動画です。
便利だったもの
next.js
sleepkunにはSSRもrouterも要らないのですが、reactのプロジェクトを作る際に楽なので採用しています。
簡単なアプリなのでreduxなどは使わずreact hooksで状態を管理しています。
後述のnowを使う際はnext.jsを使い、firebaseを使う際はAngularを使うといった感じで使い分けることが多いです。dexie
IndexedDBのwrapperライブラリです。
こんな感じでDBを定義して使います。
++id
でオートインクリメントしてくれます。const db = new Dexie("sleepkunDB"); db.version(1).stores({ logs: "++id, label, timestamp" }); db.table("logs").add(obj)dayjs
日付を扱うのに使いました。
moment.jsの軽量版(2KB)といった感じです。dayjs(log.timestamp).format("YYYY-MM-DD HH:mm")now
デプロイ先。
next.jsを作ったzeitがやっているサービスです。
next.jsを使う時はだいたいnowへデプロイしています。
無料で使えたり、設定ファイルを書かなくてもnext.jsのSSRができたり便利です。figma
ブラウザで使えるデザインツール。
画面を考えるときに使いました。まとめ
next.jsとnowの組み合わせがめっちゃ便利。
- 投稿日:2019-12-21T19:43:18+09:00
next.jsで睡眠時間記録webアプリを作りました
作ったもの
起きた時間と寝た時間を記録するだけのwebアプリケーション「sleepkun」を作りました。
データはIndexedDBに保存するようになっています。
firebaseの方が複数端末で使えるかなと思ったりしましたが、やる気の関係で見送りました。リポジトリ
リポジトリはこちら。
https://github.com/lisp719/sleepkun作業風景
30分くらいの動画です。
便利だったもの
next.js
sleepkunにはSSRもrouterも要らないのですが、reactのプロジェクトを作る際に楽なので採用しています。
簡単なアプリなのでreduxなどは使わずreact hooksで状態を管理しています。
後述のnowを使う際はnext.jsを使い、firebaseを使う際はAngularを使うといった感じで使い分けることが多いです。dexie
IndexedDBのwrapperライブラリです。
こんな感じでDBを定義して使います。
++id
でオートインクリメントしてくれます。const db = new Dexie("sleepkunDB"); db.version(1).stores({ logs: "++id, label, timestamp" }); db.table("logs").add(obj)dayjs
日付を扱うのに使いました。
moment.jsの軽量版(2KB)といった感じです。dayjs(log.timestamp).format("YYYY-MM-DD HH:mm")now
デプロイ先。
next.jsを作ったzeitがやっているサービスです。
next.jsを使う時はだいたいnowへデプロイしています。
無料で使えたり、設定ファイルを書かなくてもnext.jsのSSRができたり便利です。figma
ブラウザで使えるデザインツール。
画面を考えるときに使いました。まとめ
next.jsとnowの組み合わせがめっちゃ便利。
- 投稿日:2019-12-21T16:39:56+09:00
【React練習問題】(回答)①カウントアップ、ダウンをつくろう
課題の目的
以下の基礎的な使い方を理解してもらいたかった。
・useStateの使い方
・useCallbackの使い方
・memoの使い方
・コンポーネントに分けることこの書き方に慣れてくれることで、まずはreactのソースが少しずつ理解出来てくる。
useCallbackの機能自体はあまり活用出来ていないが、使って子のコンポーネントに渡すことが基礎であることも知ってほしかった。回答
実装方法はそれぞれ違うと思うので、一つの例だと思って見てほしいです。
import React, { useState, useCallback, memo} from "react"; import ReactDOM from "react-dom"; function App() { const [count, setCount] = useState(0); const countUp = useCallback(() => setCount(count + 1), [count]); const countDown = useCallback(() => { if(count > 0) { setCount(count - 1); } }, [count]); return ( <div> {count} <CountButton onClick={countUp} label="UP!!" /> <CountButton onClick={countDown} label="DOWN!!" /> </div> ); } const CountButton = memo(({onClick, label}) => <button onClick={onClick}>{label}</button> ) const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);仕様の実装
・useStateの利用
const [count, setCount] = useState(0);・useCallbackの利用
const countUp = useCallback(() => setCount(count + 1), [count]); const countDown = useCallback(() => { if(count > 0) { setCount(count - 1); } }, [count]);・memoの利用
・コンポーネントの切り出し
今回はボタン自体をコンポーネント化して、propsで動きを変えれるようにした。const CountButton = memo(({onClick, label}) => <button onClick={onClick}>{label}</button> )
- 投稿日:2019-12-21T16:38:08+09:00
【React】アロー関数とbind()の参照するthis
参照するthisについての話
アロー関数とbind()についての大事な特徴を理解したのでメモ.これを理解していなかったのでクリック時のイベントハンドラでエラー吐き出されるはめになった.
アロー関数
func = (arg) => {}これがアロー関数の形.関数との違いは参照するthisの場所.普通の関数はグローバルなthisを参照するけど,アロー関数では呼び出し元のthisを参照する.
let this.str = "global"; function func() { console.log(this.data); } arrowFunc = () => { console.log(this.data); }; const f = { data: 'local', execute: func }; f.execute(); // => 'global' const af = { data: 'local', execute: arrowFunc }; af.execute(); // => 'local'イベントハンドラでは呼び出し元のthis.stateを参照したいからアロー関数を使うべき.
bind()
アロー関数でなくとも,関数の参照thisをbind()で呼び出し元のthisに紐づける方法もある.
constructor(props) { super(props); this.state = { isToggleOn: true }; // このオブジェクトをイベントハンドラの内部のthisに結びつける this.handleClick = this.handleClick.bind(this); } handleClick() {} render() { <button onClick={this.handleClick}></button> }bind()で一行増やすくらいなら自分はアロー関数を使おうかと思います.可読性もアロー関数の方が上がりそう.
- 投稿日:2019-12-21T14:52:53+09:00
保守性や堅牢性を高める!モダンフロントエンド開発に必要な周辺技術をまとめてみました
はじめに
前日もくもく会で一緒になったエンジニア初学者の方を見て、ポートフォリオとしてフロントエンド開発はちゃんとできてはいるものの、実務において必要な保守性や堅牢性の高いコードについての意識がどうしても不足しているなという印象を受けました。
もちろん初学者の方にそのようなことを求めるのは酷な話なので知らないからダメ、ということを言うつもりは毛頭ありません。
が、職業としてエンジニアを目指すためにフロントエンド開発をやっている場合、保守性や堅牢性を意識すると企業からの評価が段違いなのではないかと思うのです。そこで今回の記事では単にフロントエンドが開発できるというスキルだけではなく、実務における保守性や堅牢性を意識したフロントエンドを開発するために有効になるであろう技術について説明してみたいと思います。
対象者
- プログラミング初学者の方で、モダンなJavaScriptフレームワークを使ってフロントエンド開発を行っている方
- これからモダンなJavaScriptによるフロントエンド技術をプロダクトに導入しようとしている企業のエンジニア
- ReactやVueを導入はしたが、コードの管理やバグが頻出したりして悩んでいる方
紹介する技術・ライブラリの一覧
保守性や管理性を向上させるために有効なものとして、ざっと下記が挙げられます。
- eslint, prettierによるコード解析およびコードフォーマット
- TypeScriptによる静的片付け
- ReactHooks.useReducer, Vuexによる状態の一元管理
- Storybookによるコンポーネントのカタログ管理
- Atomic Design等のコンポーネント分割ポリシー
- Jestによるメソッドの単体テスト
- Testing Library(or Enzyme)によるコンポーネントの振る舞いテスト
- CypressによるE2E(End to End)テスト
- CIパイプライン上でのlintおよび単体テストの実行による自動検査、テストの実行 ではそれぞれの技術の概要と、導入したときのメリットについてみていきます。
ESLint, prettier
lintとは?何が嬉しい?
lint とは、主にC言語のソースコードに対し、コンパイラよりも詳細かつ厳密なチェックを行うプログラムである。
静的解析ツールとも呼ばれる。
lint - Wikipedia実行するのに問題はないが、不具合の原因になるようなコードだったり、不要な記述についてチェックし、あらかじめ開発者に警告ないしエラーとして通知することでそれの解消をしてくれるツールがLintです。
ESLintにおいては静的解析を行う際に確認するルールを自由に設定することができ、例えば下記のようなことができます。
- no-console: consoleを使っていないこと
- no-empty: 空のブロックを使っていないこと
- no-func-assign: functionを再定義していないこと
- no-unreachable: 到達可能なコードが記述されていないこと
- no-eval: evalを使わないこと
等々ですね。
さらにプラグインを利用すればReactやJSXの記述を確認したり、様々なことが可能です。Prettierとは?何が嬉しい?
Prettierはコードフォーマッターのためのライブラリで、様々な言語のフォーマットが可能です。
ESLintがコードの問題を解消するのに対し、prettierではコードの整形を主眼としてしています。
業務におけるプログラミングではチーム開発が基本で、他人のコードを読んだり変更したりすることが多い。
そのため開発者ごとにコードの記述が異なると、コードの可読性が下がって効率が下がってしまいます。
Prettierを導入することで、チーム開発において重要なコードのフォーマットを維持し、自動化もさせることができます。
Further Reading - 参考
TypeScript
TypeScriptとは?何が嬉しい?
TypeScriptはMicrosoftが主体となって開発している、JavaScriptのスーパーセット言語です。
その名の通り、JavaScriptに型を与えた言語で、動的型付けのJavaScriptに対して厳密な静的型付けを与えます。
動的型付け言語は変数の型が動的に変わり、その特性上スピード感のある開発が可能ですが、その代わりに意図しない型に変換されて不具合が発生したりすることもあり、安全性という意味では静的型付け言語に劣ります。
今のところフロントエンド開発においてJavaScriptは不動の地位を占めていますが、裏を返すと動的型付けのJavaScriptしか選べなかったということでもありました。
TypeScriptはそのようなフロントエンドに対して、静的型付け機能を与え、型の不整合による不具合の発生を予防することを可能にします。
また、TypeScriptは実行前にJavaScriptに変換されます。
そのためOptional Chainingなどの機能をブラウザサポートを考えることなく利用することができ、新しい機能をフル活用することも可能です。Further Reading
Rudex, Vuex, ReactHooks.useReducer等によるステート管理
ステート管理とは?何が嬉しい?
ReactとVueにはpropsとstateという値があり、stateはその名の通りコンポーネントの状態を表しています。
このstateを使うポリシーを定めていないと、stateの管理ができず不具合の温床になることがあります。
例えばあるコンポーネントの値を使って別のコンポーネントの動作が決定され、そしてそのコンポーネントの動作によってまた別のコンポーネントの動作が...というようなことが起こり、動作のロジックが複雑になり、デバッグが困難になります。
この問題を解決するためにstateを一元管理(Single source of truth)し、state管理をシンプルにするという手法が取られることがあります。
そしてそれを実現するためのツールが、ReduxやVuex(Vue)、useReducer(React)です。
例えばReduxというライブラリでは3つの原則を掲げていて、
- Single source of truch(単一の正しい情報源)
- State is read-only(Stateは読み込みのみ)
- Changes are made with pure functions(変更は純粋関数を利用して行われる)
というものになっており、この原則によって安全な状態管理が実現されます。
ステート管理を一元的に行うことで現在のアプリケーションの状態を明確にし、複雑な状態を持たせず管理しやすくなり、stateによる不具合を抑制しやすくなります。
Further Reading
Storybookによるコンポーネントのカタログ管理
Storybookとは?何が嬉しい?
Storybook is an open source tool for developing UI components in isolation for React,
Storybookは独立した状態でReact, Vue, そしてAngularのUIコンポーネントを開発するためのオープンソースツールです。Storybook 公式サイト より
上記の公式サイトの紹介にあるように、Storybookはフロントエンドフレームワークにおけるコンポーネント開発のためのツールで、コンポーネント単位での開発と管理を可能にします。これの何が嬉しいかというと、一つのコンポーネントの修正のために、ページ全体を開いて操作をする必要がなくなる、ということです。
例えばテーブル中に存在するソート順を変更するボタンを押した際に、各行がそれぞれ指定した列で並んでいるか、というのを確認したい時に、今までのやり方ではトップページを表示して、テーブルのあるページに移動して、ボタンを押して...ということを毎回やる必要がありました。
しかしStorybookを使えば、そのテーブルコンポーネントだけを表示させることができ、さらに指定したpropsをあらかじめ与えた状態にすることも可能です。
そのため、特定の状態のコンポーネントをあらかじめ用意して、それを一目で見ることも可能というわけです。
これによってコンポーネントの確認、修正のために不要な操作をする必要がなく、モダンフロントエンドにおけるコンポーネントの管理が格段に楽になります。
下記に記載しているStorybook Demoをみると、Storybookがどのようなものか一目で分かるかと思います。
Further Reading
Atomic Design等のコンポーネント管理ポリシー
Atomic Designとは?何が嬉しい?
Atomic design is methodology for creating design systems. There are five distinct levels in atomic design:
Atomic Designはデザインシステムのための方法論である。
Atomic Designにおいては5つの別個のレベルが存在する。Atomic Design - Brad Frost
Atomic Designはデザインにおける部品を化学的な概念に当てはめたもので、部品構成の単位を明瞭にしたものです。元々はデザインガイドとして作られたが、それをフロント開発に応用しているプロダクトが増えています。
Atomic Designの何がいいかというと、コンポーネントの大きさによってコンポーネントを分類することで、管理しやすくするという点です。
Prettierの説明においてはコードの記述が開発者によってバラバラになるのを防ぐメリットがある、ということを説明しましたが、Atomic Designを使えば、開発者ごとにばらけがちなコンポーネントの大きさを統一し、再利用しやすくすることができるというわけです。
Further Reading
Jestによるメソッドの単体テスト
Jestとは?何が嬉しい?
JestはJavaScriptにおける、ユニットテストのためのツールです。
ユニットとは「単位」の意味で、ユニットテストでは小さい部品のテストを実現することができます。
基本的にはJavaScriptやTypeScriptの関数、メソッドの中で記述したロジックがテスト対象となり、関数に与えた引数やAPIリクエストの返り値ごとに、それらが期待した通りの動作になっているかを確認します。
ユニットテストを導入することで、文字通り小さい単位のテストを実施することができるため、それらをつなぎ合わせて全体のアプリケーションを動作させた時の動作が保証しやすくなります。
先ほどStorybookの項目ではページ全体ではなくコンポーネント単位での確認が可能と言いましたが、Jestではそれのメソッドバージョンと言い換えるといいかもしれません。
古典的にはページを表示して、画面を動かした時に実行されるメソッドが正しく動いているかを手動で確認する必要があったりしますが、ユニットテスト済みのメソッドのロジックに関してはテストする必要はないと言っていいでしょう。
Further Reading
Testing Library(or Enzyme)によるコンポーネントの振る舞いテスト
Testing Library, Enzymeとは?何が嬉しい?
こちらもJestの話と似ていますが、こちらは関数やメソッドではなく、画面に表示するコンポーネントをテスト対象としています。
Storybookではコンポーネントの表示がどうなるかを確認することができましたが、あくまで開発者が自分の目で確認する必要がありました。
Testing LibraryやEnzymeを利用することで開発者の目視ではなく、コード上でコンポーネントがどのように動作するかを保証させることが可能になります。
そのため後述するCIパイプラインにおける自動テストなどに組み込みやすく、コードを開発してPRを出した時にテストを実行し、常に動作が問題ないことを保証することが可能になったりします。
Further Reading
CypressやSeleniumによるE2E(End to End)テスト
Cypress, Seleniumとは?何が嬉しい?
Cypress, SeleniumはE2E(End to End)テストのためのライブラリで、E2Eテストはアプリケーション全体の動作をテストするものです。
JestやTesting Libraryはユニットやコンポーネントという小さな単位でのテストでしたが、E2Eテストではアプリケーションが全体として期待通りの動作をしてくれるのか、ということをテストします。
ユニットという単位では動作しているものの、それらをつなげた時に期待する動作をやっているかを保証してくれる機構はJestとTesting Libraryだけでは実現できません。
各種操作を画面上で行った時などのシナリオ全体での動作を保証させることが可能になるというわけです。
Further Reading
CIパイプライン上でのlintおよび単体テストの実行による自動検査、テストの実行
CIパイプラインとは?何が嬉しい?
CIとはDevOpsにおける考え方で、継続的インテグレーション(Continuous Integration)の略語です。
言葉としては難しいですが、やっていることとしては要するに新しく書いたコードをマージした時にアプリケーションが正しく動作することを確認する仕組み、と考えれば良いでしょう。
GitHubなどでCIの仕組みを導入すると、PRを出したときに自動テストを実行させ、テストが失敗したときはPRをマージできない状態にしたり、slackにテストが失敗したことを通知させたりします。
ここまでで紹介したlint, jest, testing libraryなどをCIで実行できるようにすれば、PRを出した時にコード解析とテストが自動で実行され、コードの品質やメソッド・コンポーネントの動作が常にテストされた状態のリポジトリを維持することが可能になります。
CIのためのツールとしてはCircleCIやJenkins, AWSではCodeシリーズなどがあります。
Further Reading
最後に
以上、各種関連言語、ライブラリ等について紹介させていただきました。
これらを利用することで職業エンジニアを目指している方であれば、実用的な技術を使ったポートフォリオを構築して企業からの評価を高めることができるでしょうし、実務で利用すれば保守性が高くかつ堅牢なアプリケーションを継続的に開発することが可能になります。
もちろん導入自体にもコストがかかりますし、これら全てを導入する必要はありませんが、時間とリソースが許す限り導入を検討し、そして導入するのがいいでしょう。
(時間的余裕があれば、実際にここで並べた技術を用いてフロントエンドアプリケーションを開発するハンズオン記事を書きたいと思いますが、それはまた次回...)
ここ間違ってない?これも使った方がいんじゃね?というものがあれば教えてください!
それでは、ありがとうございました!
- 投稿日:2019-12-21T13:47:14+09:00
<day3>Webアプリ完成するまで続ける開発日誌
こんにちは!山形大学のもえとです!
親の顔より見たMac。図書館でも家でも仙台でもReact。今回やること
https://qiita.com/TsutomuNakamura/items/34a7339a05bb5fd697f2
こちらのチュートリアルにて「React Router」というものを学んでいきながらSingle Page Applicationを作っていきます。・前準備
・React Router使ってみる
・Link
・button準備
$ mkdir react_router $ cd react_router $ npm init -y $ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader \ webpack webpack-cli webpack-dev-server \ react react-dom \ react-router react-router-dom以上を実行してください。最後のやつは4行全て1つの命令なのでコピペして実行してください。
少し時間がかかります。あと僕のMacbookの場合悲鳴のような排熱音がしたので図書館での作業は非推奨です。(すげぇうるさいなう)webpack-dev-server(リアルタイムで変更が見れるようにするための開発用Webサーバ)が簡単に起動するように
package.json
にちょっと付け加えます。package.json"scripts": { "start": "webpack-dev-server --content-base src --mode development --inline", // <- 追加 ...... },慣れてきたかと思いますのでコマンド操作は省きますが、
mkdir
やtouch
を使って下のようなファイル構造にしてください。react_router> node_modules > src -index.html > js - client.js > pages - Layout.jsまた、以下のjs、htmlファイルはコピペしてもらって構いません。
初期準備です。webpack.config.jsvar debug = process.env.NODE_ENV !== "production"; var webpack = require('webpack'); var path = require('path'); module.exports = { context: path.join(__dirname, "src"), entry: "./js/client.js", module: { rules: [{ test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, use: [{ loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-env'] } }] }] }, output: { path: __dirname + "/src/", filename: "client.min.js", publicPath: '/' }, devServer: { historyApiFallback: true }, plugins: debug ? [] : [ new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }), ], };src/js/pages/Layout.jsimport React from "react"; export default class Layout extends React.Component { render() { return ( <h1>KillerNews.net</h1> ); } }src/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>React</title> <!-- Bootstrap Core CSS --> <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cerulean/bootstrap.min.css" rel="stylesheet"> <!-- Custom Fonts --> <!-- <link href="font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> --> <link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,300italic,400italic,700italic" rel="stylesheet" type="text/css"> </head> <body> <!-- Navigation --> <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li> <a href="#">Featured</a> </li> <li> <a href="#">Archives</a> </li> <li> <a href="#">Settings</a> </li> </ul> </div> <!-- /.navbar-collapse --> </div> </nav> <!-- Page Content --> <div class="container" style="margin-top: 60px;"> <div class="row"> <div class="col-lg-12"> <div id="app"></div> </div> </div> <!-- Call to Action Well --> <div class="row"> <div class="col-lg-12"> <div class="well text-center"> Ad spot goes here </div> </div> <!-- /.col-lg-12 --> </div> <!-- /.row --> <!-- Content Row --> <div class="row"> <div class="col-md-4"> <h2>Heading 1</h2> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p> <a class="btn btn-default" href="#">More Info</a> </div> <!-- /.col-md-4 --> <div class="col-md-4"> <h2>Heading 2</h2> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p> <a class="btn btn-default" href="#">More Info</a> </div> <!-- /.col-md-4 --> <div class="col-md-4"> <h2>Heading 3</h2> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p> <a class="btn btn-default" href="#">More Info</a> </div> <!-- /.col-md-4 --> </div> <!-- /.row --> <!-- Footer --> <footer> <div class="row"> <div class="col-lg-12"> <p>Copyright © KillerNews.net</p> </div> </div> </footer> </div> <!-- /.container --> <script src="client.min.js"></script> </body> </html>src/js/client.jsimport React from "react"; import ReactDOM from "react-dom"; import Layout from "./pages/Layout"; const app = document.getElementById('app'); ReactDOM.render(<Layout />, app);これをコピペしたら開発用Webサーバを起動してください
$ npm start
Webサーバが起動したら
http://localhost:8080
にアクセス。するとなにやらKillerNews.netなどと表示されていたらここまではOK!
(まだ前準備終わりじゃないっす)つづいて、react-router関連のパッケージをインストール。次のコマンドを実行してください。
npm install --save-dev react-router react-router-dom
※ここで私の場合、開発日誌day1で出てきた
No Xcode or CLT version detected!
が現れました。もちろん同じ方法で解決しました。外付けにxcodeを入れると毎回これ出るみたいですが何かしら対策はあるのかなぁ。うちのMac、ストレージが128GBなもんでぜんぜん足りん。つづいてコンポーネントをどんどん作っていきましょう。
すべてsrc/js/pagesの下に作成します。
ここは手打ちをおすすめします。①記事を表示するFeaturedコンポーネント
src/js/pages/Featured.jsimport React from "react"; export default class Featured extends React.Component { render() { return ( <h1>Featured</h1> ); } }②Archivesコンポーネント
src/js/pages/Archives.jsimport React from "react"; export default class Archives extends React.Component { render() { return ( <h1>Archives</h1> ); } }③Settingsコンポーネント
src/js/pages/Settings.jsimport React from "react"; export default class Settings extends React.Component { render() { return ( <h1>Settings</h1> ); } }React Rouer
Router初登場です。
client.js
にRouterを使って変更を加えていきます。src/js/client.jsimport React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter as Router, Route } from "react-router-dom"; import Layout from "./pages/Layout"; import Featured from "./pages/Featured"; import Archives from "./pages/Archives"; import Settings from "./pages/Settings"; const app = document.getElementById('app'); ReactDOM.render( <Router> <Layout> <Route exact path="/" component={Featured}></Route> <Route path="/archives" component={Archives}></Route> <Route path="/settings" component={Settings}></Route> </Layout> </Router>, app);※RouterとRouteの間違いに注意。
このプログラムのRouterのところを説明します。
3つめのimportでReact Routerをインポートしてますね
import { BrowserRouter as Router, Route} from "react-router-dom";
※大文字小文字に注意そしてrenderの下の
<Router>
タグ内にあるのがRouterの文ということでしょうか。
<Router>
タグ内には先ほど作ったFeaturedやArchivesが羅列的に並べられていますね。
注意する点はFeaturedのところにあるexact path="/"
exact path
を渡すと、ユーザがアクセスしたパスが厳密に/
である時のみFeaturedを表示させ、/foo
とか/bar
などのときはFeaturedを表示させないんだそうです。Link
Archves、Settingsへのリンクを追加するため
Layout.js
を編集。src/js/pages/Lauout.jsimport React from "react"; import { Link } from "react-router-dom"; export default class Layout extends React.Component { render() { return ( <div> <h1>KillerNews.net</h1> {this.props.children} <Link to="archives">archives</Link>, <Link to="settings">settings</Link> </div> ); } }
archives
を押すとArchives
が表示され
settings
を押すとSettings
が表示されたらLinkがつながってます!
button
さっきの
Link
のところにbutton
を付けましょう
Layout.js
を編集。src/js/pages/Layout.jsimport React from "react"; import { Link } from "react-router-dom"; export default class Layout extends React.Component { render() { return ( <div> <h1>KillerNews.net</h1> {this.props.children} <Link to="archives"><button class="btn btn-danger">archives</button></Link> <Link to="settings"><button class="btn btn-success">settings</button></Link> </div> ); } }これはHTMLの書き換えですね!
button要素のclassを"btn tbn-danger"
にするとBootStrapのCSSが適応されるようです
ちょっときりがわるいけどday3はここまで〜〜
(2019/12/21作成)
以降の記事
day4作成中...12/21
- 投稿日:2019-12-21T12:58:37+09:00
Next.js + Emotion (CSS in JS)で始めるReact超入門
以前、React初学者向けの勉強会を開催したときに作った資料を、Qiita向けに調整したものです。
- Reactの初歩的な記法
- Next.jsでのサーバーサイドレンダリングの概要
- CSSinJSの使い方
を学ぶことができます。
事前にNode.jsをインストールしている必要があります。
Reactとは?
Reactとは、Facebookが作ったJavaScriptライブラリです。ユーザーインターフェイスをコンポーネントベースで作ることができます。
公式サイト:https://ja.reactjs.org/
シンプルなReactのサンプル
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello World</title> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> </head> <body> <div id="root"></div> <!-- babelがJSXをReact.createElement()に変換してくれる --> <script type="text/babel"> /** * 引数:propsを、JSX:<div>{props.text}</div>で受け取り、returnで返す。 * この一式をコンポーネントと呼ぶ。 */ function Hello(props) { return <div>{props.text}</div> }; /** * Helloコンポーネントを<div id="root"></div>にマウントしている。 * 関数みたいにしてみると、`Hello({ text: 'Hello, React!' });` こんな感じ。 */ ReactDOM.render( <Hello text="Hello, React!" />, document.getElementById("root") ); </script> </body> </html>Reactの特徴
Vue.jsがディレクティブを使いHTMLを拡張するような方法で開発するのに対し、ReactはガシガシJavaScriptを書いていきます。まあ、Vue.jsもガッツリ開発を始めるとガシガシJavaScriptを書くことになると思いますが(・ω・)
また、Reactは基本的にデータを受け取って適切なViewを返すことを目的としたシンプルなライブラリなので、Angularのようなルールはなく、自由度がかなり高いです。逆に言うと、しっかりとした設計ができてないと、開発途中でつらくなります(・ω・)
宣言的UI
Reactに限らず、近年のUIフレームワーク・ライブラリ、およびプログラミングにおいて主要なパラダイムである宣言的UIについて、知っておく必要があります。
そのまえに、まず、命令的と宣言的を解説します。
命令的
- 何をするかを記述する
- 前回の実行結果に依存する
- 変数の再代入が行われる
宣言的
- どういう状態になるのかを記述する
- 前回の実行結果に依存しない
- 変数に再代入しない
命令的UIと宣言的UI
命令的UI
命令的UIの例として、jQueryによるDOM操作があげられます。
<!-- この時点ではUIの最終的な状態はわからない --> <ul id="list"></ul>const animals = ["ねずみ", "うし", "とら"]; // 配列の要素分処理を繰り返し、HTML側に挿入することでUIが決定する。 animals.forEach(animal => { $('#list').append(`<li>${animal}</li>`); });宣言的UI
一方で、Vue.jsは宣言的にUIを作ることができます。
<template> <ul> <!-- この時点でUIの状態が決まっている --> <li v-for="(item, index) in list" :key="index">{{item}}</li> </ul> </template> <script> export default { data() { return { // 配列の要素によってリストの数が決定する list: ["ねずみ", "うし", "とら"] }; } }; </script>ReactやVue.jsは、jQueryの次に流行っているフレームワーク・ライブラリではなく、宣言的なUIを作るためのフレームワーク・ライブラリです。
技術選定時に宣言的なUIが必要であれば、ReactやVue.jsを使用しましょう。逆に言えば、jQueryのほうが適切な場面であれば、無理に使用する必要はありません。
Next.jsとは?
Next.jsとは、Reactでサーバーサイドレンダリングをするためのフレームワークです。Vue.jsで言うところの、Nuxt.js。簡単にルーティングできて、静的サイトの書き出しもできます。
公式サイト:https://nextjs.org/
静的サイトの書き出しならば、Gatsby.jsのほうが使いやすいかもしれませんが、Next.jsのほうがシンプルに始められるので、今回はNext.jsを使用します。
Reactとしての書き方はほぼ同じですし、メジャーなエコシステムも使用できるなので、Next.jsが使えればGatsby.jsも使えると思います。多分。
サーバーサイドレンダリングとは?
サーバーサイドレンダリングとは、PHPやRuby、Javaのように、サーバーサイドでDOMを生成してクライアントに静的なHTMLとして渡すことです。
Reactは、本来クライアントサイドで仮想DOMを生成し、それを実DOMとしてブラウザに描画します。
<!-- サーバーから返されるHTML --> <div id="app"></div>import React from 'react'; import ReactDOM from "react-dom"; // クライアントサイドでDOMを書き換える ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('root') );サーバーサイドレンダリングは、サーバー上でReactを実行し、生成したDOMをクライアントへ渡します。
たとえば、動的なコンテンツで
<title>
要素や<meta>
要素をクライアントサイドで生成すると、TwitterやFacebook等のSNSでシェアしたときには反映されません。しかし、サーバーサイド上で事前にDOMを生成すれば、クライアントからみれば静的なHTMLがレスポンスとして返ってくるので、この問題が回避できます。
Next.jsの使い方
必要なパッケージをインストール
- next
- react
- react-dom
$ mkdir nextjs-sample $ cd nextjs-sample $ npm init -y $ npm i next react react-dom
package.json
にscripts
を追記。{ "scripts": { "dev": "next" } }
./pages
ディレクトリにindex.js
を追加。$ mkdir pages $ touch pages/index.js// pages/index.js export default () => <h1>Hello, Next.js!</h1>ローカルサーバーを起動。
$ npm run dev
http://localhost:3000
にアクセスして、Hello, Next.js!
が表示されていれば、OK!ディレクトリ構成
./pages
ルーティングの対象./static
静的ファイルの置き場所
- 画像ファイルとか
ディレクトリのルールが決まっているのは、これくらい。
また、Next.js 9.1から
src
配下でも利用できるようになったので、以下でもOKです。
src/pages
ルーティングの対象src/static
静的ファイルの置き場所JSX
JSXを使用すると、JavaScript上でHTMLのような構文が使えます。
// JSX const Button = <button className="my-button">ボタン</button>これは、
React.createElement()
の糖衣構文で、JSXを使わないと下記の記述になります。const Button = React.createElement("button", { className: "my-button" }, "ボタン");極論、JSXを使わずに
React.createElement()
を使ってもなんの問題ありません。JSXを使う理由が公式のガイドにありますので、興味のある方はどうぞ。
オンライン Babel コンパイラを使うと、JSXがどのようなJavaScriptに変換されるのかを確認できます。
Next.js(React)を書いてみよう
pages/index.js
を、省略形なしの形に変更。import React from 'react' // Next.jsでは省略可能 // returnでJSXを返す関数をコンポーネントと呼ぶ function Index() { return <h1>Hello, Next.js!</h1> } // ES Modules // 本来は import されて react-dom がレンダリングするが、Next.jsでは隠蔽されている export default IndexHTMLのように、JSXでも子要素を使うことができます。
import React from 'react' function Index() { // ()で括り、;の自動挿入に対応 // returnで返すJSXは必ず1つの要素 return ( <div> <h1>Hello, Next.js!</h1> </div> ) } export default IndexJSXは
{}
でJavaScriptを使うことができます。import React from 'react' function Index() { const text = 'Next.js!' return ( <div> {/* コメントアウト */} <h1>{`Hello, ${text}`}</h1> </div> ) } export default Index
Heading
コンポーネントを作って、JSX内で使ってみましょう。import React from 'react' // 見出し用のコンポーネント function Heading(props) { // 属性の値は、オブジェクトのプロパティとして渡される return <h1>{props.text}</h1> } function Index() { const text = 'Next.js!' return ( <div> { /** * コンポーネントの属性でテキストを渡す * これをProps(プロップス)と呼ぶ */ } <Heading text={`Hello, ${text}`} /> </div> ) } export default Index
Heading
コンポーネントに、子要素を渡してみます。import React from 'react' // Propsはオブジェクトなので、分割代入が使える function Heading({ children }) { // childrenで子要素を受け取る return <h1>{children}</h1> } function Index() { const text = 'Next.js!' return ( <div> {/* コンポーネントの子要素でspan要素を渡す */} <Heading> <span>{`Hello, ${text}`}</span> </Heading> </div> ) } export default Index
div
がいらねえときは、React.Fragment
が便利です。import React from 'react' function Heading({ children }) { return <h1>{children}</h1> } function Index() { const text = 'Next.js!' // React.Fragmentを使うとその要素はレンダリングされない return ( <React.Fragment> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <p>divでラップしたないねん</p> </React.Fragment> ) } export default Index
React.Fragment
は、糖衣構文として<></>
とも使えます。記述量が遥かに少なくてすむので、とくに理由がなければ、こちらを使用しましょう。import React from 'react' function Heading({ children }) { return <h1>{children}</h1> } function Index() { const text = 'Next.js!' // <React.Fragment></React.Fragment>は<></>とも書ける return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <p>divでラップしたないねん</p> </> ) } export default Indexファイルを分けてみましょう。
$ mkdir components $ touch components/Heading.js// components/Heading.js // {} と return を省略できる function Heading({ children }) { return <h1>{children}</h1> } export default Heading// pages/index.js import React from 'react' import Heading from '../components/Heading' function Index() { const text = 'Next.js!' return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <p>divでラップしたないねん</p> </> ) } export default Index
map
メソッドで要素の反復処理をしてみましょう。// pages/index.js import React from 'react' import Heading from '../components/Heading' // 配列 const member = ['ネズミ', '牛', 'トラ', 'うさぎ'] function Index() { const text = 'Next.js!' return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <ul> {member.map((name, index) => ( <li key={index}>{name}</li> ))} </ul> </> ) } export default Index
onClick
でイベント発火できます。// pages/index.js import React from 'react' import Heading from '../components/Heading' const member = ['ネズミ', '牛', 'トラ', 'うさぎ'] function Index() { const text = 'Next.js!' return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <ul> {member.map((name, index) => ( <li key={index}>{name}</li> ))} </ul> {/* onClickに関数を書く */} <button onClick={() => console.log('onClick')}>ボタン</button> </> ) } export default IndexuseStateで関数コンポーネントに状態をもたせる
React 16.8で、hooksという新機能が追加されました。Reactで
state
などの機能を使う場合、これまではクラスで書かないといけませんでしたが、hooksの登場で関数コンポーネントでも副作用のある機能を使うことができるようになりました。今回は、関数コンポーネントに状態をもたせることができる、
useState
を使ってみましょう。// pages/index.js // `useState`をインポート import React, { useState } from 'react' import Heading from '../components/Heading' const member = ['ネズミ', '牛', 'トラ', 'うさぎ'] function Index() { const text = 'Next.js!' /** * const [変数, 変数の値を変える関数] = useState(初期値) * 以下では、`value`変数の初期値に`No, Click.`の文字列を代入しています。 * setValue('Yes, Click!!')を実行すると、 * valueの値を`No, Click.`から`Yes, Click!!`に変えることができます。 */ const [value, setValue] = useState('No, Click.'); const onClickEvent = () => setValue('Yes, Click!!'); return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <ul> {member.map((name, index) => ( <li key={index}>{name}</li> ))} </ul> <button onClick={() => console.log('onClick')}>ボタン</button> {/* クリックすると、`No, Click.`が`Yes, Click!!`に変わる */} <button onClick={onClickEvent}>{value}</button> </> ) } export default IndexNext.js独自の機能
Link
コンポーネントでルーティングさせてみましょう。// pages/index.js import React, { useState } from 'react' import Link from 'next/link' // Linkコンポーネントを追加 import Heading from '../components/Heading' const member = ['ネズミ', '牛', 'トラ', 'うさぎ'] function Index() { const text = 'Next.js!' const [value, setValue] = useState('No, Click.'); const onClickEvent = () => setValue('Yes, Click!!'); return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <ul> {member.map((name, index) => ( <li key={index}>{name}</li> ))} </ul> <button onClick={() => console.log('onClick')}>ボタン</button> <button onClick={onClickEvent}>{value}</button> {/* Linkコンポーネントでルーティングできる */} <Link href="/batman"><a>バットマンページへ</a></Link> </> ) } export default Index
pages/batman.js
を作成した上、バットマンページへのリンクをクリックすると、再読み込みなしでページ遷移できます。つまり、SPAです。$ touch pages/batman.js// pages/batman.js import React from 'react' function Batman() { return <div>batman</div> } export default Batman
getInitialProps
で非同期データ取得
getInitialProps
は、Next.jsのライフサイクルメソッドです。ページが読み込まれたときはサーバーサイドで実行され、以降、Link
コンポーネントによって別のpages
コンポーネントへ移動した場合にクライアントサイドで実行されます。以下の実装をして、
http://localhost:3000
からhttp://localhost:3000/batman
に遷移したときと、http://localhost:3000/batman
をリロードしたときのコンソールの表示を確認してみましょう。// pages/batman.js import React from 'react' function Batman({ text }) { return <div>{text}</div> } Batman.getInitialProps = () => { const text = 'I am Batman !!' console.log(text) return { text } // returnしたオブジェクトをコンポーネントのPropsとして受け取れます } export default Batman遷移したときはブラウザ側のコンソール、リロードしたときは開発側のコンソールに、それぞれログが出たかと思います。
Next.jsはサーバーサイドレンダリングのためのフレームワークなので、今書いているJavaScriptがサーバーサイド(Node.js)なのか?それとも、クライアントサイドなのか?を意識することが必要です。
非同期でデータ取得
バットマンAPIを叩いて、非同期に情報を取得してみましょう。ページ読み込み時になにかしらの処理をする場合は、
getInitialProps
メソッドを使います。Node.jsではfetchメソッドが使えないので、
isomorphic-unfetch
をインストールして使います。$ npm i isomorphic-unfetch// pages/batman.js import React from 'react' import fetch from 'isomorphic-unfetch' function Batman({ shows }) { return ( <div> <h1>Batman TV Shows</h1> <ul> {shows.map(show => ( <li key={show.id}> <div><img src={show.image.medium} /></div> <div>{show.name}</div> </li> ))} </ul> </div> ) } Batman.getInitialProps = async () => { const res = await fetch('https://api.tvmaze.com/search/shows?q=batman') const data = await res.json(); return { shows: data.map(entry => entry.show) } } export default Batmanサーバーサイドレンダリングの使い所
たとえば、動的なコンテンツで
<title>
要素や<meta>
要素をクライアントサイドで生成すると、TwitterやFacebook等のSNSでシェアしたときには反映されません。しかし、サーバーサイド上で事前にDOMを生成すれば、クライアントからみれば静的なHTMLがレスポンスとして返ってくるので、この問題が回避できます。
クソアプリを作ったので、これを実際に試してみましょう。
$ touch pages/nameApp.js $ touch pages/yourName.js// nameApp.js import React, { useState } from 'react' import { useRouter } from 'next/router' function NameApp() { const [name, setValue] = useState('') /** * Next.jsのルーターオブジェクト * https://nextjs.org/docs#userouter */ const router = useRouter() const onClickEvent = () => { // yourname?name=【name】に遷移する router.push({ pathname: '/yourName', query: { name }, }) } const onChangeEvent = event => setValue(event.target.value) return ( <> <div>君の名は。。。</div> <input value={name} onChange={onChangeEvent} /> <button onClick={onClickEvent}>click!!</button> </> ) } export default NameApp
nameApp.js
のやっていることは、ReactやNext.jsを使わない方法で書くとこんな感じです。<form action="yourName/" method="GET"> <div>君の名は。。。</div> <input name="name"/> <button type="submit">click!!</button> </form>続いて、遷移先の
yourName.js
を実装します。// yourName import React from 'react' import Head from 'next/head' function YourName({ query }) { const { name } = query return ( <> {/* Headコンポーネントで`title`や`meta`が設定できる */} <Head> <title>{name} | YourName</title> <meta name="description" content={`君の名は、${name}ですね。`} /> </Head> <div> 君の名は、<strong>{name}</strong>ですね。 </div> </> ) } YourName.getInitialProps = ({ query }) => { return { query } } export default YourNameフォームに名前を入力して隣のボタンをクリックすると、入力した名前を表示することができる画期的なアプリです。
command + option + u
でソースを確認してみましょう。サーバーから取得したHTMLの段階で、title
やmeta
が設定されていることがわかります。イメージを掴んでいただくために、試しにPHPで実装してみました。(PHPが全然わからないので細かいところはご勘弁を。。。(´;ω;`))
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>YourName</title> </head> <body> <form action="yourName/" method="GET"> <div>君の名は。。。</div> <input name="name"/> <button type="submit">click!!</button> </form> </body> </html>上のHTMLで
yourName/?name=ほげぼげ
みたいな感じになるので、PHPでパラメーターを受け取りHTMLとしてクライアントにレスポンスします。<!-- yourName/index.php --> <?php $name = htmlspecialchars($_GET['name']); ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title><?php echo $name; ?> | YourName</title> <meta name="description" content="君の名は、<?php echo $name; ?>ですね。" /> </head> <body> <div>君の名は、<?php echo $name; ?>ですね。</div> </body> </html>より掘り下げたい場合は、公式ドキュメントを確認してください。
また、GitHubのexampleに豊富なサンプルがあるので、とても参考になります。
EmotionでCSS in JS
Emotionとは、JavaScriptでCSSスタイルを記述するために設計されたライブラリです。後発ライブラリのため、styled-component等の良いとこ取りをしています。
必要なパッケージをインストールしましょ。
- @emotion/styled
- @emotion/core
$ npm i @emotion/styled @emotion/core
Emotionを使ってみよう
@emotion/styledを使い、styled-componentライクなコンポーネントを作ってみます。
// components/Heading.js // @emotion/styledをインポート import styled from '@emotion/styled' // styled.{要素}`{css}` の形で使用します。 // 定数に代入することで、コンポーネントとして利用できます。 // ブラウザ上ではユニークな文字列のCSSクラスが付与されるので、CSSはスコープになります。 const HeadingStyle = styled.h1` font-size: 20px; color: red; ` function Heading({ children }) { return <HeadingStyle>{children}</HeadingStyle> } export default HeadingCSS部分はテンプレートリテラルなので、
${}
内でJavaScriptが利用できます。// components/Heading.js import styled from '@emotion/styled' // フォントサイズを定数化 const fontSize = 20 // テンプレートリテラル内で定数を使用 const HeadingStyle = styled.h1` font-size: ${fontSize}px; color: red; ` function Heading({ children }) { return <HeadingStyle>{children}</HeadingStyle> } export default Headingコンポーネント側からProps経由で値を渡すことができます。別ファイルにしてデータを渡してみましょう。
$ touch components/HeadingStyle.js// components/HeadingStyle.js import styled from '@emotion/styled' // ES Modules // 関数の引数としてデータを受け取ります export const HeadingStyle = styled.h1` font-size: ${props => props.fontSize}px; color: red; `// components/Heading.js import { HeadingStyle } from './HeadingStyle' const fontSize = 20 function Heading({ children }) { return <HeadingStyle fontSize={fontSize} >{children}</HeadingStyle> } export default HeadingSCSSのようにネストが使えます。
// components/HeadingStyle.js import styled from '@emotion/styled' // SCSSのように&が使えます。 export const HeadingStyle = styled.h1` font-size: ${props => props.fontSize}px; color: red; &:hover { color: green; } `CSS in JSのメリット
たとえば、ブレークポイントをJavaScriptで管理すれば、Carouselのライブラリ等と共通の値を使うことができます。
$ mkdir const $ touch const/breakPoints.js// const/breakPoints.js const breakPoints = { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200 } export default breakPoints// components/HeadingStyle.js import styled from '@emotion/styled' import breakPoints from '../const/breakPoints' export const HeadingStyle = styled.h1` font-size: ${props => props.fontSize}px; color: red; @media (min-width: ${breakPoints.md}px) { color: green; } `;react-slickを使ってみましょ。
https://react-slick.neostack.com/
$ npm i react-slick slick-carousel raw-loader $ touch next.config.js
next.config.js
で、Next.jsが隠蔽しているwebpackの設定にアクセスできます。raw-loaderを追加して、CSSファイルを扱えるようにします。// next.config.js module.exports = { webpack: config => { config.module.rules.push({ test: /\.css$/, use: "raw-loader" }); return config } }スライダーコンポーネントを作り、トップページで使ってみましょう。
$ touch components/MySlider.js// components/MySlider.js import React from 'react' import styled from '@emotion/styled' import Slider from 'react-slick' import slickCss from 'slick-carousel/slick/slick.css' import slickThemeCss from 'slick-carousel/slick/slick-theme.css' import breakPoints from '../const/breakPoints' const settings = { infinite: false, slidesToShow: 2, slidesToScroll: 2, responsive: [ { breakpoint: breakPoints.md, // const/breakPoints.jsの値が使える settings: { infinite: true, slidesToShow: 1, slidesToScroll: 1, } } ] }; const SliderWrapperStyle = styled.div` ${slickCss} ${slickThemeCss} ` function MySlider({ member }) { return ( <SliderWrapperStyle> <Slider {...settings}> {member.map((animal, index) => ( <div key={index}>{animal}</div> ))} </Slider> </SliderWrapperStyle> ) } export default MySlider// pages/index.js import React, { useState } from 'react' import Link from 'next/link' import MySlider from "../components/MySlider"; import Heading from '../components/Heading' const member = ['ネズミ', '牛', 'トラ', 'うさぎ'] function Index() { const text = 'Next.js!' const [value, setValue] = useState('No, Click.'); const onClickEvent = () => setValue('Yes, Click!!'); return ( <> <Heading> <span>{`Hello, ${text}`}</span> </Heading> <button onClick={() => console.log("onClick")}>ボタン</button> <button onClick={onClickEvent}>{value}</button> <Link href="/batman"> <a>バットマンページへ</a> </Link> {/* member配列をPropsで渡す */} <MySlider member={member} /> </> ); } export default Index他にもEmotionでいろいろなことができるので、ぜひ掘り下げてみてください。
- 投稿日:2019-12-21T12:47:44+09:00
Gatsby組込みのLinkコンポーネントを使ってナビゲーションバーを自作する
初めに
この記事はGatsby.js Advent Calendar 2019 21日目の記事です。
GatsbyJSを色々いじってたらいい感じのナビゲーションバーが自作できた気がしました。目次
- 本記事の目標
- 前提 - 開発環境 -
- Gatsby Link APIについて
- ディレクトリ構成
- まずはひな型を作る
- 共通のレイアウトは
Layout
コンポーネントとして定義するHeader
コンポーネントの実装- 終わりに
本記事の目標
こんな感じのナビゲーションバーを作りたいと思います。
それっぽいものがフロントエンド初心者の私でも作れたので投下します。
重要なことですが、このレイアウトではBootstrap4は使っていません。サンプルを公開しています。触ってみてください → サンプル
ソースコード→ https://github.com/koralle/gatsby-link-navbar前提 - 開発環境 -
以下私の手元の開発環境になります。
- Windows 10
- yarn 1.21.1
- Gatsby CLI version: 2.8.15
Gatsby Link APIについて
このナビゲーションバーを作るうえで、今回はGatsby組込みのGatsby Link APIを使用しています。
簡単に説明すると、サイト内リンクをこのGatsby Link APIを使って実装すると、そのリンクを踏んで画面遷移する前に、関連するリソースを事前にfetchしてくれる(公式ではこれを"preloading"と呼んでいる)ので、結果的にサイトパフォーマンスの向上につながります。今回はこのGatsby Link APIの中の
Link
コンポーネントを使用します。公式ドキュメント→Gatsby Link API | GatsbyJS
これに関しては12/25の投稿日に記事を書けたらいいなと思います。
ディレクトリ構成
gatsby-link-navbar
というプロジェクトを作成し、/src
以下のディレクトリを編集しました。
gatsby-link-navbar/src/components/
以下のファイルが画面のレイアウトを作ります。> tree src HOGEHOGE\GATSBY-LINK-NAVBAR\SRC ├─components │ ├─Header │ │ Header.css │ │ Header.js │ │ │ └─Layout │ Layout.css │ Layout.js │ └─pages about.js hobby.js index.js link.js skills.jsこのような構成にすることで、リソース、URL、そしてコンテンツが以下のような対応関係になるようにしました。
ただ、今回は意図したレイアウトが描画されていることが確認できればOKなので、gatsby-link-navbar/src/pages/
以下は今回ほとんど作りこんでません。
リソース URL コンテンツ ./src/pages/about.js localhost:8000/about/ "About" ./src/pages/hobby.js localhost:8000/hobby/ "Hobby" ./src/pages/index.js localhost:8000 トップ画面 ./src/pages/link.js localhost:8000/link/ "Link" ./src/pages/skills.js localhost:8000/skills/ "Skills" まずはひな型を作る
今回はstarterとして
gatsby-starter-hello-world
を使います。> gatsby new gatsby-link-navbar https://github.com/gatsbyjs/gatsby-starter-hello-world次のコマンドを叩いた後、ブラウザで
localhost:8000
にアクセスして"Hello, world!"が表示されていることを確認してください。> cd gatsby-link-navbar > gatsby develop共通のレイアウトは
Layout
コンポーネントとして定義するまずは全ページ共通のレイアウトを
Layout
コンポーネント(layout.js
)として作成します。
先にlayout.js
の内部を確認します。layout.jsimport React from 'react'; import Header from '../Header/Header'; import './Layout.css'; export default ({ children }) => ( <div className="page-root"> <Header /> <div className="page-body"> {children} </div> </div> )大雑把に言えば
Header
コンポーネントとページの中身を収容する<div>
タグを縦に並べています。
Layout
コンポーネントをこのように定義すると、個々のコンテンツを描画するコードは以下のように書くことができます。./src/pages/hogehoge.js{/* "hogehoge"ページ */} const Hogehoge = () => { return ( <Layout> {/* ここにlayout.js内の{children}に当たる内容を記述する。 */} </Layout> ); }
Layout.js
はHeader.js
に依存しているので、次にHeader
コンポーネントつまりナビゲーションバーを実装します。
Header
コンポーネントの実装
Link
コンポーネントを使用するために、まずはLink APIをインポートします。// Gatsby標準組込みなので事前の"yarn add"や"npm install"の必要はなし。 import { Link } from 'gatsby';私はナビゲーションバーに表示したい項目を例えば以下のようにしました。
// ナビゲーションバーに表示する項目 const NavMenuItem = ["Home", "About", "Skills", "Hobby", "Link"];次に、ナビゲーションバーの項目のそれぞれに設定するスタイルを記述します。
今回はアクティブになっている項目の
Link
コンポーネントは色を反転させ、太字にします。
私はHeader.js
内ではアクティブ時と非アクティブ時の差分のみを記述し、それ以外でLinkコンポーネントに適用したいスタイルは同じ階層のHeader.css
に記述しました(この辺は好みの問題だと思います)。// 普段のリンクはこのスタイル const LinkStyles = { background: 'rebeccapurple', color: 'white', fontWeight: "normal" } // アクティブになった項目は色を反転させる const ActiveStyles = { background: 'white', color: 'rebeccapurple', fontWeight: "bold", }この二つのスタイルをナビゲーションバーに表示する項目(=
Link
コンポーネント)全てに適用します。
アクティブ時と非アクティブ時の切り替えはGatsby側が引き受けてくれるので、スタイルの切り替え処理を私たちが記述する必要がありません。const NavMenuLiTag = NavMenuItem.map((item) => { let page_link = ""; if (item === "Home") { page_link = "/"; } // e.g.) "/about/", "/hobby/" else page_link = "/" + item.toLowerCase() + "/"; return ( <li key={page_link}> <Link to={page_link} style={LinkStyles} activeStyle={ActiveStyles} className="page-link" > {item} </Link> </li> ) });最終的に
Header
コンポーネントは以下のようになりました。./src/components/Header/Header.jsimport React from 'react'; import { Link } from 'gatsby'; import './Header.css' const Header = (props) => { // ナビゲーションバーに表示するリンク const NavMenuItem = ["Home", "About", "Skills", "Hobby", "Link"]; // 普段のリンクはこのスタイル const LinkStyles = { background: 'rebeccapurple', color: 'white', fontWeight: "normal" } // アクティブになったリンクは色を反転させる const ActiveStyles = { background: 'white', color: 'rebeccapurple', fontWeight: "bold", } // ナビゲーションリンクの作成 const NavMenuLiTag = NavMenuItem.map((item) => { let page_link = ""; if (item === "Home") { page_link = "/"; } else page_link = "/" + item.toLowerCase() + "/"; return ( <li key={page_link}> <Link to={page_link} style={LinkStyles} activeStyle={ActiveStyles} className="page-link" > {item} </Link> </li> ) }); return ( <header className="App-header"> <nav className="App-navbar"> <p className="App-logo"><Link to="/" >koralle</Link></p> <div className="App-navbar-item"> <ul> {NavMenuLiTag} </ul> </div> </nav> </header> ); } export default Header;終わりに
訂正等あればコメントお願いします...
- 投稿日:2019-12-21T11:55:07+09:00
【React練習問題】①カウントアップ、ダウンをつくろう
概要
これはReactを学ぶ上で、実際の現場で利用できる実装を取得するためのプログラム。
可読性、パフォーマンスなども求める。課題仕様
コードサンドボックスを利用する
https://codesandbox.io/s/画面
・0から始まるカウントアップ、ダウンを実装する
・今どこまでカウントされているかを画面に表示すること
・カウントアップボタンを表示して押せるようにすること
・カウントダウンボタンを表示して押せるようにすること
・デザインの綺麗さなどは問いませんルール
・FCでつくる
・useStateを使う
・useCallbackを使う
・memoを使う
・カウントアップ、ダウンボタンをそれぞれコンポーネントに分ける回答