20200921のReactに関する記事は12件です。

ReactとTypescriptで簡単なTODOやってみた

内容

こちらで学んだ内容を残します
【世界で7万人が受講】Understanding TypeScript - 2020年最新版

環境構築

まずreactアプリを作成します。以下のコマンドでtypescriptを最初から導入できます。globalでインストールすれば以下のコマンドが確実に実行されるようです。

$ sudo npm install -g create-react-app
$ create-react-app my-app --template typescript
$ cd my-app/src
// 不要なファイルを削除
$ rm -rf App.css App.test.tsx logo.svg serviceWorker.ts setupTests.ts 

そしてApp.tsxの中身を以下のようにします。

import React from 'react';

function App() {
 return (
    <div className="App"></div>;
  )
}

export default App;

index.tsxの中身を以下に修正します。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

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

index.cssを以下のようにします。

html {
  font-family: sans-serif;
}

body {
  margin: 0;
}

最初のカスタムコンポーネントを作る

まずサーバーを起動しておきます。

$ npm start

App.tsxのconst AppにFunctionコンポーネントを割り当てる

React.FCとはReact Function Componentの略だそうです。その名の通り、関数形式でコンポーネントを作成しています。これを使用する理由はただのfunctionでは型が十分ではないからです。
またこのように割り当てることで自動補完が使えます。

App.tsx

const App: React.FC = () => {
 return (
    <div className="App"></div>;
  )
}

TODOリストのコンポーネントディレクトリとファイルを作成します

$ mkdir components
$ touch TodoList.tsx

TodoList.tsx

import React from 'react';

interface TodoListProps {
  items: { id: string; text: string }[];
}

const TodoList: React.FC<TodoListProps> = props => {
  return (
    <ul>
      {props.items.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
};

export default TodoList;

toDoListコンポーネントではli要素を返すようにします。
ここで大事なことは、mapで返しているli要素の中にidでkeyを指定していることです。
隣り合った要素を一意に識別するためにkeyがないとreactは中身を確認せずに全て同じ要素だと認識するからです。

これをApp.tsxに組み込みます。

import TodoList from './components/TodoList';

const App: React.FC = () => {
  const todos = [{ id: 't1', text: 'TypeScriptコースの完了' }];
  return (
    <div className="App">
      <TodoList items={todos} /> <-ここ
    </div>
  );
};

また、親コンポーネントから子コンポーネントにtodosを渡しているのですが、typescriptがこのpropsがどういうものか判断するためinterfaceを使用しています。React.FCでは子コンポーネントのpropsの型まできちんと見ます。オブジェクト型の配列です

import React from 'react';

interface TodoListProps {
  items: { id: string; text: string }[];
}

const TodoList: React.FC<TodoListProps> = props => {
  return (
    <ul>
      {props.items.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
};

export default TodoList;

refを使ったユーザー入力の取得

componentsディレクトリ配下に新規ファイルを作成します

$ touch NewTodo.tsx

ここに新しくTodoリストを作成して、今あるTodoリストに追加する処理を書きます。

まずApp.tsxに新しく追加するためのリストを追加します。

import NewTodo from './components/NewTodo';

<div className="App">
    <NewTodo /> <-ここ
    <TodoList items={todos} />
</div>

NewTodo.tsxを以下のように修正します。

import React, { useRef } from 'react';

const NewTodo: React.FC = () => {
  const textInputRef = useRef<HTMLInputElement>(null);
  const todoSubmitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = textInputRef.current!.value;
    console.log(enteredText);
  };

  return (
    <form onSubmit={todoSubmitHandler}>
      <div>
        <label htmlFor="todo-text">Todo内容</label>
        <input type="text" id="todo-text" ref={textInputRef} />
      </div>
      <button type="submit">TODO追加</button>
    </form>
  );
};

export default NewTodo;

ボタンがsubmitされた際にその値を受け取り、Appコンポーネントに渡すようにします。

まずsubmitされた際にinputのvalueを受け取るようにします。イベント関数を書きます。eventをpreventしてjsの操作にして、ref(Hooks)を使用して参照を読み取る。双方向バインディング。input要素のrefを追加。
useRefのジェネリクスはinput要素なのでを入れる。
nullを渡して、初期値はなしです。

import useRef from 'react'

const textInputRef = useRef<HTMLInputElement>(null);
const todoSubmitHandler = (event: React.FormEvent) => {
  event.preventDefault();
  const enteredText = textInputRef.current!.value;
  console.log(enteredText);
};

コンポーネント間の連携

NewTodoコンポーネント間で取得した値をAppコンポーネントに渡したい。
そのためイベントハンドラ関数をApp.tsxの中にtodoAddHandlerとして追加します。これでinputのイベントを拾えます。onAddTodoで関数の参照プロパティを渡します。

const todoAddHandler = (text: string) => {

  };

<NewTodo onAddTodo={todoAddHandler} />

そして、NewToDoの中でpropsとしてイベントハンドラを受け取ります。
その際に、受け取るPropsの肩を指定してあげる必要があります。
今回はtypeで指定します。そして関数のジェネリクスでPropsの型を定義します。

type NewTodoProps = {
  onAddTodo: (todoText: string) => void;
};

const NewTodo: React.FC<NewTodoProps> = props => {

そして以下で要素をイベントハンドラ経由で取得できました。

props.onAddTodo(enteredText);

stateの利用・型

AppのstateをuseStateを利用していきます。
App.tsxを以下のようにします。

import React, { useState } from 'react';

const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const todoAddHandler = (text: string) => {
    setTodos(prevTodos => [
      ...prevTodos,
      { id: Math.random().toString(), text: text },
    ]);
  };

useStateの第一引数はtodosという最新の配列、setTodosでstateを変更していきます
setTodosの第一引数はidをランダムで、第二引数は受け取ったテキストを。
ここで、このままではstateの型はnever型と判定されてしまいます。なぜならuseStateを空の配列で初期化しているからです。

const [todos, setTodos] = useState([]);
  const todoAddHandler = (text: string) => {
    setTodos({ id: Math.random().toString(), text: text })

もしuseStateに文字列を渡すのであればnever型にはなりません。
空の配列を渡したい場合は、ジェネリクス型で指定してあげれば大丈夫です。

 <{id: string; text: string;}>[]

このようにしてtodoの型を指定してあげてもいいのですが、他の場所でも使用したいので名前付き型として他のファイルに移動します。

$ touch todo.models.ts

そしてこの中に

export interface Todo {
  id: string;
  text: string;
}

としてexportしてあげて、それをApp.tsxのなかで使用します。

import { Todo } from './todo.model';

const [todos, setTodos] = useState<Todo[]>([]);

このままではsetTodosの中身は毎回新しいものになってしまいます。そこで以下のように修正します。

const todoAddHandler = (text: string) => {
    setTodos(prevTodos => [
      ...prevTodos,
      { id: Math.random().toString(), text: text },
    ]);
  };

stateの更新が通常非同期で行われるため、前回の状態を受け取りつつ、それを更新する処理にしたいです。その場合、useStateから受け取った関数のsetTodosの引数に関数を渡してあげることで、前回の最新の状態を更新することができます。(...prevTodos)

削除機能を作る

まず、TodoList.tsxの以下の箇所を修正します

<span>{todo.text}</span>
  <button>削除</button>

そしてApp.tsxのなかで新しいハンドラ関数を作成します。これはtodoIdを受け取って、そのIdを削除する処理です、fileterで既存の配列から特定の要素を除いた新しい配列を作成します。これは関数を受け取ります。
todo.idと、受け取ったtodoId(削除したいId)を除いた新しい配列を作成する。

const todoDeleteHandler = (todoId: string) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== todoId))
  }

これがTodoListの中から呼ばれた時に削除するように関数を呼び出します。

<TodoList items={todos} onDeleteTodo={todoDeleteHandler} />

TodoListコンポーネントにonDeleteTodoを関数として追加します。

interface TodoListProps {
  items: { id: string; text: string }[];
  onDeleteTodo: (id: string) => void; <-ここ
}

そしてonclickされた時にこれが呼ばれるようにします。ここでは参照だけします。bindメソッドでtodo.idを渡してあげて引数としています。

<button onClick={props.onDeleteTodo.bind(null, todo.id)}>削除</button>

これで動作完了です。あとはいい感じにスタイルを追加して終わりです。

以下にここまでの全てのコードを残します。


ソースコードツリー

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.tsx
│   ├── components
│   │   ├── NewTodo.css
│   │   ├── NewTodo.tsx
│   │   ├── TodoList.css
│   │   └── TodoList.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   └── todo.model.ts
├── tsconfig.json
└── yarn.lock


ソースコード

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

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

App.tsx

import React, { useState } from 'react';

import TodoList from './components/TodoList';
import NewTodo from './components/NewTodo';
import { Todo } from './todo.model';

// function App() {
//   return <div className="App"></div>;
// }

const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  const todoAddHandler = (text: string) => {
    setTodos(prevTodos => [
      ...prevTodos,
      { id: Math.random().toString(), text: text },
    ]);
  };

  const todoDeleteHandler = (todoId: string) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== todoId))
  }

  return (
    <div className="App">
      <NewTodo onAddTodo={todoAddHandler} />
      <TodoList items={todos} onDeleteTodo={todoDeleteHandler} />
    </div>
  );
};

