20191221のReactに関する記事は18件です。

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

https://reactjs.org/

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.

https://reactjs.org/

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 にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なっています。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限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 --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active 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

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
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

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

半年間業務で学んだ技術を使って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

https://reactjs.org/

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.

https://reactjs.org/

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 にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限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 --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active 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

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
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

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

業務で学んだ技術を使って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

https://reactjs.org/

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.

https://reactjs.org/

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 にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限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 --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active 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

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
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

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

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

https://reactjs.org/

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.

https://reactjs.org/

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 にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限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 --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active 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

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
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

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

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

https://reactjs.org/

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.

https://reactjs.org/

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 にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限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 --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active 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

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
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

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

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.js

store.jsに初期値を設定&Reducerを置き、Providerを定義する

今回はカウンターアプリなので、以下のように設定します。

src/store.js
import { 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.js
import 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.js
import 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.js
import 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のチュートリアルを見るとよくわかると思います。

参考文献

今回カウンターアプリを作るにあたり以下の記事を参考にさせていただきました。ありがとうございました。

Reduxから Context API with Hooks へ

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

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

どんな機能を作るか?

土台なので,まずはユーザーグループを管理する上で最低限必要となりそうな機能のみ考えています.
それらの機能の大枠とフローは下記の通りです.

スクリーンショット 2019-12-21 23.17.13.png

この記事でやっていること

ユーザーグループの作成から新規ユーザーの追加まで,です.

今回のユーザーグループ作成の機能では,誰でもユーザーグループを作成できるものの,ユーザーグループを作ったユーザー=管理者という形になっています.また,グループに新しいユーザーを追加できるのもグループを作った管理者のみとなっています.

新規ユーザーグループの作成・管理者登録

0. Firebaseの設定

公式のクイックスタートと変わりないですが,とりあえずこれがなければ始まらないので.
Google Providerやfunctionsは追々使うので入ってますが,本記事では使ってません.

configFirebase.tsx
import 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. トップページ

スクリーンショット 2019-12-21 20.02.10.png

これはソースいらないと思いますが一応.

TopPage.tsx
import 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. ユーザーグループ・管理者アカウントの作成

スクリーンショット 2019-12-21 20.02.56.png

とりあえず必要最低限の情報を取得し,
firebase.auth().createUserWithEmailAndPassword()
に投げてユーザーグループの管理者となるアカウントの登録を行っています.
なので,ここではユーザーグループという箱を作ってる訳ではなく,グループを作れるアカウントを作成してるだけです.

注)
本来はここでメールアドレスの所有確認を前段に挟みますが,私の都合で省略しています.
メールアドレスの本人確認メールを投げ,継続URLから登録に進む,,,という流れは追って修正するか,別記事書いてこちらにリンク貼ります.
(公式だとこの辺の機能ですね.https://firebase.google.com/docs/auth/web/passing-state-in-email-actions?hl=ja)
ここはGoogleやFacebookのProviderを使っても良いですが,個人都合でメアド&パスワードにしています.

ソースは各機能に関する部分を抜粋して載せていきます.
本筋に関係のない部分は割愛しています.

SignUpForm.tsx
const 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のページにて,入力したアドレスでユーザーが作成されています.

スクリーンショット 2019-12-21 20.44.59.png

3. 管理者登録後の遷移

ユーザーグループの作成(というより管理者登録)を終えたら,
firebase.auth().onAuthStateChanged()
で認証状態の変化を検知し,ユーザーグループの管理画面へ遷移させます.
(前述のソースコードではルーティング部分の処理を省略してるので,自動で遷移はしません)

スクリーンショット 2019-12-21 20.59.17.png

管理者の初回ログインでは,グループの識別に使うグループUIDの作成と,管理者権限の管理をfirestoreで行えるよう役割を付与しています.本記事ではグループUID生成をカバーできていないので,これまた追々更新したいところ...

スクリーンショット 2019-12-21 23.38.38.png

2回目以降のログインや,管理者権限を持たないユーザーのログイン時は,firestoreからユーザーが所属するグループや役割を取得する形になっています.

App.tsx
import 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.tsx
const 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

スクリーンショット 2019-12-21 22.32.30.png

ここでグループへ追加ボタンを押すと,先ほどのFirebase Authenticationのページへ新規ユーザーが作成されます.

スクリーンショット 2019-12-21 22.31.26.png

AddUserPage.tsx
type 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>
  );
};

おわりに

