- 投稿日:2020-09-21T23:39:03+09:00
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.tsxTodoList.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/0bbe4ab6ff4858a4e186useRef
https://ja.reactjs.org/docs/hooks-reference.html#useref
- 投稿日:2020-09-21T19:15:39+09:00
【Reactで】小説投稿サイトなどに良くある「全○話」みたいなのを表示させる方法
はじめに
今回は小説投稿サイトなどによくある「全○話」を自作アプリにて表示させるのに結構苦労した(3日かかった)ので、戒めとして残しておきたいと思います。
実現したかったこと
用意したのは
「シリーズ」
というフォルダ的な役割を持つモデルと、「アイテム」
というシリーズに複数個格納されるモデルの2つ(1対多の関係)
ルートページ
にて「シリーズ」全件を表示させ、その「シリーズ」が所有する「アイテム」を全て取得し、その総数をカウント
させ「全〜件」という形で表示
させたい。苦労した理由
- 表示させたいのが
ルートページ
だったからです。普通なら各シリーズが所有するアイテム
を取得しようとする場合、例えばURLが"/series/104"
なら、シリーズのパラメータ(この場合なら104)を取得して、そのパラメータを頼りにアイテムを取得します。なので、パラメータが存在しないルートページでどうやって各シリーズのパラメータを取得すりゃええんじゃいとと半ばキレかけながら考えていたわけです(今思えば単純な話でした)
環境・前提等
環境
フロントエンド
- React(v16.8以上)
- React Hooks(カスタムフックを使う)
- axios
バックエンド
- Rails(5.2系)
前提
- CORSの設定、モデル作成などの工程は省略します。
- PUMAでRails側のローカルホストをデフォルトで3001に指定しています。
Rails側
- モデル
コントローラ
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.rbRails.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.rbclass 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 endReact側のコード
// 階層 //src // ├ Home.js // ├ Series.js // ├ ItemCount.js // └ useFetch.jsuseFetchカスタムフック
src/useFetch.jsimport { 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.jsimport 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 HomeSeriesコンポーネント
src/Series.jsimport 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 SeriesItemCountコンポーネント
src/ItemCount.jsimport 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
- 投稿日:2020-09-21T18:33:34+09:00
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> ); };パスが変わった時点で再度レンダリングすればきれいになるよ、ということらしい
目から鱗
- 投稿日:2020-09-21T16:56:27+09:00
herokuで.envの環境変数が反映されない!!!
Herokuで個人開発してる作品をDeployしようとしたら、なぜか
Server did not receive report: Origin Error message: API key not valid. Please pass a valid API key.
のエラーが出たので対処法を記しておきたいと思います。結論から言いますと、どうやらHerokuの方でも.envの設定が必要なようです。
heroku plugins:install heroku-config
heroku config:pushで再びHerokuにgit push heroku masterしたら解決しました!!!!
で解決です!!!
- 投稿日:2020-09-21T16:25:08+09:00
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参考
- 投稿日:2020-09-21T15:44:53+09:00
【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参考
https://stackoverflow.com/questions/53545800/internal-modules-cjs-loader-js582-throw-err
https://qiita.com/TakuTaku04/items/02ff2f4555f705e8c055
- 投稿日:2020-09-21T13:25:11+09:00
RaspberryPi上でCommonLispでWebアプリ作ってLチカ
RaspberryPi上でCommonLispでWebアプリ作ってLチカ
Raspberry Pi 4 買ったけど全然使ってなかったから久しぶりに引っ張り出してきて遊んでみた
事前準備
サーバ
Roswellをインストール
roswell/wiki/Installationにかかれている手順でインストール
インストール後、以下の手順でパスを通す$ echo 'PATH=$HOME/.roswell/bin:$PATH' >> ~/.profile $ source ~/.profileClackをインストール
$ ros install clackUtopianをインストール
$ 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 stableLチカ
サーバ
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.jsximport 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以下のような感じになる
「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.jsximport 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以下のような感じになる
色んなGPIOピンが試せる
- 投稿日:2020-09-21T12:55:45+09:00
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
importもない状態で、インテリセンスまで効いた上で動いていますね。あたかも String インスタンスにもとからそういうメソッドがあったかのように。
ちなみに、拡張メソッドは pascal表記 が良いと思います。
区別しやすいのと、バッティング防止ですね。ライブラリ作成者とかぶったら困りますからね。説明します
拡張したいオブジェクトと同名のインターフェイスを定義
今回は String のインスタンスを拡張したいので、もとの定義を参照すると
とありますので、どこか適当なフォルダに同名のインターフェイスと実装したいメソッドを定義します。
これだけで、もう String にはメソッドが生えます。TypeScriptだから当然ですね。定義に一致するメソッド本体の実装(prototype汚染を防ぎつつ)
JavaScriptのprototypeに直接メソッドをはやすと思わぬ副作用が出ます。これを prototype 汚染といいます。
これを抑止した上で prototype を拡張する場合は Object.defineProperty を利用します。
詳細は上述リンクをご参照ください。
今回の実装は value:function として実装します。注意点はアロー関数だと this のスコープが変わるので、function で実装してください。
この中の this は String インスタンス本体を指し示します。呼び出し元だと困りますからね。Bundleするようにimport する or 最初から headerに scriptタグ書いて読み込んでおく
ここで作成した拡張メソッドは、各 tsx や ts で 個別に import はしません。
最初の例でも
import には style.css しか存在しませんが、正しくインテリセンスも仕事して、実行時もそれが呼び出されています。TypeScript上でインテリセンスが仕事するのは、TSにインターフェイスがあるからで、あとはこの本体が ブラウザでの実行時に bundle されるか何かで読み込まれていればよいのです。
普通にHTMLなので HTML,CSS,JSで構成され、HTML上にJSがロードされる必要があります。ここら辺は JQuery 開発者であれば、何を当たり前のことを・・・と思うかもしれませんね。時代の流れですかね。react であれば root 定義で読んどけば bundle されるので以降は気にせず好きな場所でimport気にせずに利用できます。
ジェネリッククラスの拡張
例えば、配列に拡張メソッドを生やそうとすると
Array<T>
が対象になります。
TS上は ジェネリック ですが、JSになれば何も関係ないので、同じ手順で同じように作成すると、配列に拡張メソッドが作成できます。Chunk や GroupBy なんてあると便利なので、サンプル作ってみましょう。
simple array extensions
適当な場所に ジェネリック付きinterface 定義して、Object.defineProperty 上ではもはやJSの世界なので、型なく扱えます
React で 実践的な拡張メソッド
たとえば トースト通知のライブライで有名なものに notistackがあります
使い方もリンクの先にありますが
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 するみたいな、内部実装の気持ちはわかるのですが、使用する側からみて直観的ではないのと、冗長だと感じました。
notistack extensions
トーストをインスタンス化して、info や error などを呼び出せば適切な位置に適切なアイコンとともにメッセージを出すサンプルです。銀の弾丸ではないので、何にでも使えるわけではないのですが、拡張メソッドを使うことでコードの凝縮性が高まる、プロジェクトで一貫した処理が行えるなどカバナンス効果などなど期待できることもあります
プロジェクトメンバーが好き勝手に生やすと困るので、ルールを決めてみんなで相談して生やしていければ、疎結合な開発資産もたまっていって、よいのではないでしょうか。
- 投稿日:2020-09-21T01:09:13+09:00
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> <!-- MyIncrementButtonにincrementCounterを渡してボタンを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> <!-- MyIncrementButtonにincrementCounterを渡してボタンを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を正しく理解する
- 投稿日:2020-09-21T00:51:59+09:00
material ui の modal で Cannot read property 'hasOwnProperty' of undefined エラーが起きたときの解決法
- 投稿日:2020-09-21T00:43:57+09:00
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を修正。
今回の気付き
- ReactではレンダリングテンプレートがJavaScriptであることにより,関数化などの柔軟な実装が可能。
- 下位コンポーネントで発生したイベントを,上位コンポーネントのハンドラで処理するためにプロパティが使える(要調査)。
- v-forで生成される要素に固有のハンドラを割り当てるために,
@click
にアロー関数を設定する方法が使える。
- 投稿日:2020-09-21T00:33:42+09:00
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ディレクトリ構成
+-- backend ... | | | +-- frontend | +-- src | +-- Components | +-- Todo.js | +-- Todo.css | +-- TodoList.js | +-- TodoList.css | +-- App.js +-- App.css +-- index.js +-- index.css3 App.js
App.jsimport 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.jsimport 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 Todoreact hooks の中で最もよく使う
useState
使用する際は、まず、import react, {useState} from 'react';
でuseState
をimport
します。
これを忘れると'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 }, ])(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()
,form
をsubmit
した際、ページがリフレッシュしません。
...
これで、配列をコピーします。めちゃめちゃシンプルですよね。
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
選択したtodo
のisCompleted
の逆にします。・Todo 削除
filter()
関数を使用します。// Delete const deleteHandler = (id) => { setTodos(todos.filter(todo => todo.id !== id)) } ;5 TodoList.js
Todoで作成した、
state
とfunction (関数)
を 渡していきます。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