export default App;

todo.model.ts

export interface Todo {
  id: string;
  text: string;
}

TodoList.tsx

import React from 'react';

import './TodoList.css';

interface TodoListProps {
  items: { id: string; text: string }[];
  onDeleteTodo: (id: string) => void;
}

const TodoList: React.FC<TodoListProps> = props => {
  return (
    <ul>
      {props.items.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={props.onDeleteTodo.bind(null, todo.id)}>削除</button>
        </li>
      ))}
    </ul>
  );
};

export default TodoList;

NewTodo.tsx

import React, { useRef } from 'react';

import './NewTodo.css';

type NewTodoProps = {
  onAddTodo: (todoText: string) => void;
};

const NewTodo: React.FC<NewTodoProps> = props => {
  const textInputRef = useRef<HTMLInputElement>(null);
  const todoSubmitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = textInputRef.current!.value;
    props.onAddTodo(enteredText);
  };

  return (
    <form onSubmit={todoSubmitHandler}>
      <div className="form-control">
        <label htmlFor="todo-text">Todo内容</label>
        <input type="text" id="todo-text" ref={textInputRef} />
      </div>
      <button type="submit">TODO追加</button>
    </form>
  );
};

export default NewTodo;

index.css

html {
  font-family: sans-serif;
}

body {
  margin: 0;
}

TodoList.css

ul {
  list-style: none;
  width: 90%;
  max-width: 40rem;
  margin: 2rem auto;
  padding: 0;
}

li {
  margin: 1rem 0;
  padding: 1rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
  border-radius: 6px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

NewTodo.css

form {
  width: 90%;
  max-width: 40rem;
  margin: 2rem auto;
}

.form-control {
  margin-bottom: 1rem;
}

label, input {
  display: block;
  width: 100%;
}

label {
  font-weight: bold;
}

input {
  font: inherit;
  border: 1px solid #ccc;
  padding: 0.25rem;
}

input:focus {
  outline: none;
  border-color: #50005a;
}

button {
  background: #50005a;
  border: 1px solid #50005a;
  color: white;
  padding: 0.5rem 1.5rem;
  cursor: pointer;
}

button:focus {
  outline: none;
}

button:hover,
button:active {
  background: #6a0a77;
  border-color: #6a0a77;
}

参考

雰囲気で使わない @types/react
https://qiita.com/Takepepe/items/0bbe4ab6ff4858a4e186

useRef
https://ja.reactjs.org/docs/hooks-reference.html#useref

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

【Reactで】小説投稿サイトなどに良くある「全○話」みたいなのを表示させる方法

はじめに

今回は小説投稿サイトなどによくある「全○話」を自作アプリにて表示させるのに結構苦労した(3日かかった)ので、戒めとして残しておきたいと思います。

実現したかったこと

  • 用意したのは「シリーズ」というフォルダ的な役割を持つモデルと、「アイテム」というシリーズに複数個格納されるモデルの2つ(1対多の関係)

  • ルートページにて「シリーズ」全件を表示させ、その「シリーズ」が所有する「アイテム」を全て取得し、その総数をカウントさせ「全〜件」という形で表示させたい。

苦労した理由

  • 表示させたいのがルートページだったからです。普通なら各シリーズが所有するアイテムを取得しようとする場合、例えばURLが"/series/104"なら、シリーズのパラメータ(この場合なら104)を取得して、そのパラメータを頼りにアイテムを取得します。

  なので、パラメータが存在しないルートページでどうやって各シリーズのパラメータを取得すりゃええんじゃいとと半ばキレかけながら考えていたわけです(今思えば単純な話でした)

環境・前提等

環境

  • フロントエンド

    • React(v16.8以上)
    • React Hooks(カスタムフックを使う)
    • axios
  • バックエンド

    • Rails(5.2系)

前提

  • CORSの設定、モデル作成などの工程は省略します。
  • PUMAでRails側のローカルホストをデフォルトで3001に指定しています。

Rails側

  • モデル

スクリーンショット 2020-09-21 16.29.12.png

  • コントローラ

    • Api::V1::SeriesController
    • このコントローラにてシリーズ全件を返すアクションと、アイテムのカウントを返すアクションを作成する。
  • ルーティング

    • ルート:"/""api/v1/series#index"
    • アイテム取得: "api/v1/item_count/:series_id""api/v1/series#item_count"

React側

  • 用意するコンポーネント
    • Homeコンポーネント: シリーズを全件取得し、Seriesというコンポーネントに各データを順繰り渡す役割りを持たせる。
    • Seriesコンポーネント: このコンポーネントにて各シリーズを表示させる。
    • ItemCountコンポーネント: 各シリーズが持つアイテムの総数だけを表示させる。
    • useFetchカスタムフック: Railsからデータを取得する。

Rails側のコード

ルーティング

routes.rb
 Rails.application.routes.draw do

  # ルート
  root to: 'api/v1/series#index'
  # アイテムのカウント
  get 'api/v1/item_count/:id', to: 'api/v1/series#item_count'

end

コントローラ

app/controller/api/v1/series_controller.rb
class Api::V1::SeriesController < ApplicationController

    # item_countアクションに、パラメータから取得したシリーズをコールバック
    before_action :set_series, only: [:item_count]

    def index
        @series =Series.all
        render json: { 
            status: 200,
            series: @series,
            keyword: "index_of_series"  # React側で使う
        }
    end

    def item_count
        @items = @series.items.all    # シリーズに関連付けられているアイテムの取得
        @items_count = @items.count    # アイテムの総数をカウント
        render json: {
            status: 200, 
            item_count: @item_count,   # カウントをJSONとしてReactへ送信
            keyword: "item_count"     # React側で使う
        }
    end


    private

        # パラメータを頼りにシリーズを取得
        def set_series
            @series = Series.find(params[:id])
        end

end

React側のコード

// 階層

//src
//  ├ Home.js
//  ├ Series.js
//  ├ ItemCount.js
//  └ useFetch.js

useFetchカスタムフック

src/useFetch.js
import { useState, useEffect } from "react"
import axios from 'axios'

// カスタムフックでは文頭はuseが必須
// useFetchの引数に、methodとurlを渡す
// これは、HomeとItemCountコンポーネントにて、Railsとの通信に使う
// HTTPリクエストと、ルーティングを指定するため
export default function useFetch({method, url}) {
    // 初期値の定義。
    const [items, setItems] = useState("")

    useEffect(() => {
        const getItems = () => {
            // ここのmethodとurlにて、Home・ItemCountコンポーネントから
            // 送られてくるメソッドとルーティングを代入することになる。
            axios[method](url)
                .then(response => {
                    let res = response.data
                    let ok = res.status === 200
                    // シリーズ全件取得
                    // Rails側で指定したkeywordはここで使う。
                    // そうしてカウントとの区別を付けている。
                    if (ok && key === 'index_of_series') {
                        setItems({ ...res.series })
                    // シリーズごとのアイテムの総数を取得
                    } else if (ok && key === 'item_count') {
                        setItems(res.item_count)
                    }
                })
                .catch(error => console.log(error))
        }
        getItems()
    }, [method, url, items])

    return {
        items  // items変数を他のコンポーネントで使えるようにする。
    }
}

Homeコンポーネント

src/Home.js
import React from 'react'

import Series from './Series'
import useFetch from './useFetch'

function Home() {
    // ここでは、useFetchからRailsで取得したシリーズのデータを受け取っている。
    // methodはget、urlはRailsのルートのURLを指定。これにより、
    // useFetchからRailsのルートのルーティングへリクエストが送信され、
    // その後Railsから受け取ったデータをitemsへ格納します。
    const { items } = useFetch({
        method: "get",
        url: 'http://localhost:3001'
    })

    return (
        <div>
             {/* Object.keys()メソッドを使い、JSONで送られてくるitemsを */}
             {/* ループ処理で1個ずつSeriesコンポーネントに渡している。 */}
             {/* JSONは、{ {...}, {...}, {...} }のようなものであると想定 */}
              {Object.keys(items).map(key => (
                  <Series key={key} items={items[key]} />
              ))}
        </div>
    )
}

export default Home

Seriesコンポーネント

src/Series.js
import React from 'react'

import ItemCount from './ItemCount'

function Series(props) {
    // Homeから送られてくるpropsを頼りに、各シリーズのidをここで取得しています。
    // このidをパラメータとして使うことで、各シリーズの所有するアイテムにアクセスすることができます。
    const seriesId = props.items.id
    const seriesTitle = props.items.title

    return (
        <div>
             <div>{seriesTitle}</div>
             {/* ItemCountコンポーネントに、シリーズのidを渡す。 */}
             <ItemCount {...props} seriesId={seriesId} />
        </div>
    )
}

export default Series

ItemCountコンポーネント

src/ItemCount.js
import React from 'react'

import useFetch from './useFetch'

function SeriesCount(props) {
    // useFetchを使いRailsと通信。
    // methodはget、urlはRailsの`api/v1/item_count/${props.seriesId}`を指定。
    // id部分にSeriesコンポーネントから渡ってくる各シリーズのidを嵌め込むことで、
    // Railsの"api/v1/item_count/:id"というルーティングへリクエストが送信され、
    // その後Railsから各シリーズの持つアイテムのカウント数を受け取り、最後にitemsへ格納されます。
    const { items } = useFetch({ 
       method: 'get', 
       url: `http://localhost:3001/api/v1/item_count/${props.seriesId} `
    })

    return (
        <div>
            {/* Railsから送られてくるアイテムの総数をここにレンダリングします。 */}
            (このシリーズは全部で {items} 個のアイテムを所有しています)
        </div>
    )
}

export default SeriesCount

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

react-router-bootstrap で LinkContainers が2カ所以上 Active にするのをなんとかしたいな

困った

サイドメニューに設定したLinkContainerとは別に、ヘッダロゴにもトップへのリンクをつけようとしたんだけど、いざロゴをクリックしたらサイドメニューのActiveが消えずに、事前に表示していた場所とトップの2カ所がアクティブになってしまう

せいかい

Issueを読もう

https://github.com/react-bootstrap/react-router-bootstrap/issues/242#issuecomment-613761912

export const MyMenu = () => {
  const {pathname} = useLocation() // previously imported from 'react-router-dom'
  return (
    <ListGroup key={pathname}>
      <LinkContainer exact to="/path/to/first"><ListGroup.Item action active={false}>First Item</ListGroup.Item></LinkContainer>
      <LinkContainer exact to="/path/to/second"><ListGroup.Item action active={false}>Second Item</ListGroup.Item></LinkContainer>
      <LinkContainer exact to="/path/to/third"><ListGroup.Item action active={false}>Third Item</ListGroup.Item></LinkContainer>
    </ListGroup>
  );
};

パスが変わった時点で再度レンダリングすればきれいになるよ、ということらしい

目から鱗

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

herokuで.envの環境変数が反映されない!!!

Herokuで個人開発してる作品をDeployしようとしたら、なぜか
Server did not receive report: Origin Error message: API key not valid. Please pass a valid API key.
のエラーが出たので対処法を記しておきたいと思います。

結論から言いますと、どうやらHerokuの方でも.envの設定が必要なようです。
:fist_tone1:heroku plugins:install heroku-config
:fist_tone1:heroku config:push

で再びHerokuにgit push heroku masterしたら解決しました!!!!

で解決です!!!

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

iOS14でReact Nativeアプリで画像が表示されない

パッチファイル作成

以下コマンドでルートディレクトリ配下に patches/react-native+0.61.5.patch を作成する。

$ npx patch-package react-native

npx: 150個のパッケージを3.464秒でインストールしました。
patch-package 6.2.2
patch-package: you have both yarn.lock and package-lock.json
Defaulting to using npm
You can override this setting by passing --use-yarn or deleting
package-lock.json if you don't need it

• Creating temporary folder
• Installing react-native@0.61.5 with npm
• Diffing your files with clean files
✔ Created file patches/react-native+0.61.5.patch

以下コードを追加する。

diff --git a/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m b/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m
index 01aa75f..4ef8307 100644
--- a/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m
+++ b/node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m
@@ -269,6 +269,8 @@ - (void)displayLayer:(CALayer *)layer
   if (_currentFrame) {
     layer.contentsScale = self.animatedImageScale;
     layer.contents = (__bridge id)_currentFrame.CGImage;
+  } else {
+    [super displayLayer:layer];
   }
 }