本記事ではここまでです.
ユーザーグループを管理するアカウントの作成から,新規ユーザー追加までを行いました.
とりあえず作ってみるかと勢いで始めましたが,書いてると色々足りない部分が出てきますね...
自分は書き始めないと詳細な設計部分などが見えてこないタイプですが,色々甘すぎました.
必要な機能の全体像や設計は改めて整理したいと思います.

(次は肝心の新規ユーザーを対象グループへ紐付ける部分,新規ユーザーが初回ログインした時にどんな処理が走るかを書いていきます.)

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

【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を呼び出すだけで必要な関数を呼び出すことだできるようになる。

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

【React練習問題】②カスタムhooksをつかって表示制御をしよう

概要

これはReactを学ぶ上で、実際の現場で利用できる実装を取得するためのプログラム。
可読性、パフォーマンスなども求める。

課題仕様

コードサンドボックスを利用する
https://codesandbox.io/s/

画面

・commentを表示するためのボタンを2つ作る
・表示ボタンを押すと、コメントとその表示を消すボタンがコメントと同じ要素内に用事される
以下、私の実装結果の画面

comment実装.gif

ルール

・FCでつくる
・useStateを1回だけ使う
・useCallbackを2回だけ使う
・memoを使う

ヒント

・useStateを1回だけ使う
・useCallbackを2回だけ使う
これはカスタムフックスを使うことで解決できる

回答

https://qiita.com/takujiro_0529/items/564b0ac28a90df550d58

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

睡眠時間記録webアプリを作る際に便利だったライブラリなど

作ったもの

https://sleepkun.now.sh/

起きた時間と寝た時間を記録するだけのwebアプリケーション「sleepkun」を作りました。
データはIndexedDBに保存するようになっています。
firebaseの方が複数端末で使えるかなと思ったりしましたが、やる気の関係で見送りました。

image.png

リポジトリ

リポジトリはこちら。
https://github.com/lisp719/sleepkun

作業風景

30分くらいの動画です。

next.jsで睡眠時間記録webアプリを作る

便利だったもの

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の組み合わせがめっちゃ便利。

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

next.jsで睡眠時間記録webアプリを作りました

作ったもの

https://sleepkun.now.sh/

起きた時間と寝た時間を記録するだけのwebアプリケーション「sleepkun」を作りました。
データはIndexedDBに保存するようになっています。
firebaseの方が複数端末で使えるかなと思ったりしましたが、やる気の関係で見送りました。

image.png

リポジトリ

リポジトリはこちら。
https://github.com/lisp719/sleepkun

作業風景

30分くらいの動画です。

next.jsで睡眠時間記録webアプリを作る

便利だったもの

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の組み合わせがめっちゃ便利。

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

【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>
)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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()で一行増やすくらいなら自分はアロー関数を使おうかと思います.可読性もアロー関数の方が上がりそう.

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

保守性や堅牢性を高める!モダンフロントエンド開発に必要な周辺技術をまとめてみました

はじめに

前日もくもく会で一緒になったエンジニア初学者の方を見て、ポートフォリオとしてフロントエンド開発はちゃんとできてはいるものの、実務において必要な保守性や堅牢性の高いコードについての意識がどうしても不足しているなという印象を受けました。

もちろん初学者の方にそのようなことを求めるのは酷な話なので知らないからダメ、ということを言うつもりは毛頭ありません。
が、職業としてエンジニアを目指すためにフロントエンド開発をやっている場合、保守性や堅牢性を意識すると企業からの評価が段違いなのではないかと思うのです。

そこで今回の記事では単にフロントエンドが開発できるというスキルだけではなく、実務における保守性や堅牢性を意識したフロントエンドを開発するために有効になるであろう技術について説明してみたいと思います。

対象者

  • プログラミング初学者の方で、モダンな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

最後に

以上、各種関連言語、ライブラリ等について紹介させていただきました。

これらを利用することで職業エンジニアを目指している方であれば、実用的な技術を使ったポートフォリオを構築して企業からの評価を高めることができるでしょうし、実務で利用すれば保守性が高くかつ堅牢なアプリケーションを継続的に開発することが可能になります。

もちろん導入自体にもコストがかかりますし、これら全てを導入する必要はありませんが、時間とリソースが許す限り導入を検討し、そして導入するのがいいでしょう。

(時間的余裕があれば、実際にここで並べた技術を用いてフロントエンドアプリケーションを開発するハンズオン記事を書きたいと思いますが、それはまた次回...)

ここ間違ってない?これも使った方がいんじゃね?というものがあれば教えてください!

それでは、ありがとうございました!

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