以下のコマンドを叩き、 node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m に反映させる。267行目付近。

$ patch -p1 -i patches/react-native+0.61.5.patch
- (void)displayLayer:(CALayer *)layer
{
  if (_currentFrame) {
    layer.contentsScale = self.animatedImageScale;
    layer.contents = (__bridge id)_currentFrame.CGImage;
  } else {
    [super displayLayer:layer]; // 記載されていることを確認
  }
}

キャッシュクリアしてビルド

$ watchman watch-del-all
$ rm -rf ios/build
$ rm -rf node_modules
$ rm -rf ~/Library/Developer/Xcode/DerivedData
$ yarn
$ cd pod; pod install

参考

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

【Nodeが壊れた】internal/modules/cjs/loader.js:582 throw err;

Reactで作られたサービスをクローンして勉強していたら、Nodeが壊れた(のかもしれない)

internal/modules/cjs/loader.js:582
    throw err;
    ^

Error: Cannot find module 'C:\Users\User\Desktop\NodeJsProject\app.js'

解決策

理由はわからんが、以下のコマンドを打ったらうまくいった

$ npm uninstall --save-dev request
$ npm install --save request
$ npm start

スクリーンショット 2020-09-21 15.44.07.png

参考
https://stackoverflow.com/questions/53545800/internal-modules-cjs-loader-js582-throw-err
https://qiita.com/TakuTaku04/items/02ff2f4555f705e8c055

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

RaspberryPi上でCommonLispでWebアプリ作ってLチカ

RaspberryPi上でCommonLispでWebアプリ作ってLチカ

Raspberry Pi 4 買ったけど全然使ってなかったから久しぶりに引っ張り出してきて遊んでみた

事前準備

サーバ

Roswellをインストール
roswell/wiki/Installationにかかれている手順でインストール
インストール後、以下の手順でパスを通す

$ echo 'PATH=$HOME/.roswell/bin:$PATH' >> ~/.profile
$ source ~/.profile

Clackをインストール

$ ros install clack

Utopianをインストール

$ ros install fukamachi/utopian

フロント

nvm使ってNode.jsインストール

$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
$ nvm install stable --latest-npm
$ nvm alias default stable

Lチカ

サーバ

utopian 使って適当にさくっと作る
めっちゃ楽

app.lisp
(defpackage #:server-app
  (:use #:cl
        #:cffi
        #:utopian))
(in-package #:server-app)

;;; WiringPi Wrapper
;; Lチカに必要なものだけ適当に書いておく
(define-foreign-library libwiringPi
  (:unix "libwiringPi.so")
  (t (:default "libwiringPi")))

(use-foreign-library libwiringPi)

(defconstant +output+   1)
(defconstant +gpio-14+ 14)
(defconstant +on+       1)
(defconstant +off+      0)

(defcfun ("wiringPiSetupGpio" wiringpi-setup-gpio) :int)
(defcfun ("pinMode" pin-mode) :void
  (pin :int) (mode :int))
(defcfun ("digitalWrite" digital-write) :void
  (pin :int) (value :int))

(wiringpi-setup-gpio)
(pin-mode +gpio-14+ +output+)

;;; Controller
(defun aget (item alist)
  (cdr (assoc item alist :test #'string=)))

(defun index (params)
  (declare (ignore params)))

(defun blink (params)
  (let ((status (aget "led" params)))
    (cond ((string= status "on")
           (digital-write +gpio-14+ +on+))
          ((string= status "off")
           (digital-write +gpio-14+ +off+))
          (t nil))))

;;; Routes
(defroutes *routes* ((:GET "/" #'index)))
(route :GET "/blink" #'blink)

;;; Run
(defapp minimal-app () ())
(make-instance 'minimal-app
               :routes *routes*)

以下のコマンドでサーバ起動

$ clackup -s utopian app.lisp --address {ユーザ名}.local

フロント

React.js でフロント側も適当にさくっと作る
Reactの新しいプロジェクトを作成し、実行できるか確認する

$ npx create-react-app my-app
$ cd my-app
$ npm start

デフォルトのソースコードをすべて削除し、新しく index.jsx ファイルを作成する

$ sudo rm -f ./src/*
$ touch ./src/index.jsx

ついでにstyled-components だけ入れとく

$ npm install --save styled-components

index.jsx の中身は以下の通り

index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';

const FormWrapper = styled.div``;
const Button = styled.button``;

const App = () => {
  const serverUrl = 'http://{ホスト名}.local:5000';

  const fetchBlinkLed = (query = '') => (
    fetch(`${serverUrl}/blink?${query}`, {
      method: 'GET',
      mode: 'no-cors',
      credentials: 'same-origin',
    })
  );

  const ledOn = () => {
    fetchBlinkLed('led=on');
  };

  const ledOff = () => {
    fetchBlinkLed('led=off');
  };

  return (
    <FormWrapper>
      <Button onClick={ledOn} >LED ON</Button>
      <Button onClick={ledOff} >LED OFF</Button>
    </FormWrapper>
  );
};

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

以下のコマンドでフロント起動

$ npm start

別PCなどでブラウザでフロント側にアクセスする(今回の場合はポート番号3000)

{ホスト名}.local:3000

以下のような感じになる

Screenshot from 2020-09-21 12-56-52.png

「LED ON」「LED OFF」ボタンクリックでLチカできる

Digital Write

ちょっとだけ応用

サーバ

app.lisp
;; ラッパーは外部ファイルへ
(load "wiringpi-wrapper.lisp" :external-format :utf-8)

(defpackage #:server-app
  (:use #:cl
        #:utopian
        #:wiringpi-wrapper))
(in-package #:server-app)

;;; Controller
(defun aget (item alist)
  (cdr (assoc item alist :test #'string=)))

(defun index (params)
  (declare (ignore params)))

(defconstant +output+ 1)
(defconstant +high+   1)
(defconstant +low+    0)

(wiringpi-setup-gpio)

;; Lチカだけでなく、Digital Write全般
(defun d-write (params)
  (let ((pin    (parse-integer (aget "pin" params)))
        (status (aget "status" params)))
    (pin-mode pin +output+)
    (cond ((string= status "high")
           (digital-write pin +high+))
          ((string= status "low")
           (digital-write pin +low+))
          (t nil))))

;;; Routes
(defroutes *routes* ((:GET "/" #'index)))
(route :GET "/d-write" #'d-write)

;;; Run
(defapp minimal-app () ())
(make-instance 'minimal-app
               :routes *routes*)

以下のコマンドでサーバ起動

$ clackup -s utopian app.lisp --address {ホスト名}.local

フロント

index.jsx
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';

const FormWrapper = styled.div``;
const Label = styled.div`
  fontWeight: bold;
`;
const Button = styled.button``;
const Select = styled.select``;

const App = () => {
  const serverUrl = 'http://fireflower0.local:5000';
  // GPIOピンセレクタ
  const gpioPins = [
    { label: 'GPIO02', value: 2 },
    { label: 'GPIO03', value: 3 },
    { label: 'GPIO04', value: 4 },
    { label: 'GPIO05', value: 5 },
    { label: 'GPIO06', value: 6 },
    { label: 'GPIO07', value: 7 },
    { label: 'GPIO08', value: 8 },
    { label: 'GPIO09', value: 9 },
    { label: 'GPIO10', value: 10 },
    { label: 'GPIO11', value: 11 },
    { label: 'GPIO12', value: 12 },
    { label: 'GPIO13', value: 13 },
    { label: 'GPIO14', value: 14 },
    { label: 'GPIO15', value: 15 },
    { label: 'GPIO16', value: 16 },
    { label: 'GPIO17', value: 17 },
    { label: 'GPIO18', value: 18 },
    { label: 'GPIO19', value: 19 },
    { label: 'GPIO20', value: 20 },
    { label: 'GPIO21', value: 21 },
    { label: 'GPIO22', value: 22 },
    { label: 'GPIO23', value: 23 },
    { label: 'GPIO24', value: 24 },
    { label: 'GPIO25', value: 25 },
    { label: 'GPIO26', value: 26 },
    { label: 'GPIO27', value: 27 },
  ];

  const [pin, setPin] = useState(0);

  const fetchDigitalWrite = (query = '') => {
    fetch(`${serverUrl}/d-write?${query}`, {
      method: 'GET',
      mode: 'no-cors',
      credentials: 'same-origin',
    })
  };

  const digitalWriteHigh = () => {
    fetchDigitalWrite(`pin=${pin}&status=high`);
  };

  const digitalWriteLow = () => {
    fetchDigitalWrite(`pin=${pin}&status=low`);
  };

  const makeSelect = () => (
    <Select onChange={v => setPin(v.target.value)} >
      {gpioPins.map(pin => (
        <option key={pin.value} value={pin.value} >
          {pin.label}
        </option>
      ))}
    </Select>
  );

  return (
    <FormWrapper>
      <Label >Digital Write</Label>
      {makeSelect()}
      <Button onClick={digitalWriteHigh} >High</Button>
      <Button onClick={digitalWriteLow} >Low</Button>
    </FormWrapper>
  );
};

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

以下のコマンドでフロント起動

$ npm start

別PCなどでブラウザでフロント側にアクセスする(今回の場合はポート番号3000)

{ホスト名}.local:3000

以下のような感じになる

Screenshot from 2020-09-21 15-46-45.png

色んなGPIOピンが試せる

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

React に TypeScript で 拡張メソッドを作る

拡張メソッド とは

既存のライブラリ(たとえばNPMで取得した各種ライブラリ)、stringやnumberといった型に対して、変更を加えることなく、インスタンスメソッドや静的メソッドを追加するテクニックです。

簡単な例からいきましょう

string にインスタンス拡張メソッドを作成

TypeScriptを利用して文字を取り扱う場合

    let hoge = " AA;BB;CC";
    console.log(hoge.trim());

    const huga = "AA;BB;CC ";
    console.log(huga.trim());

と string 型の 変数 や 引数 を利用しますね。

ちなみに、ES標準型は String で 内部的に String.prototype.length のような形で prototype上に定義され、私たちがTypeScriptで開発する場合は、d.ts 経由で(lib.es5.d.tsなど)それがあたかもインスタンスメソッドであるかのように扱えます。

同じことで、次の 3つの手順 を踏むことで既存の型に最初から存在したかのようなメソッドを作成できます

  • 拡張したいオブジェクトと同名のインターフェイスを定義
  • 定義に一致するメソッド本体の実装(prototype汚染を防ぎつつ)
  • Bundleするようにimport する or 最初から headerに scriptタグ書いて読み込んでおく

簡単な例のゴール

半角カタカナを判断する拡張メソッドのサンプル
simple string extensions
スクリーンショット 2020-09-21 113517.png
importもない状態で、インテリセンスまで効いた上で動いていますね。あたかも String インスタンスにもとからそういうメソッドがあったかのように。
ちなみに、拡張メソッドは pascal表記 が良いと思います。
区別しやすいのと、バッティング防止ですね。ライブラリ作成者とかぶったら困りますからね。

説明します

拡張したいオブジェクトと同名のインターフェイスを定義

今回は String のインスタンスを拡張したいので、もとの定義を参照すると
スクリーンショット 2020-09-21 113852.png
とありますので、どこか適当なフォルダに同名のインターフェイスと実装したいメソッドを定義します。

image.png
これだけで、もう String にはメソッドが生えます。TypeScriptだから当然ですね。

定義に一致するメソッド本体の実装(prototype汚染を防ぎつつ)

JavaScriptのprototypeに直接メソッドをはやすと思わぬ副作用が出ます。これを prototype 汚染といいます。
これを抑止した上で prototype を拡張する場合は Object.defineProperty を利用します。
image.png
詳細は上述リンクをご参照ください。
今回の実装は value:function として実装します。注意点はアロー関数だと this のスコープが変わるので、function で実装してください。
この中の this は String インスタンス本体を指し示します。呼び出し元だと困りますからね。

Bundleするようにimport する or 最初から headerに scriptタグ書いて読み込んでおく

ここで作成した拡張メソッドは、各 tsx や ts で 個別に import はしません。
最初の例でも
スクリーンショット 2020-09-21 113517.png
import には style.css しか存在しませんが、正しくインテリセンスも仕事して、実行時もそれが呼び出されています。

TypeScript上でインテリセンスが仕事するのは、TSにインターフェイスがあるからで、あとはこの本体が ブラウザでの実行時に bundle されるか何かで読み込まれていればよいのです。
普通にHTMLなので HTML,CSS,JSで構成され、HTML上にJSがロードされる必要があります。ここら辺は JQuery 開発者であれば、何を当たり前のことを・・・と思うかもしれませんね。時代の流れですかね。

react であれば root 定義で読んどけば bundle されるので以降は気にせず好きな場所でimport気にせずに利用できます。
image.png

ジェネリッククラスの拡張

例えば、配列に拡張メソッドを生やそうとすると Array<T> が対象になります。
TS上は ジェネリック ですが、JSになれば何も関係ないので、同じ手順で同じように作成すると、配列に拡張メソッドが作成できます。

Chunk や GroupBy なんてあると便利なので、サンプル作ってみましょう。
simple array extensions
image.png
適当な場所に ジェネリック付きinterface 定義して、Object.defineProperty 上ではもはやJSの世界なので、型なく扱えます
image.png

React で 実践的な拡張メソッド

たとえば トースト通知のライブライで有名なものに notistackがあります
image.png

使い方もリンクの先にありますが

import { useSnackbar } from 'notistack';
const MyButton = () => {
    const { enqueueSnackbar, closeSnackbar } = useSnackbar();

    const handleClick = () => {
        enqueueSnackbar('I love hooks');
    };

    return (
        <Button onClick={handleClick}>Show snackbar</Button>
    );
}

トースト通知を enqueueして 閉じるアクションで dequeue するみたいな、内部実装の気持ちはわかるのですが、使用する側からみて直観的ではないのと、冗長だと感じました。
image.png
notistack extensions
トーストをインスタンス化して、info や error などを呼び出せば適切な位置に適切なアイコンとともにメッセージを出すサンプルです。

銀の弾丸ではないので、何にでも使えるわけではないのですが、拡張メソッドを使うことでコードの凝縮性が高まる、プロジェクトで一貫した処理が行えるなどカバナンス効果などなど期待できることもあります

プロジェクトメンバーが好き勝手に生やすと困るので、ルールを決めてみんなで相談して生やしていければ、疎結合な開発資産もたまっていって、よいのではないでしょうか。

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

react-reduxでHooksを使うには

はじめに

前回 の続きです。

セットアップ

下記のようにDOMをrenderする部分(コンポーネントのツリーの最上位に位置するコンポーネント)をProviderタグで包み込んでstoreをコンポーネント全体で使えるようにします。

const store = createStore(rootReducer)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

react-reduxを使う前にuseCallbackを理解する

useCallbackってなに?

すごく簡単に言うとコールバック関数を返すメソッド。

// ドキュメントより

const memoizedCallback = useCallback(
    () => {
        doSomething(a, b);
    },
    [a,b],
);

初回のrender時にはdoSomething()が実行される、以後は第2引数で設定したa,bのいずれかが変化した場合のみ新たにdoSomething()を実行し、memoizedCallback に代入する。

ただし、再renderされた時にa,bのいずれにも変化がなかった場合はdoSomething()は新しく実行されずに前に実行された結果が代入されたmemoizedCallback が返される。

これはアロー式で関数を定義するとrenderごとに毎回定義した関数を実行してしまい(=厳密に言うと関数のオブジェクトを作る)、結果不必要なrenderを行うことを避けるための処置になります。

React HooksのuseCallbackを正しく理解する ## useCallbackとは何か より

useCallbackがやることは、「コールバック関数の不変値化」です。
「関数を決定する処理のメモ化」と言えるかもしれません。アロー式は原理的に常に新規関数オブジェクトを作ってしまいますが、useCallbackは「意味的に同じ関数」が返るかどうかを判別して、同じ値を返す関数が返る>>べきなら新規のアロー式を捨てて、前に渡した同じ結果を返す関数を使い回しで返します。

では、具体的にどういうところで使うのかというと例えば以下のような例があります。

import React, { useState } from 'react';
import Form from 'react-bootstrap/Form';

function Example() {
    const[text, setText] = useState("");
    const[email, setEmail] = useState("");

    return(
        <Form.Group controlID="exampleForm.ControlInput1">
            <Form.Label>Example Input</Form.Label>
            <Form.Control type="email" rows={3}  placeholder="email" onChange={(e) => setEmail(e.target.value)}/>
        </Form.Group>
        <Form.Group controlID="exampleForm.ControlTextarea1">
            <Form.Label>Example textarea</Form.Label>
            <Form.Control as="textarea" rows={3}  onChange={(e) => setText(e.target.value)}/>
        </Form.Group>
    )
}


Input、及びTextareaの入力にuseStateを使い、onChangeのイベントハンドラでアロー関数を使っていますが、前述の通りアロー関数はrenderごとに毎回定義した関数を生成して実行するので、
例えば上記の例だとInputまたはTextareaでpropsに変更がある度に、renderが始まってどちらも再描画されてしまいます。
これを防ぐために以下のようにします。

import React, { useState } from 'react';
import Form from 'react-bootstrap/Form';

function Example() {
    const[text, setText] = useState("");
    const[email, setEmail] = useState("");

    function ExampleuseCallback1() {
        const[text, setText] = useState("");
        const Textarea_onChange = useCallback((e) => setText(e.target.value), [setText]);
    }

    function ExampleuseCallback2() {
        const[email, setEmail] = useState("");
        const Email_onChange = useCallback((e) => setEmail(e.target.value), [setEmail]);
    }

    return(
        <Form.Group controlID="exampleForm.ControlInput1">
            <Form.Label>Example Input</Form.Label>
            <Form.Control type="email"  placeholder="email" onChange={Email_onChange}/>
        </Form.Group>
        <Form.Group controlID="exampleForm.ControlTextarea1">
            <Form.Label>Example textarea</Form.Label>
            <Form.Control as="textarea" rows={3}  onChange={Textarea_onChange}/>
        </Form.Group>
    )
}


useCallbackでイベントハンドラをラップすることでExample Inputに変更があった場合はそちらはrender時に再描画されますが、Example textareaは直前のrenderの値がパスされるだけで再描画はされません。(逆もしかり)

こういうことをcallback関数をメモ化するというそうです。

useSelector

useSelector()は引数にグローバルストアを指定し、必要なステートをプロパティとして取り出します。アクションがディスパッチされるとステートが更新されていた場合のみ、コンポーネントを再レン​​ダリングします。

ReduxではstateがStoreと呼ばれるものに集約されるのでそこから必要なstateを取り出すためのメソッドということになります。
例を見てみると

import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

export const CounterComponent = () => {
    // storeにあるstateのうち、sate.counterを呼び出して渡す
    const counter = useSelector(state => state.counter)
    return
        <div>{counter}</div>
}

export const TodoListItem = props => {
        // storeにあるstateのうち、state.todosのうちpropsで渡されたidのものを呼び出して渡す
    const todo = useSelector(state => state.todos[props.id])
    return
        // todoプロパティの中からtextの値を抽出
        <div>{todo.text}</div>
}

またメモ化した例だと以下のようになる。

import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

// createSelectorでstoreから必要なstateを呼び出す処理をメモ化

const selectNumOfDoneTodos = createSelector(
    state => state.todos,
    todos => todo.filter(todo => todo.isDone).length
)

export const DoneTodosCounter = () => {
    // メモ化しSelectorをuseSelectorに代入。これでstate、todosプロパティに変更がない場合は初回以降の再renderはない。
    const NumOfDoneTodo = useSelector(selectNumOfDoneTodos)
    return
        <div>{NumOfDoneTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>
  )
}


また少し複雑だが、単一のコンポーネント、インスタンスでしか使用されないSelectorがコンポーネントのpropsに依存する場合の書き方は以下の通りである。

import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';


// selectorの設定
const selectNumOfTodosWithIsDoneValue = createSelector(
    state => state.todos,
    (_, isDone) => isDone,
    (todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)

export const TodoCounterForIsDoneValue = ({ isDone }) => {
    // ここでグローバルステートから引っ張ってくるstateを現在のstate,isDoneを引数にしたselectNumOfTodosWithIsDoneValueメソッドで返す
    const NumOfTodosWithIsDoneValue = useSelector(state =>
        selectNumOfTodosWithIsDoneValue(state, isDone)
    )

    return
        <div>{NumOfTodosWithIsDoneValue}</div>
}

export const App = () => {
    return (
        <>
            <span>Number of done Todos:</span>
            <TodoCounterForIsDoneValues isDone={true}/>
        <>
    )
}


しかし、今度はSelectorがコンポーネントのpropsに依存するが複数のコンポーネント、インスタンスで使用される場合はどうなるかというと以下の通りである。

import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

const makeNumOfTodosWithIsDoneSelector = () => createSelector(
    state => state.todos,
    (_, isDone) => isDone,
    (todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)

export const TodoCounterForIsDoneValue = ({ isDone }) => {
    // makeNumOfTodosWithIsDoneSelectorの実行結果をuseMemoでメモ化する
    const selectNumOfTodoWithIsDone = useMemo(
        makeNumOfTodoWithIsDoneSelector, []
    )

    const numOfTodosWithIsDoneValue = useSelector(state =>
        selectNumOfTodosWithIsDone(state, isDone)
    )

    return
    <div>{numOfTodosWithIsDoneValue}</div>
}

export const App = () => {
        return (
            <>
                <span>Number of done todos:</span>
                <TodoCounterForIsDoneValue isDone={true} />
            </>
        )
}

useDispatch

useDispatchはRedux ストアからディスパッチ関数への参照を返し、必要に応じてアクションをディスパッチするために使うことができます。

子コンポーネントにこれを利用してコールバック関数を渡す場合はやはり前述の例に習ってuseCallbackでメモ化するのがよいそうです。
では、実例を見てみます。

import React from 'react';
import { useDispatch } from 'react-redux';

export const CounterComponent = ({ value }) => {
    // storeに紐付く、dispatchを取得
    const dispatch = useDispatch()

    return (
        <div>
            <span>{value}</span>
            <button onClick={() => dispatch({ typeL 'increment-counter' })}>
                Increment counter
            </button>
        </div>
    )
}


これをuseCallbackを使ってリファクタリングすると以下の通りになる。

import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';

export const CounterComponent = ({ value }) => {
    const dispatch = useDispatch()
    // dispatchをuseCallbackでラップする
    const incrementCounter = useCallback(
        () => dispatch({ type: 'increment-counter '}),[dispatch]
    )
}

// React.memoでReactでコンポーネントをメモ化する
// これにより、メモ化されたコンポーネントが返した要素を再render時に比較して、必要な場合のみrenderを行う
// 実際はincrementCounterと逆の処理を行うdecrementCounterコンポーネントを用意し、どちらかの更新があった際に変更のないボタンを再renderしない……といったようなことをするための処置

export const MyIncrementButton = React.memo(({ onIncrement }) => (
  <button onClick={onIncrement}>Increment counter</button>
))

return (
    <div>
      <span>{value}</span>
      <!-- MyIncrementButtonincrementCounterを渡してボタンをrenderする -->
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  )
}



ポイントは

  • useCallbackでdispatchをラップしていること(特に、incrementCounterはイベントハンドラ用途なので)

  • React.memoでIncrement counterボタンを作るコンポーネントをラップすることによってボタンの不必要な再renderを防いでいること

の2点です。

ここでドキュメントの例ではイマイチ腑に落ちない……かもしれないので、参考先のページであるベストな手法は? Reactのステート管理方法まとめ さんから以下のコードを引用させていただいてuseSelectorとuseDispatchについて実例を踏まえて理解を深めていきます。


import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';


const add_todo = 'add_todo';
const completed_task = 'completed_task';

// action
export const addTodo = payload => ({
    type: add_todo,
    payload
});

export const completedTask = payload => ({
    type: completed_task,
    payload
});


// reducer
const initialState = { todos: [] };

export const todoReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'add_Todo':
            const newTodos = [...state.todos];
            newTodos.push({ id: state.todos.length + 1, task: action.payload });
            return {
                ...state,
                todos: newTodos
            };
        case 'complete_task':
            const filteredTodos = [...state.todos];
            filteredTodos.splice(action.payload, 1);
            return {
                ...state,
                todos: filteredTodos
            };
        default:
            return state;
    }
};

// Redux

export const Todo = () => {
    // テキストインプット用のローカルステート
    // これはフォームの入力部分の管理に使う(=このコンポーネントでのみの利用でよい)のでuseStateでローカルに管理するほうが楽
    const [input, updateInput] = useState("");

    // useSelector,useDispatch
    const dispatch = useDispatch();
    const { todos } = useSelector(state => state.todo);

    // テキストインプットを監視するHooks
    const onChangeInput = useCallback(
        event => {
            updateInput(event.target.value);
        },
        [updateInput]
    );

    // チェックボックスのイベント更新を監視するHooks
    const onCheckListItem = useCallback(
        event => {
            dispatch({ type: 'complete_task', payload: event.target.name });
        },
        [dispatch]
    );

    // ローカルステートとDispatch関数を監視するHooks
    const addTodo = useCallback(() => {
        dispatch({ type: 'add_todo', payload: input });
        updateInput("");
    }, [input, dispatch]);

    return (
        <>
            <input type="text" onChange={onChangeInput} value={input} />
            <button onClick={addTodo}>追加</button>
            <ul>
                {todos.map((todo, index) => (
                    <li key={todo.id}>
                        <input type="checkbox" onChange={onCheckListItem} name={index} />
                        {todo.task}
                    </li>
                ))}
            </ul>
        </>
    );
};


Todoアプリにおけるタスクの追加と既存のタスクの一覧にチェックボックスをつけて完了・未完了で区別する……という処理になります。
ポイントは以下の通りです。

  • ローカルステートとuseSelector・useDispatchでのステート管理を使い分けて併用している

  • ローカルステートとDispatch関数を同時にuseCallbackでラップしている

2番目の点については、タスクの追加には当然テキストインプットの部分が関わってくるのでそのローカルステートが変わったときにのみdispatchとupdateInputが実行されるのが適切だということですね。

useDispatchをuseCallbackする理由については先述の通りです。今回もonChange、onClickとイベントハンドラで使っているのがわかると思います。

useStore

こちらは冒頭で示した

const store = createStore(rootReducer)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

コンポーネントにReduxストアへの参照を渡すという処理がありますが、useStoreはこの処理で渡されたReduxストアへの参照を返します。

しかし、通常は上記のコードを実装した上で、useSelectorを使うので、ドキュメントによるとReducerの置き換えなどのようにどうしても特別にストアへのアクセスを必要とするような処理に使うのが推奨されているようです。

単純にstoreを取得するには以下のようなコードで実装できます。

import React from 'react'
import { useStore } from 'react-redux'

export const CounterComponent = ({ value }) => {
    const store = useStore()

    return
        <div>{store.getState()}</div>
}

ドキュメントではあくまでのuseStoreの処理の例として紹介されていて実装することは非推奨となっています。

なお、この状態ではstoreの状態が更新されてもコンポーネントは自動的に更新されることはありません。

また、余談としては

const store = createStore(rootReducer)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

こちらのコードではContextを使うことができるようです。
Contextは普段、コンポーネントがpropsでリレーしながらデータをやり取りしているのに対してContextに収容されたデータはpropsを介さず直接アクセスできるという特徴があります。

ドキュメントの例を見てみましょう。

import React from 'react'
import {
    Provider,
    createStoreHook,
    createDispatchHook,
    createSelectorHook
} from 'react-redux'


// 現在のContextを返す
const MyContext = React.createContext(null)

// MyContextを引数に各カスタムフックを定義する
export const useStore = createStoreHook(MyContext)
export const useDispatch = createDispatch(MyContext)
export const useSelector = createSelector(MyContext)

const myStore = createStore(rootReducer)

export function MyProvider({ children }) {
    return (
        <Provider context={MyContext} store={myStore}>
            <!-- 子コンポーネント  -->
            {children}
        </Provider>
    )
}

こうすることでContextがグローバルに定義されてpropsを介さずともuseStore、useDispatch、useSelectorにアクセスできるということになります。

補足

従来のconnectを使っていた部分をReduxでもHooksで代用できるようになったというのが今回のお話ですがそれ故の不具合もあるようで、react-reduxでHooksを使う場合はuseSelectorを使って以下のことに、留意するべきだとドキュメントには書いてあります。

Don't rely on props in your selector function for extracting data

セレクタ関数のpropsに頼らずにデータを抽出する

In cases where you do rely on props in your selector function and those props may change over time, or the data you're extracting may be based on items that can be deleted, try writing the selector functions defensively. Don't just reach straight into state.todos[props.id].name - read state.todos[props.id] first, and verify that it exists before trying to read todo.name.

セレクタ関数でpropsに依存している場合で、それらのpropsが時間の経過とともに変化する可能性がある場合や、抽出するデータが削除可能な項目に基づいている可能性がある場合は、セレクタ関数を防御的に記述してみてください。state.todos[props.id].nameに直接手を伸ばしてはいけません - state.todos[props.id]を最初に読み、todo.nameを読み込もうとする前に存在するかどうかを確認してください。

Because connect adds the necessary Subscription to the context provider and delays evaluating child subscriptions until the connected component has re-rendered, putting a connected component in the component tree just above the component using useSelector will prevent these issues as long as the connected component gets re-rendered due to the same store update as the hooks component.

connect は必要な Subscription をコンテキストプロバイダに追加し、接続されたコンポーネントが再レンダリングされるまで子サブスクリプションの評価を遅らせるので、useSelector を使用して接続されたコンポーネントをコンポーネントのすぐ上のコンポーネントツリーに置くことで、接続されたコンポーネントが hooks コンポーネントと同じストア更新によって再レンダリングされる限り、これらの問題を防ぐことができます。

As mentioned earlier, by default useSelector() will do a reference equality comparison of the selected value when running the selector function after an action is dispatched, and will only cause the component to re-render if the selected value changed. However, unlike connect(), useSelector() does not prevent the component from re-rendering due to its parent re-rendering, even if the component's props did not change.

前述したように、デフォルトでは useSelector() は、アクションがディスパッチされた後にセレクタ関数を実行する際に選択された値の参照し、値の変更がないかの比較を行い、選択された値が変更された場合にのみコンポーネントの再レンダリングを行います。ただし、connect() とは異なり、useSelector() は、コンポーネントのプロップが変更されていなくても、親の再レンダリングによってコンポーネントが再レンダリングされるのを防ぐことはできません。

If further performance optimizations are necessary, you may consider wrapping your function component in React.memo():

よって上記の問題の最適化が必要な場合は関数コンポーネントを React.memo() でラップすることを検討してください。

2番目に関しては今回のuseSelectorの項でやった以下のコードを見ればわかりますね。

import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

export const CounterComponent = () => {
    // storeにあるstateのうち、sate.counterを呼び出して渡す
    const counter = useSelector(state => state.counter)
    return
        <div>{counter}</div>
}

export const TodoListItem = props => {
        // storeにあるstateのうち、state.todosのうちpropsで渡されたidのものを呼び出して渡す。
    const todo = useSelector(state => state.todos[props.id])
    return
        // todoプロパティの中からtextプロパティの値を抽出。必ずstate.todos[props.id]を読み込んたあとの実行にする。
        <div>{todo.text}</div>
}

最後の部分に関しても今回の例に以下のように出てきました。

import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';

export const CounterComponent = ({ value }) => {
    const dispatch = useDispatch()
    // dispatchをuseCallbackでラップする
    const incrementCounter = useCallback(
        () => dispatch({ type: 'increment-counter '}),[dispatch]
    )
}

// React.memoでReactでコンポーネントをメモ化する
// これにより、メモ化されたコンポーネントが返した要素を再render時に比較して、必要な場合のみrenderを行う
// 実際はincrementCounterと逆の処理を行うdecrementCounterコンポーネントを用意し、どちらかの更新があった際に変更のないボタンを再renderしない……といったようなことをするための処置

export const MyIncrementButton = React.memo(({ onIncrement }) => (
  <button onClick={onIncrement}>Increment counter</button>
))

return (
    <div>
      <span>{value}</span>
      <!-- MyIncrementButtonincrementCounterを渡してボタンをrenderする -->
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  )
}


この例にはuseSelectorは使われていませんが、実際には合わせて使うことが殆どなので気をつけておきましょうと言うことですね。

ドキュメントでは以下のように例が示されています。

const CounterComponent = ({ name }) => {
    const counter = useSelector(state => state.counter)
    return (
        <div>
            {name} : {counter}
        </div>
    )
}

export const MemoizedCounterComponent = React.memo(CounterComponent)

つまり、useSelectorを含むコンポーネントに関して内部に不必要に再renderされるのが望ましくない箇所がある場合はメモ化しましょうというわけですね。
上記の例だとreturn以下が更新がない限り再renderしたくない部分になります。

参考

React Redux Hooks
React Context
ベストな手法は? Reactのステート管理方法まとめ
React hooksを基礎から理解する (useContext編)
React hooksを基礎から理解する (useCallback編) と React.memo)
React HooksのuseCallbackを正しく理解する

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

material ui の modal で Cannot read property 'hasOwnProperty' of undefined エラーが起きたときの解決法

概要

material ui の modal コンポーネントを使用したときに Cannot read property 'hasOwnProperty' of undefined というエラーが起きてしまいました。そのときの対処法を共有します。

解決法

<Modal> の中に div タグを書くようにしてください。

return (
<Modal>
  <div>
    test
  </div>
</Modal>
)

参考:
https://stackoverflow.com/questions/56622246/react-material-ui-modal-typeerror-cannot-read-property-hasownproperty-of-unde

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

ReactチュートリアルをVue.jsで実装

Vue.jsとReactの勉強のため,Reactのチュートリアルの「Tic Tac Toe」をVue.jsで実装してみる。

vue-cliを使ってプロジェクトを作成(省略)。
バージョンはVue2。

components/フォルダ以下に次のファイルを作成。

  • Square.vue
    • ゲームの1マスを管理
  • Board.vue
    • ゲームボード(9マス)を管理
  • Game.vue
    • ゲーム全体の状態(手番,履歴)を管理

ちなみにcssもチュートリアルと同じになるように移植した。

Square.vue

Square.vue
<template>
  <button class="square" @click="onClick">{{value}}</button>
</template>

<style scoped>
.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}
.square:focus {
  outline: none;
  background: #ddd;
}
</style>

<script>
export default {
  name: 'Square',
  props: {
    value: String,
    onClick: Function,
  },
}
</script>

Squareコンポーネントはゲームの1マスにあたるボタンを表示する。
同時にボタンクリックのイベントを発火してゲームが進行するのだが,ゲームの状態は上位のGameコンポーネントが管理している。
通常はemitを使うところなのだろうが,ここではbutton@clickに上位コンポーネントからプロパティとして引き渡されたonClick(名前は何でもよい)関数を当ててみたら上手く動いた。(この方法が正しいのかどうか,どなたかご教示ください。)

Board.vue

Board.vue
<template>
  <div>
    <div class="board-row" v-for="r in [0,1,2]" :key="r">
      <square v-for="c in [0,1,2]" :key="c"
        :value="squares[r*3+c]"
        :onClick="() => onClick(r*3+c)"
      />
    </div>
  </div>
</template>

<style scoped>
.board-row:after {
  clear: both;
  content: "";
  display: table;
}
.status {
  margin-bottom: 10px;
}
</style>

<script>
import Square from '@/components/Square'

export default {
  name: 'Board',
  components: {
    Square,
  },
  props: {
    squares: Array,
    onClick: Function,
  },
}
</script>

Boardコンポーネントはゲームの盤面である9つのマスを管理する。
Reactのチュートリアルでは,Square 1つのレンダリングを関数にして,それを9回呼び出す方式をとっている。
Reactではレンダリングのテンプレート自体がJavaScript(の拡張)なのでそういったことができるが,Vue.jsで相当する方法が判らなかったので,ここではv-forによる二重ループで実装した。
onClickは「上から渡されたハンドラ関数をマス番号の引数付きで呼び出す」アロー関数に設定する。

Game.vue

Game.vue
<template>
  <div class="game">
    <div class="game-board">
      <board
        :squares="current.squares"
        :onClick="handleClick"
      />
    </div>
    <div class="game-info">
      <div>{{status}}</div>
      <ol>
        <li v-for="(step, move) in history" :key="move">
          <button @click="() => jumpTo(move)">{{move ? 'Go to move #'+move : 'Go to game start'}}</button>
        </li>
      </ol>
    </div>
  </div>
</template>

<style scoped>
.game {
  display: flex;
  flex-direction: row;
}
.game-info {
  margin-left: 20px;
}
ol, ul {
  padding-left: 30px;
}
</style>

<script>
import Board from '@/components/Board'

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ]
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a]
    }
  }
  return null
}

export default {
  name: 'Game',
  components: {
    Board,
  },
  data() {
    return {
      history: [{
        squares: Array(9).fill(null)
      }],
      stepNumber: 0,
      xIsNext: true,
    }
  },
  computed: {
    current() {
      return this.history[this.stepNumber]
    },
    winner() {
      return calculateWinner(this.current.squares)
    },
    status() {
      return this.winner ? 'Winner: ' + this.winner : 'Next player: ' + (this.xIsNext ? 'X' : 'O')
    },
  },
  methods: {
    handleClick(i) {
      const history = this.history.slice(0, this.stepNumber + 1)
      const current = history[history.length - 1]
      const squares = current.squares.slice()
      if (calculateWinner(squares) || squares[i]) {
        return
      }
      squares[i] = this.xIsNext ? 'X' : 'O'
      this.history = history.concat([{ squares: squares }])
      this.stepNumber = history.length
      this.xIsNext = !this.xIsNext
    },
    jumpTo(step) {
      this.stepNumber = step
      this.xIsNext = (step % 2) === 0
    },
  }
}
</script>

Gameコンポーネントはゲーム全体の状態を管理し,履歴を遡るための機能を提供している。
やはりレンダリングテンプレートを関数にできないことから,<template>の中がReactに比べて無理をしがち。履歴制御ボタンの@clickやテキストにそのあたりが表れている。
Vue.jsの流儀であれば,ここもコンポーネントにするべきなのだろうが,今回はReactチュートリアルとの比較しやすさをとった。

App.vue

App.vue
<template>
  <div id="app">
    <game />
  </div>
</template>

<style lang="scss">
#app {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}
</style>

<script>
import Game from '@/components/Game'

export default {
  name: 'App',
  components: {
    Game,
  },
}
</script>

最後にGameコンポーネントを呼び出すようにApp.vueを修正。

動作画面はこんな感じ。
tic-tac-toe.png

今回の気付き

  • ReactではレンダリングテンプレートがJavaScriptであることにより,関数化などの柔軟な実装が可能。
  • 下位コンポーネントで発生したイベントを,上位コンポーネントのハンドラで処理するためにプロパティが使える(要調査)。
  • v-forで生成される要素に固有のハンドラを割り当てるために,@clickにアロー関数を設定する方法が使える。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hooks を使ってTodoApp を作成する。

1 はじめに

2020年9月 現在

日本語でReact について検索すると、Class base で書かれた記事はよく見つかるけれど、react hooks を使って書かれた(関数)で書かれた記事が少ないと感じます。
海外サイトを見ていると、Reactの記述をClass baseで書いている人は、2020年9月 現在ほとんどいません。
記事が書かれた時期が2年前だとほとんどClass base ですが、、、(2018年11月 react hooks 発表前 or 直後)
現在は、React Hooksを使って関数で書くことが主流です。(簡潔に書けるから)
なので、これからReactを勉強する もしくは 勉強し始めの方は、React Hook を使ったアプリ制作を推奨します。

2 概要

React hooks を 用いて、こんな感じのTodo アプリを作っていきます。
Todo を Create, Complete, Delete します。
・ github link : https://github.com/TokyoProgramming/todolist

Todolist_new_1

ディレクトリ構成

   +-- backend ...
   |
   |
   |
   +-- frontend
       |   
       +-- src 
            |   
            +-- Components
            |   +-- Todo.js
            |   +-- Todo.css
            |   +-- TodoList.js
            |   +-- TodoList.css
            |
            +-- App.js 
            +-- App.css
            +-- index.js
            +-- index.css

3 App.js

App.js
import React from 'react';
import './App.css';
import Todo from "./Components/Todo";


function App() {
  return (
    <div className="app">
        <div className='body__card'>
            <h1>My TodoList</h1>
            <div className='body__todo'>
                <Todo />
            </div>
        </div>
    </div>
  );
}

export default App;

4.Todo.js

Todo.js
import React, {useState} from 'react';
import './Todo.css';
import {TransitEnterexit} from "@material-ui/icons";
import TodoList from "./TodoList";


function Todo() {
    const [todos, setTodos] = useState([])
    const [value, setValue] = useState('')

    //Create
    const CreateTodo = (e) =>{
        e.preventDefault();

        // console.log('create new todos')
        setTodos([...todos, { id: Math.random()*1000, text:value, isCompleted: false }]);
        setValue('')
    }

    // Complete
    const completeTodo = (id) => {
        const done = todos.map(todo => {
            if (todo.id === id) {
                todo.isCompleted = !todo.isCompleted
            }
            return todo
        })
        setTodos(done)
    };

    // Delete
    const deleteHandler = (id) => {
        setTodos(todos.filter(todo => todo.id !== id))
    } ;


    return (

        <div className="todo">
            <div className='todo__body'>
                <form
                    className="todo__input__form">
                    <TransitEnterexit/>

                    <input
                        className="todo__input"
                        value={value}
                        onChange={event => setValue(event.target.value)}
                        placeholder="  Set  your  todo . . . . ."
                    />
                    <button
                        disabled={!value}
                        type="submit"
                        onClick={CreateTodo}>submit</button>
                </form>
            </div>

            <div className='todo__todolist'>
                <ul>
                    {todos.map(todo => (
                        <TodoList
                            todo={todo}
                            key={todo.id}
                            // 他のcomponentには、関数も引き渡すことができます。
                            completeTodo = {completeTodo}
                            deleteHandlers = {deleteHandler}
                        />
                    ))}
                </ul>
            </div>
        </div>
      )
 }
 export default Todo

react hooks の中で最もよく使うuseState
使用する際は、まず、import react, {useState} from 'react';useStateimportします。
これを忘れると'useState' is not defined no-undef のエラー表示がされてしまいます。

Todo.jsでは、

    const [todos, setTodos] = useState([])
    const [value, setValue] = useState('')

2つのuseStateを定義しました。
(1) const [todos, setTodos] = useState([]) では、実際に作成するTodoを配列に格納していくためのものです。

例えば、以下のようにを書き換えるとわかりやすいと思います。

const[todos, setTodos] = useState([
    {
        id:1,
        text: "todo1",
        isCompleted: false
    },
    {
        id:2,
        text: "todo2",
        isCompleted: false
    },
])

Todolist_hardcoded

(2)const [value, setValue] = useState('')
実際にTodoを作成するフォーム機能のための、useStateです。
公式:https://ja.reactjs.org/docs/hooks-reference.html#usestate
例えば、const [value, setValue] = useState('Yoooooo')を記述すると、フォーム欄にあらかじめ、Yooooooと書かれています。

・Todo 作成

//Create
    const CreateTodo = (e) =>{
        e.preventDefault();

        // console.log('create new todos')
        setTodos([...todos, { id: Math.random()*1000, text:value, isCompleted: false }]);
        setValue('')
    }

e.preventDefault(),formsubmitした際、ページがリフレッシュしません。
... これで、配列をコピーします。めちゃめちゃシンプルですよね。
setTodos([配列をコピー, "追加"])
"追加": { id: Math.random()*1000, text:value, isCompleted: false }
idは、uniqueIdでなければならないので、uuidを使用する方法が一般的ですが、Math.random() でランダムな数字を作成します。残りは、text:value, isCompleted: falseを設定しておきます。

・Todo 完了

// Complete
    const completeTodo = (id) => {
        const done = todos.map(todo => {
            if (todo.id === id) {
                todo.isCompleted = !todo.isCompleted
            }
            return todo
        })
        setTodos(done)
    };

map()関数: https://ja.reactjs.org/docs/lists-and-keys.html#embedding-map-in-jsx

todo.isCompleted = !todo.isCompleted選択した todoisCompletedの逆にします。

・Todo 削除

filter()関数を使用します。

// Delete
    const deleteHandler = (id) => {
        setTodos(todos.filter(todo => todo.id !== id))
    } ;

5 TodoList.js

Todoで作成した、statefunction (関数)を 渡していきます。

import React, {useState} from 'react';
import './TodoList.css';

import DeleteForeverIcon from '@material-ui/icons/DeleteForever';
import DoneIcon from '@material-ui/icons/Done';
import {Edit} from "@material-ui/icons";

function TodoList( {todo, completeTodo, deleteHandlers, updateHandler } ) {



    return (
        <div className="todoList">
            <div
                className='todolist__card'
                style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
            >
                <DoneIcon
                    onClick={() => completeTodo(todo.id)}
                />
                {(todo.isCompleted) ? (
                    <p className="doneTodo">{ todo.text }</p>
                ):(
                    <p>{ todo.text}</p>
                )}
                <DeleteForeverIcon
                    onClick={() => deleteHandlers(todo.id)}
                />
            </div>
        </div>
      )
 }

 export default TodoList

あとは、cssを記述すると、上記のTodoアプリが出来上がります。

github link : https://github.com/TokyoProgramming/todolist

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