<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",    // <- 追加
    ......
  },

慣れてきたかと思いますのでコマンド操作は省きますが、
mkdirtouchを使って下のようなファイル構造にしてください。

react_router
> node_modules
> src
  -index.html
  > js
    - client.js
    > pages
      - Layout.js

また、以下のjs、htmlファイルはコピペしてもらって構いません。
初期準備です。

webpack.config.js
var 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.js
import 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 &copy; KillerNews.net</p>
        </div>
      </div>
    </footer>
  </div>

  <!-- /.container -->
  <script src="client.min.js"></script>
</body>
</html>
src/js/client.js
import 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.js
import React from "react";

export default class Featured extends React.Component {
  render() {
    return (
      <h1>Featured</h1>
    );
  }
}

②Archivesコンポーネント

src/js/pages/Archives.js
import React from "react";

export default class Archives extends React.Component {
  render() {
    return (
      <h1>Archives</h1>
    );
  }
}

③Settingsコンポーネント

src/js/pages/Settings.js
import React from "react";

export default class Settings extends React.Component {
  render() {
    return (
      <h1>Settings</h1>
    );
  }
}

React Rouer

Router初登場です。
client.jsにRouterを使って変更を加えていきます。

src/js/client.js
import 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.js
import 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>

    );
  }
}

http://localhost:8080にアクセス

archivesを押すと

Archives

が表示され
settingsを押すと

Settings

が表示されたらLinkがつながってます!

button

さっきのLinkのところにbuttonを付けましょう
Layout.jsを編集。

src/js/pages/Layout.js
import 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

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

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も使えると思います。多分。

サーバーサイドレンダリングとは?

サーバーサイドレンダリングとは、PHPRubyJavaのように、サーバーサイドで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.jsonscriptsを追記。

{
  "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 Index

HTMLのように、JSXでも子要素を使うことができます。

import React from 'react'

function Index() {
  // ()で括り、;の自動挿入に対応
  // returnで返すJSXは必ず1つの要素
  return (
    <div>
      <h1>Hello, Next.js!</h1>
    </div>
  )
}

export default Index

JSXは{}で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 Index

useStateで関数コンポーネントに状態をもたせる

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 Index

Next.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の段階で、titlemetaが設定されていることがわかります。

イメージを掴んでいただくために、試しに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 Heading

CSS部分はテンプレートリテラルなので、${}内で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 Heading

SCSSのようにネストが使えます。

// 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でいろいろなことができるので、ぜひ掘り下げてみてください。

公式ガイド:https://emotion.sh/docs/introduction

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

Gatsby組込みのLinkコンポーネントを使ってナビゲーションバーを自作する

初めに

この記事はGatsby.js Advent Calendar 2019 21日目の記事です。
GatsbyJSを色々いじってたらいい感じのナビゲーションバーが自作できた気がしました。

目次

  • 本記事の目標
  • 前提 - 開発環境 -
  • Gatsby Link APIについて
  • ディレクトリ構成
  • まずはひな型を作る
  • 共通のレイアウトはLayoutコンポーネントとして定義する
  • Headerコンポーネントの実装
  • 終わりに

本記事の目標

こんな感じのナビゲーションバーを作りたいと思います。
それっぽいものがフロントエンド初心者の私でも作れたので投下します。
重要なことですが、このレイアウトではBootstrap4は使っていません。

サンプルを公開しています。触ってみてください → サンプル
ソースコード→ https://github.com/koralle/gatsby-link-navbar

無題.png

前提 - 開発環境 -

以下私の手元の開発環境になります。

  • 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.js
import 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.jsHeader.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.js
import 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;

終わりに

訂正等あればコメントお願いします...

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

【React練習問題】①カウントアップ、ダウンをつくろう

概要

これはReactを学ぶ上で、実際の現場で利用できる実装を取得するためのプログラム。
可読性、パフォーマンスなども求める。

課題仕様

コードサンドボックスを利用する
https://codesandbox.io/s/

画面

・0から始まるカウントアップ、ダウンを実装する
・今どこまでカウントされているかを画面に表示すること
・カウントアップボタンを表示して押せるようにすること
・カウントダウンボタンを表示して押せるようにすること
・デザインの綺麗さなどは問いません

image.png

ルール

・FCでつくる
・useStateを使う
・useCallbackを使う
・memoを使う
・カウントアップ、ダウンボタンをそれぞれコンポーネントに分ける

回答

https://qiita.com/takujiro_0529/items/ac3eeaed96b35f57d0fc

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