20210416のReactに関する記事は10件です。

React + RailsのSPAでdestroyアクションを実装する

これは何? React+Railsで簡単なタスク管理アプリを作っています。 ラベルの隣にある「削除」ボタンを押すと、確認メッセージののち、ラベルが削除されます。 実行環境は以下の通りです。 Rails 6.0.3 React 17.0.2 また、今回のディレクトリ構成は以下の通りです。(関係のある箇所だけ表示) . ├── controllers │   └── api │      └── labels_controller.rb └── javascript    └── pages    └── Labels.jsx Rails側 ある意味衝撃でした。詳細確認中ですが、controllerにもroutesにもdestroy関連アクションを何も記載しなくても動作しました。(axiosのためかしら。。。。) SPA側 削除メソッド自体はとてもシンプルで、axios.delete(URL[, オプション])で渡せばOKでした。その他、実装においてスラスラといかなかったところは解説します。 import React, { useState } from 'react' import axios from 'axios' export const Labels = (props) => { const [labels, setLabels] = useState([]) React.useEffect(async () => { const response = await axios.get('/api/labels'); setLabels(response.data) }, []) const removeLabel = (labelId, e) => { e.preventDefault(); if (window.confirm("ラベルを削除します。よろしいですか?")) { axios.delete("/labels/" + labelId, { headers: { // ★1解説します 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') } }) .then( res => { // ★3解説します const targetIndex = labels.findIndex( label => { return label.id === res.data.id }) const newLabels = labels.slice(); newLabels.splice(targetIndex, 1); setLabels(newLabels) }) .catch(data => { console.log(data); }) } } return ( <div> <h1>ラベル一覧</h1> // 略 <h3>ラベル</h3> <ul> {labels.map(label => ( <li key={label.id}> {label.name} <a href="" onClick={(e) => removeLabel(label.id, e)}>削除</a> // ★2解説します </li> ))} </ul> </div> ) } CSRFトークン ★1の部分です。RailsではGET以外のajaxリクエストにデフォルトで、ヘッダーにセキュリティトークンを付与します(詳細はRailsセキュリティガイドを参照)。 axios.delete("/labels/" + labelId, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') } }) AjaxでGET以外のリクエストを実施するときには、リクエストヘッダーのX-CSRF-Tokenの項に、トークンを付与する必要があります。 トークンは、<head>タグ内、<meta name="csrf-token" ...>のcontentに記載されています。これは別のRailsアプリの画像ですが、該当箇所にトークンがありますね。 トークンの上の行にある、こちらの記載は詳しく調べきれていないのですが、 'X-Requested-With': 'XMLHttpRequest', こちらの記事によると、 また、クロスドメインの場合、headerに'X-Requested-With' : 'XMLHttpRequest'を入れて送信しなければ、api側でAjaxであることが伝わらない... 引用:クロスドメインで'X-Requested-With'の設定 とのことだったので、RailsにAjaxのリクエストであることを伝えるコードなのだと今は理解しておきます。 onClickイベントの渡し方 ★2の部分です。「削除」のリンクがある箇所です。 <a href="" onClick={(e) => removeLabel(label.id, e)}>削除</a> 大したことではないのですが、Reactチュートリアルにこのような記載もあるくらい、間違えやすい箇所でしたので、備忘も兼ね記載しておきます。 onClick={() => alert('click')} と記載したときに onClick プロパティに渡しているのは関数であることに注意してください。React はクリックされるまでこの関数を実行しません。() => を書くのを忘れて onClick={alert('click')} と書くのはよくある間違いであり、こうするとコンポーネントが再レンダーされるたびにアラートが表示されてしまいます。 JSの式を渡さずに、関数を渡すように心がけます。 findIndex とか splice とか ★3の部分です。ここも難しいことはしていないのですが、私がJSに詳しくないので、勉強も兼ねての記載です。 なお、ロジックそのものはこちらの記事のものを利用させていただきました。 React + Rails API モードで基本的な CRUD アプリを作ってみた (フロントエンド編 その2) .then( res => { const targetIndex = labels.findIndex( label => { return label.id === res.data.id }) const newLabels = labels.slice(); newLabels.splice(targetIndex, 1); setLabels(newLabels) }) findIndex ... 配列.findIndex(条件)の形で、条件を満たす最初の要素のインデックス番号を返します。条件を満たすものがない場合は-1を返します。 splice ... 配列.splice(スタート位置[, 削除する数])の形で、スタート位置(インデックス番号)から指定の数の要素を取り除きます。 完成! 以上で、削除機能の実装は完了です。次回は、「1回1回フォームにリクエストを書くのは面倒なので、まとめて一括で設定する」に取り組んでみたいです。 色々やったけどうまくいかなかった。。。。 また、アップデートアクションを作ったら続きを記載します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンポーネント指向について

はじめに 私は、何かを学ぼうとするとき、ついつい基本的な概念を飛ばして、 真っ先にテクニックなどを学ぼうとしてしまいます。 ゲームの説明書は、お出かけ中とかゲームのできない状況だと 夢中で読みますが、ゲームができる時にわざわざ説明書を読みません。 読まずにゲームを始めます。 なぜかゲームをクリアした後に読んだりはしますw (今でも本棚にあるFF12の完全攻略本を見るとあの頃を思い出し胸が高鳴ります) プログラミングの学習においてもそうです。 Reactを学ぶ際、生まれた経緯、コンセプト等は全く調べず、 とりあえずUdemyとかで何か学習教材を探して勉強をはじめました。 改めて今、それを反省し、Reactの基本概念に立ち返るという意味で、 Reactの重要なコンセプトである「コンポーネント指向」について 自分なりにまとめようと思いました。 コンポーネントとは まずコンポーネントの言葉の意味を調べてみると 「構成要素、部品」 と書いてあります。 ホームページやアプリは様々なコンポーネントが集まって成り立っています。 例えばTodoアプリであれば、 ①Todoを登録するInputエリア。 ②登録したTodoを表示するエリア。 ③完了したTodoを表示するエリア。 たったこれだけのアプリでも3つのエリアに分類されていますし、 ①であればさらに細かく言うとTodoを入力する部分と確定ボタンに 分けることができます。 ②と③はよく見ると使い回しすれば簡単に作れそうですよね? そういった部分を一つのコンポーネントとして作成して、 あらゆる部分でそのまま使ったり、ごく一部を書き換えて再利用する。 そうすることで早く作ることができます。 そして何より修正があった時、元のコンポーネントを修正するだけで いいので、エラーにも強い。 こういったコンポーネントを意識することが、Reactの大前提であると 天下の公式様もトップページで仰っております。 コンポーネントの分け方 React公式より引用します。 単一責任の原則 (single responsibility principle) があり、これはすなわち、 ひとつのコンポーネントは理想的にはひとつのことだけをするべきだということです。 将来、コンポーネントが肥大化してしまった場合には、小さなコンポーネントに分割するべきです。 先ほどのTodoアプリの例を用いるならば、①で使用した青色の確定ボタン。 これは後々別の部分で使用しそうだな、と思ったら、 このボタンだけのファイルを作成します。 このファイルには、ボタンの機能しかありません。 なんと贅沢なファイルの使い方! と思った方もいるかもしれませんが、それが上記の ひとつのコンポーネントは理想的にはひとつのことだけをするべき ということなのです。 ひとつのファイルにいろんな関数などをごちゃごちゃ書いていては後々 どこに何が書いてあるかわからなくなります。 「いっぱい書きすぎて訳分からなくなりそう....」 と思ったらファイルを分けてみましょう。 Atomic Designについて 「ファイルを分けるといっても、どうやって分けるの?」 そんな時参考になる考え方がAtomic Designです! Atomic Designとは、コンポーネントを5段階に分けて 完成品を作っていく手法のことです。 1. Atom 1段階目はアトムと呼ばれる段階です。 日本語に訳すと「原子」。 つまりこれ以上分解できない最小の単位のことを指します。 またまた先ほどのTodoアプリの例で例えると、①のインプットゾーンや ボタン、②ならTodoの内容、またはチェックボタン、ゴミ箱ボタンがそうです。 1つの要素しかないファイルがAtomです。 Molecure 2段階目はモルキュールです。 先ほど作ったAtomを組み合わせて、さらに使いやすいコンポーネントを 作成していきます! Todoアプリで例えると ②のTodoの内容+チェックボタン+ゴミ箱 のAtomを足し合わせて一つのファイルを作成すれば、 どれだけTodoが増えてもそのファイルを使いまわせばいいだけなので楽ちんですね。 Organism 3段階目はオーガニズムです。 AtomやMolecureを足し合わせてファイルを作成します。 Todoアプリで言うところの①や②、③のそれぞれのエリアが当てはまります。 Templates 4段階目はテンプレートです。 Atom、Molecule、Organismを組み合わせてファイルを作成します。 Todoアプリで例えると.....あれ、ありませんね。 かなり小規模のアプリなのでそういうこともあるでしょう。うん。 ...ちゃんと例えられるアプリを例に出せよ? はい、仰るとおりです。次は頑張ります。 Pages 5段階目はページです!これが最後となります。 Atom、Molecure、Organism、Templatesを組み合わせて作成します。 Todoアプリで例えるならタイトル+①+②+③です。 Todoアプリそのものといってもいいかもしれません。 こういった具合にコンポーネントに分けて、利用できるものは再利用して 作成していきます。 注意点 ここまで長々と書いてきましたが、注意点としてはただ闇雲に全ての要素を コンポーネント化する必要はないということです。 Atomとして作成するのはあくまで何度も再利用しそうなものだけで構いません。 なんでもほどほどが肝心です。 コンポーネント化しすぎて逆に時間がかかるようではいけないので 何度も練習して感覚を磨くのが大事かなと思います。 さいごに コンポーネント化は何もReactのみに使える話ではありません。 例えばTwitterのアカウント。 作成当初は1つのアカウントに色んなことを呟くと思います。 ゲーム、漫画、アニメ、グルメ、学校、勉強、ムカつく上司への愚痴etc... しかし途中で自分のツイートを見返した時、多くの情報が混ざりすぎて 関連する内容だけ見たいのに!ともどかしくなるかもしれません。 そのため私は趣味用と学習用で2つアカウントを所有しています。 こうすれば趣味のアカウントを開くときはのほほんと、 学習用をアカウントを開く時は、身が引き締まります。 これもコンポーネント化じゃないかなと思います。 とりあえず長くなったり見づらくなったら整理! コンポーネント化とはそんな感じだと捉えてます。 以上。ありがとうございました! 参考 React公式 React初学者が必ず押さえておきたい考え方とは?【コンポーネント指向のフロントエンド】 アトミックデザインとは? コンポーネント単位で創るUIデザイン手法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】jestを使用したテスト

セットアップ・クリーンアップ処理 サンプルコード import { unmountComponentAtNode } from "react-dom"; let container = null; // セットアップ beforeEach(() => { // documentにDOM要素を描画する container = document.createElement('div'); document.body.appendChild(container); }); // クリーンアップ afterEach(() => { // documentからDOM要素を削除する unmountComponentAtNode(container); container.remove(); container = null; }); unmountComponentAtNode() DOMからマウントされたReactコンポーネントを削除する イベントハンドラや state をクリーンアップする act() react-dom/test-utils が提供する act は、 act() 内の処理がすべて完了し、DOM に反映されていることを保証する import { act } from 'react-dom/test-utils'; act(() => { // コンポーネントをレンダリングする }); // コンポーネントのレンダリングが完了していることが保証されている
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hello Worldの次はマインスイーパを作ろう(React+FastAPI)

はじめに 言語やフレームワークのサンプルやHello Worldを動かすことが出来た新人プログラマの皆さんは学習の次ステップで何をすればいいか悩むと思います。 世間一般ではブラックジャックがメジャーなようですが、 私はマインスイーパを作ることで理解を深める機雷処理言語学習法を実践しています。 この記事ではReact+FastAPIでマイスイーパを作成する流れを記載します。 Windows 10にはマインスイーパが初期インストールされていないので、新入社員や学生さんにマインスイーパを知らない層がいそうなのが怖いですが、気にしないことにします。 開発環境 前回、学習用のReact+FastAPI環境を作成したので、こちらを利用していきます。 FastAPIは新規のソースを用意するので、uvicornコマンドが動作中の場合はCtrl+Cで停止してください。 ReactはApp.jsをそのまま編集していくので、yarn startしたままでOKです。 バックエンド(Python+FastAPI) APIの仕様 以下のAPIで実装します。 API メソッド パラメータ 説明 /ms GET なし session IDの一覧取得 /ms POST width : intheight : intmine : intseed : int sessionの作成 /ms/{session} GET session : str 画面に表示する情報の取得 /ms/{session} POST session : strx : inty : int セルを開く /ms/{session}/flag POST session : strx : inty : intflag : int(1=ON,0=OFF) フラッグの設定 DELETEメソッドでsessionを削除するAPIも作成したほうがよいが、今回は不採用。 モデル セルの状態を管理するモデルクラスを実装します。 getPublicInfo()というjsonに変換可能なdictで情報を返すメソッドを用意しています。 このdictをFastAPIで返すとjson型の応答になります。 ロジックの説明は以前に作成した仕組みを参照。 ms_model.py import random import json class pointOffset: def __init__(self, x, y): self.x = x self.y = y @staticmethod def rounds(): return [pointOffset(-1, -1), pointOffset(0, -1), pointOffset(1, -1), pointOffset(-1, 0), pointOffset(1, 0), pointOffset(-1, 1), pointOffset(0, 1), pointOffset(1, 1)] class Cell: def __init__(self, isOpen, isFlag, isMine): self.isOpen = isOpen self.isFlag = isFlag self.isMine = isMine def __repr__(self): return json.dumps(self.getPublicInfo()) def getPublicInfo(self): result = { 'open': int(self.isOpen), 'flag': int(self.isFlag), } if self.isOpen: result['mine'] = int(self.isMine) return result class Field: def __init__(self, width, height, mine): self.cells = list() self.width = width self.height = height self.mine = mine for i in range(self.width * self.height): cellMine = True if i < mine else False self.cells.append(Cell(False, False, cellMine)) random.shuffle(self.cells) def __repr__(self): return json.dumps(self.getPublicInfo()) def getPublicInfo(self): rest = self.mine publicCells = list() for i, cell in enumerate(self.cells): cellInfo = cell.getPublicInfo() if cellInfo["open"] == 1 and cellInfo["mine"] == 0: cellInfo["number"] = self.roundNum( i % self.width, int(i / self.width)) if cellInfo["open"] == 0 and cellInfo["flag"] == 1 : rest = rest - 1 publicCells.append(cellInfo) status = "continue" if self.isOver(): status = "over" elif self.isClear(): status = "cleared" result = { 'cells': publicCells, 'width': self.width, 'height': self.height, 'mine': self.mine, 'status': status, 'rest' : rest } return result def cell(self, x, y): if 0 > x or 0 > y or self.width <= x or self.height <= y: return None return self.cells[y * self.width + x] def open(self, x, y): cell = self.cell(x, y) if cell is None: return if cell.isOpen: return cell.isOpen = True if not cell.isMine: if self.roundNum(x, y) == 0: # 0なら隣接cellをOpenする for offset in pointOffset.rounds(): self.open(x + offset.x, y + offset.y) def flag(self, x, y, isFlag): cell = self.cell(x, y) if cell is None: return cell.isFlag = isFlag def roundNum(self, x, y): round = 0 for offset in pointOffset.rounds(): if self.isMine(x + offset.x, y + offset.y): round += 1 return round # 指定セルが存在してmine状態の場合にTrueを返す def isMine(self, x, y): cell = self.cell(x, y) if cell is None: return False return cell.isMine def isOver(self): for y in range(self.height): for x in range(self.width): if self.cell(x, y).isOpen and self.cell(x, y).isMine: return True return False def isClear(self): closeCount = 0 for y in range(self.height): for x in range(self.width): if not self.cell(x, y).isOpen: closeCount += 1 return True if closeCount == self.mine else False モデルとAPIを繋ぐ管理クラスの実装 シングルトンで保持する動作にします。 本来はsessionをキーにしてモデルの状態をDBに格納するべきですが、今回は簡易実装にしました。 ms_manager.py from ms_model import Field import uuid import random class ms_manager: singleton_instance = None sessions = dict() def __new__(cls, *args, **kwargs): # シングルトン if cls.singleton_instance == None: cls.singleton_instance = super().__new__(cls) return cls.singleton_instance def create(self, width, hight, mine, seed_value): random.seed(seed_value) hash = str(uuid.uuid4()).replace("-","") self.sessions[hash] = { "session": hash, "status": "new", "field": Field(width, hight, mine) } return {'session': hash} def get_list(self): session_list = list() for session in self.sessions.keys(): session_list.append(session) return session_list def get_sessison(self, session): if not session in self.sessions: return {"error": "session is not found"} return self.sessions[session]["field"].getPublicInfo() def open(self, session, x, y): if not session in self.sessions: return {"error": "session is not found"} self.sessions[session]["field"].open(x, y) return self.sessions[session]["field"].getPublicInfo() def flag(self, session, x, y, flag): if not session in self.sessions: return {"error": "session is not found"} self.sessions[session]["field"].flag(x, y, bool(flag)) return self.sessions[session]["field"].getPublicInfo() pythonでシングルトンは初めて作りました。不思議な仕様ですね。 APIの実装 FastAPIで、APIの定義を行います。 APIが呼ばれたらmanagerにそのまま引数を渡します。 レスポンスもそのままmanagerの戻り値の辞書を返します。 ms_main.py from fastapi import FastAPI from ms_manager import ms_manager from starlette.middleware.cors import CORSMiddleware from pydantic import BaseModel app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"] ) class CreateParam(BaseModel): width: int hight: int mine: int seed: int class OpenParam(BaseModel): x: int y: int class FlagParam(BaseModel): x: int y: int flag: int @app.post("/ms") def ms_post_root(param : CreateParam): return ms_manager().create(param.width, param.hight, param.mine, param.seed) @app.get("/ms") def ms_get_root(): return {"session": ms_manager().get_list()} @app.get("/ms/{session}") def ms_get_session(session: str): return ms_manager().get_sessison(session) @app.post("/ms/{session}") def ms_post_session(session: str, param : OpenParam): return ms_manager().open(session, param.x, param.y) @app.post("/ms/{session}/flag") def ms_post_flag(session: str, param:FlagParam): return ms_manager().flag(session, param.x, param.y, param.flag) 以下のコマンドでFastAPIを起動してください。 uvicorn ms_main:app --reload --host 0.0.0.0 APIのテスト用のスクリプトも補足に記載しておきます。* フロントエンド(React) 実装するイベントは下表とします。 操作 説明 API リセットボタン 状態をリセットします。 POST /ms 描画 セルの状態を取得します。 GET /ms/{session} クリック セルを開きます。 POST /ms/{session} Ctrl+クリック フラッグを設定/解除します。 POST /ms/{session}/flag 各種操作でバックエンドへの要求し、取得した情報の表示を行います。 画像ファイル類はpublicフォルダに格納して参照します。* Table.js import './Table.css'; import React from "react"; import axios from "axios"; class Table extends React.Component { constructor(props) { super(props); this.state = { table : '' , message:'' }; this.base_url = "http://localhost:8000/ms"; this.session = ''; this.width = 30; this.hight = 16; this.mine = 99; this.fields = null; } componentDidMount = () => { this.updateTable(); } handleClick = () => { this.updateTable(); } updateTable = () => { axios .post(this.base_url , { "width": this.width, "hight": this.hight, "mine": this.mine, "seed" : Date.now()}) .then(res => { var data = res.data; this.get_fields(data.session); }) .catch(err =>{ console.log(err); }); } get_fields = (session) => { this.session = session; const session_url = this.base_url + "/" + this.session; axios .get(session_url) .then(res => { this.showTable(res.data); }) .catch(err =>{ console.log(err); }); } showTable = (fields) => { this.fields = fields; var items = [] for(var y = 0 ; y < fields.height ; y++){ for(var x = 0 ; x < fields.width ; x++){ var i = y * fields.width + x; var c = fields.cells[i]; var filename = ''; if(c.open === 0) if(c.flag === 1) filename = `${process.env.PUBLIC_URL}/flag.png`; else filename = `${process.env.PUBLIC_URL}/close.png`; else if(c.mine === 1) filename = `${process.env.PUBLIC_URL}/mine.png` else if(c.number) filename = `${process.env.PUBLIC_URL}/` + c.number + `.png` else filename = `${process.env.PUBLIC_URL}/open.png`; items.push(<img name={i} src={filename} onClick={(e)=>{ this.choiceDiv(e) } } />); } items.push(<br/>); } this.setState({table : (<dev>{items}</dev>)}); if(fields.status === "over") alert("over"); else if(fields.status === "cleared") alert("cleared"); var rest_label = "[Rest:" + fields.rest + "]" ; this.setState({message : rest_label}); } choiceDiv = (e) => { var num = parseInt(e.currentTarget.name); var x = num % this.width; var y = num / this.width; if( this.fields.status === "over" || this.fields.status === "cleared") { return; } if (e.ctrlKey){ if( this.fields.cells[num].open === 0){ var flag = 0; if(this.fields.cells[num].flag === 0){ flag = 1 } this.setFlag(x, y, flag) } } else{ if( this.fields.cells[num].open === 0 && this.fields.cells[num].flag === 0){ this.openCell(x,y) } } } setFlag = (x,y,flag) => { const flag_url = this.base_url + "/" + this.session + "/flag"; axios .post(flag_url , { "x": x, "y": y, flag: flag}) .then(res => { this.showTable(res.data); }) .catch(err =>{ console.log(err); }); } openCell = (x,y) =>{ const session_url = this.base_url + "/" + this.session; axios .post(session_url , { "x": x, "y": y}) .then(res => { this.showTable(res.data); }) .catch(err =>{ console.log(err); }); } render() { return ( <div> <button onClick={() => this.handleClick()}>[リセット]</button> <div>{this.state.message}</div> <div className="Table-fields">{this.state.table}</div> </div> ); } } export default Table; セルの配置がずれないようにcssを用意します。 Table.css .Table-fields { font-size: 0; } デフォルトのApp.jsから不要な物を削除し、Tableのみを表示させます。 App.js import './App.css'; import Table from "./Table"; function App() { return ( <div className="App"> <header className="App-header"> <Table /> </header> </div> ); } export default App; 動作確認 ブラウザでReactのページを表示します。 ボタンやセルをクリックしてみてください。 おわりに モデルがバックエンドにあるので、Reactは操作のイベントと表示だけの実装となりシンプルになったように感じました。 Reactでは描画更新のためにstateに設定するのが重要ということは理解しましたが、ちゃんと画面設計できるようになるには慣れが必要そう。 Reactの勉強というよりは、Web開発の練習にちょうどいい難易度だと思いますので、皆さんも自分で使用とロジックを考えて作ってみてください。 補足 アイコンの作成 使用したアイコンのPNG画像はpythonで作成しました。 生成スクリプトも置いておきます。 generate_icons.py from PIL import Image, ImageDraw, ImageFont w = 24 h = 24 lineWidth = 2 font_path = 'C:/WINDOWS/Fonts/impact.ttf' # for Windows 太目のフォント font_size = 20 font_y_offset = -2 # 計算で中央に配置してもいまいちなので微調整 OpenBgColor = (220, 220, 220) OpenLineColor = (255, 255, 255) param = [ {"filename": "1.png", "text": "1", "textColor": (0, 0, 255), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "2.png", "text": "2", "textColor": (0, 100, 0), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "3.png", "text": "3", "textColor": (255, 0, 0), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "4.png", "text": "4", "textColor": (0, 0, 100), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "5.png", "text": "5", "textColor": (100, 25, 25), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "6.png", "text": "6", "textColor": (0, 100, 100), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "7.png", "text": "7", "textColor": (0, 0, 0), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "8.png", "text": "8", "textColor": (100, 100, 100), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "open.png", "text": "", # 0の場合は数字表示しない "textColor": (0, 0, 0), "bgColor": OpenBgColor, "lineColor": OpenLineColor}, {"filename": "mine.png", "text": "", "textColor": (0, 0, 0), "bgColor": (255, 0, 0), "lineColor": OpenLineColor}, # 開く前のはLineの色が違う {"filename": "close.png", "text": "", "textColor": (0, 0, 0), "bgColor": (128, 128, 128), "lineColor": (64, 64, 64)}, {"filename": "flag.png", "text": "", # TODO 旗のデザインにしたいが単色 "textColor": (0, 0, 0), "bgColor": (0, 0, 255), "lineColor": (64, 64, 64)}, ] font = ImageFont.truetype(font_path, font_size) for item in param: image = Image.new("RGB", (w, h), item["bgColor"]) draw = ImageDraw.Draw(image) if item["text"]: text_w, text_h = draw.textsize(item["text"], font=font) draw.text(((w - text_w) / 2, (h - text_h) / 2 + font_y_offset), item["text"], item["textColor"], font=font) draw.line(((0, h-lineWidth), (w, h-lineWidth)), item["lineColor"], width=lineWidth) draw.line(((w-lineWidth, 0), (w-lineWidth, h)), item["lineColor"], width=lineWidth) image.save(item["filename"]) APIのテスト APIを動作確認するためのPythonスクリプトです。 dockerコンテナで別なターミナルを起動して実行してください。 Python環境があれば、ホストPCで実行してもよいです。 ms_client.py import requests import json import time base_url = "http://127.0.0.1:8000/ms" def printField(response): cells = response["cells"] width = response["width"] hight = response["height"] status = response["status"] print("[{}*{}]".format(width, hight)) for i, cell in enumerate(cells): if i % width == 0 and i != 0: print("") # 改行 if cell["open"] == 0 and cell["flag"] == 1: print("F", end="") elif cell["open"] == 0 and cell["flag"] == 0: print("/", end="") elif cell["open"] == 1: if "mine" in cell and cell["mine"] == 1: print("*", end="") elif "number" in cell: print("{}".format(cell["number"]), end="") print("\nstatus={}\n".format(status)) def main(): response = requests.post(base_url, json={'seed': int(time.time()), 'width': 5, 'hight': 6, 'mine': 2, }) res_create = response.json() session_id = res_create['session'] session_url = "{}/{}".format(base_url, session_id) response = requests.get(session_url) printField(response.json()) loop = True while loop: print("input [x y]") # "1 0"のようにスペースで区切ってxとyの座標を入力しEnterキー input_x, input_y = map(int, input().split()) response = requests.post(session_url, json={'x': input_x, 'y': input_y}) res_open = response.json() printField(res_open) if res_open["status"] != "continue": loop = False if __name__ == '__main__': main()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Heroku】git push heroku masterしたとき、error: failed to push some refs to 'https://git.heroku.com/herokuname-12345.git'と怒られた時の対処法

症状 git push heroku masterをしたところ、下記エラーメッセージが表示されてしまい、herokuにpushできませんでした。 gitpushherokumaster git push heroku master Counting objects: 219, done. Compressing objects: 100% (193/193), done. Writing objects: 100% (219/219), 3.96 MiB | 2.48 MiB/s, done. Total 219 (delta 70), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: remote: remote: -----> Building on the Heroku-20 stack remote: -----> Determining which buildpack to use for this app remote: -----> Ruby app detected remote: -----> Installing bundler 2.2.15 remote: -----> Removing BUNDLED WITH version in the Gemfile.lock remote: -----> Compiling Ruby/Rails remote: Command: 'set -o pipefail; curl -L --fail --retry 5 --retry-delay 1 --connect-timeout 3 --max-time 30 https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-20/ruby-2.6.3.tgz -s -o - | tar zxf - ' failed on attempt 1 of 3. remote: Command: 'set -o pipefail; curl -L --fail --retry 5 --retry-delay 1 --connect-timeout 3 --max-time 30 https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-20/ruby-2.6.3.tgz -s -o - | tar zxf - ' failed on attempt 2 of 3. remote: remote: ! remote: ! The Ruby version you are trying to install does not exist on this stack. remote: ! remote: ! You are trying to install ruby-2.6.3 on heroku-20. remote: ! remote: ! Ruby ruby-2.6.3 is present on the following stacks: remote: ! remote: ! - cedar-14 remote: ! - heroku-16 remote: ! - heroku-18 remote: ! remote: ! Heroku recommends you use the latest supported Ruby version listed here: remote: ! https://devcenter.heroku.com/articles/ruby-support#supported-runtimes remote: ! remote: ! For more information on syntax for declaring a Ruby version see: remote: ! https://devcenter.heroku.com/articles/ruby-versions remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: ! remote: ! ## Warning - The same version of this code has already been built: e4e8b0a69aeb09b76275b11e90d4096eb832212c remote: ! remote: ! We have detected that you have triggered a build from source code with version e4e8b0a69aeb09b76275b11e90d4096eb832212c remote: ! at least twice. One common cause of this behavior is attempting to deploy code from a different branch. remote: ! remote: ! If you are developing on a branch and deploying via git you must run: remote: ! remote: ! git push heroku <branchname>:main remote: ! remote: ! This article goes into details on the behavior: remote: ! https://devcenter.heroku.com/articles/duplicate-build-version remote: remote: Verifying deploy... remote: remote: ! Push rejected to murmuring-headland-65418. remote: To https://git.heroku.com/murmuring-headland-65418.git ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/murmuring-headland-65418.git' 翻訳すると、「インストールしようとしているRubyバージョンはこのスタックに存在しません。」 「エラー:一部の参照を「https://git.heroku.com/herokuname-12345.git」にプッシュできませんでした」 解決方法 Gemfileのrubyをしている箇所をコメントアウトすることで、上記エラーが表示されなくなりました。 herokuがruby2.6.3をサポートしていなかったようです。 Gemfile # ruby '2.6.3' ターミナル #bundleinstallしておく bundle install #gitにpushしておく git add -A git commit -m "changeGemfile" git push heroku master 参考 【初心者向け】RailsチュートリアルでHerokuデプロイ時に「Ruby 2.6.3はインストールできない」と言われたときの適切な対処方法 https://qiita.com/jnchito/items/c3035cc49a9cef053549
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Native + Expo CLI 環境構築 Mac編

Node.jsのインストール Node.jsのバージョン管理をするためnodebrewをインストールする brew install nodebrew インストール可能なnode.jsのバージョンを確認 nodebrew ls-remote 指定のバージョンのインストール nodebrew install (version番号) 最新バージョンのインストール nodebrew install latest ※最新バージョンインストール時に、No such file or directoryのエラーが出た。実際にディレクトリがなかったので作成し、再度インストールすることで対応。 expoのインストール npm i -g expo-cli 続いて、プロジェクト作成 expo init (プロジェクト名) 注意 インストール後に環境パスが通っていない場合、「node -v」がnode not foundと出るのでパスを通す必要がある。 echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile 実行 Xcodeのシミュレータを使うので、Xcodeがインストール済みが前提です。 expo start 以下のように表示されればOK
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React初心者から初心者に送る初歩的知識

私はバリバリのReact初心者(初めて2週間)ですが、そんな私でも簡単なタイピングゲームを作れました https://hobbypro.github.io/typing/ そんな初心者な私が、これからReactを始める人に「Reactやるならこの知識覚えといたほうがいいよ」を共有したいと思います。 React始めるなら覚えておくべき知識 心構え編 ほぼjavascript・jQueryという認識でOK Reactを勉強し始めると、やれSPAだ仮想DOMがあーだこーだと小難しい話が出てきます。 しかし、最初のうちはそんなことを覚えなくても、結構テキトーに使えます。 あまり難しいことは考えず「js使える場面ならReactも使える」くらいの認識でOKです。 環境構築編 webpackとbabelの背景と用途を知っておく 残念ながらReactを使うには、環境構築の知識も必要らしいです。 それがwebpackとbabelです。 nodeでこれらを含めたツールをインストールして、せっせとReact開発用の環境を作る必要があります。 環境構築が面倒な場合は、CDNだけでもできるらしい 自分はこれ試してないのですが、CDNでもできるらしいです。 環境構築とか面倒クセェって人は、こちらを採用しても良いのかも。 【React.js】CDNを使ってReactを手軽に試す(React 入門) | Code Database 初っ端編 まずはReact公式のチュートリアルをやってみるのがオススメ 様々な入門記事があると思いますが、Reactの使い方を簡単に知りたいなら公式チュートリアルが一番な気がします。 実際、抑えておくべき利用法をめちゃくちゃ丁寧に解説してくれてます。 また、実際に手を動かして検証できるページも用意してくれているので、頭への定着度が抜群です。 チュートリアルを始めるために環境構築をするなんていうこともありません。ブラウザひとつあればReactを体験できます。 初心者の方々はまずこのチュートリアルを行うことをオススメします チュートリアル:React の導入 – React Tips編 「state」と「props」は厚めに理解する この2つはReactのキモです。 チュートリアルをやるとわかると思うのですが、ありとあらゆる場面でstateとpropsが登場します。 なのでこれらの背景や用途はサボらずに理解すると良いです。 state とライフサイクル – React コンポーネントと props – React とりあえずメソッドはbindする example.jsx <div onClick={this.handlerClick.bind(this)}> </div> このようにメソッド?をコンポーネントなどに渡す時にはbindしておくと安全です。 逆にbindしないことで、うまく動かないということが結構おきます。 もっと詳しく知りたい方は、他の記事で調べちゃってください。 コンポーネントに関数を渡す – React key属性を理解しとく 私的にReact1番のハマりポイントです。 ちゃんと理解してないと、「stateを更新してるのに表示が全く変わらない!!」なんてことが発生します。 実は私もあまり正確に理解しておらず、ぽいkey属性を与えて乗り越えてまして... 皆さんは、エラーで時間を無駄にしないよう、ちゃんと理解しちゃってください! https://ja.reactjs.org/docs/reconciliation.html ReactでCSSを使いたいなら、一旦脳死でstyled-componentsを採用 Reactコンポーネントを装飾する方法は結構あるそうです。 Reactのコンポーネントのスタイリングをどうやるか - Qiita ただ自分が調べた感じ「styled-components」を使ってる方々が多い印象を受けました。 なので脳死でstyled-componentsを利用してみましょう。 使い方もそんなに複雑ではないです。 styled-components まとめ 以上、React初心者からReact初心者に送る初歩的知識たちでした! 他にも覚えておくといい知識はたくさんありますが、やりながら調べれば間に合うと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2021年4月最新版】爆速でGatsbyでTailwindcssを使う方法

今回の記事は最近巷で有名なGatsby.jsでTailwindcssを使用する手順をご紹介します。 ほぼ公式のドキュメント通りに進めていくのですが、いくつか変更点があるのでそれらを説明しながらやっていきます。 一応公式ドキュメントを載せておきますね。 https://www.gatsbyjs.com/docs/how-to/styling/tailwind-css/ はじめに GatsbyでTailwindcssを使用する場合、ReactもそうなのですがclassName="text-center"のようにクラス名に指定してもTailwindcssを適用することはできません。 そこで登場するのがtwin.macroです。 これを使って例えばtw="text-center"と指定するとTailwindcssを適用することが可能です。 設定も全然難しくないので肩の力を抜いてご覧ください! それでは早速説明を見ていきましょう! 必要パッケージのインストール まずは必要なパッケージをインストールします。 npm install tailwindcss autoprefixer postcss gatsby-plugin-postcss sass gatsby-plugin-sass npm install -D twin.macro @emotion/react @emotion/styled gatsby-plugin-emotion プラグインの設定 次はプログインの設定を行っていきます。 gatsby-config.jsに以下のコードをコピペしてください。 gatsby-config.js module.exports = { /* Your site config here */ plugins: [ 'gatsby-plugin-material-ui', 'gatsby-plugin-postcss', 'gatsby-plugin-emotion', { resolve: `gatsby-plugin-sass`, options: { postCssPlugins: [ require("tailwindcss"), require("./tailwind.config.js") ], }, }, ], } postcss.config.jsにてtailwindcssを追加(postcss.config.jsを作成していない方は手動で作成してください)。 postcss.config.js module.exports = () => ({ plugins: [require("tailwindcss")], }) tailwindの設定 下記のコマンドでtailwind.config.jsを作成。 npx tailwindcss init そしてgatsby-config.jsと同じ階層でgatsby-browser.jsとtailwind.cssを作成してください。 そしたら、tailwind.cssにて以下をインポート。 tailwind.css @tailwind base; @tailwind components; @tailwind utilities; tailwind.cssの読み込み。 gatsby-browser.js import "./tailwind.css"; これで準備は整いました。 あとはローカルサーバーを立ち上げ、以下を例にTailwindcssを適用させてみて下さい! import React from "react" import 'twin.macro' // これをインポートしないとtwが使えない import { Header } from "../components" export default function Home() { return ( <div> <h1 tw="text-black">Hello</h1> </div> ) } このようにしてGatsby.jsでTailwindcssを適用することができました! そこまで難しくは無かったような気がする。 他にもたくさん記事を上げているので是非ご覧下さい! Thank you for reading 参考記事 ・GatsbyにTailwind CSSを導入する最短手順(CSS-in-JS)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】useHistoryのstateから値を取得する【TypeScript】

const param = history.location.state["param"]; _人人人人人人人人人人人人人人人人人人_ > オブジェクト型は 'unknown' です。 <  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄ 概要 TypeScriptで構築した Reactのプロジェクトにて LinkまたはuseHistory.pushで設定したstateの値を 取得するためのワークアラウンド的な実装のメモ。 実装 stateに値を設定したはいいものの、Propsとかどう書けばいいの? 実装方法がわからんなー、と困っていまして。以下の様にして対処しました。 stateの値を取得するための関数は以下ように実装しました。 ポイントは一度any型に変換をかけるところです。 これでJavaScript同様にブラケットを使ったプロパティアクセスができるようになります。 stateの取得 const getState = (state: any, name: string): string => (state as any)?.[name] ?? ""; あとはリンクにstateを仕込み、必要に応じて値を取り出すだけです。 stateの設定 <Link to={{ pathname: "/", state: { param: "top" } }}>トップ</Link> stateの取得 const history = useHistory(); const param = getState(history?.location?.state, "param"); オプショナルチェイニング演算子を活用すればかなり簡潔に記述できますね。 Optional chaining (?.) - JavaScript | MDN オプショナルチェイニング演算子 ?. は、接続されたオブジェクトチェーンの深くに位置するプロパティの値を、チェーン内の各参照が正しいかどうかを明示的に確認せずに読み込むことを可能にします。 ?. 演算子の機能は . チェーン演算子と似ていますが、参照が nullish (null または undefined) の場合にエラーとなるのではなく、式が短絡され undefined が返されるところが異なります。 関数呼び出しで使用すると、与えられた関数が存在しない場合、 undefined を返します。 注意 これは手っ取り早く実装を済ませるための、その場しのぎ的な方法です。 適切なPropsの設定などがおそらくあるはず。 他に適した実装が無いか、注意深く検討してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React/FastAPI初心者が3日でCRUDする(1週間だけ頑張る Day6of9)

 はじめに 1週間N(E)ETになった社会人の学習記録です。 今日もReactのサンプルを写経し、使い方を覚えます。 前回はReadまでできたので、Create/Update/Deleteを作ります。 実装 基本、参考サイト1 2 3のコピペ 「@material-ui/grid」から「@material-ui/data-grid」に変更した。 Create 1.classの利用 TypeScriptを使ったので、classを使ってちょっと固くしてみた。 VSCodeのTypeScript変換予測は超優秀だなと思った。 User.ts interface User { username: string; email: string; password: string; is_active: boolean; is_superuser: boolean; } class User { constructor() { this.username = "new user"; this.email = "sample@example.com"; this.password = "p@ssw0rd"; this.is_active = true; this.is_superuser = false; } } Rubyの「instance_variable_set」相当の書き方がわからなかった。 (参考元みたいにJSONのまま使うべきなのだろうか) App.tsx handleInputChange( itemName: keyof User, e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement> ) { const newUser = Object.assign({}, this.state.userInput) // さすがにこれはダサすぎる switch (itemName) { case 'username': newUser.username = e.target.value; break; case 'email': newUser.email = e.target.value; break; case 'password': newUser.password = e.target.value; break; } this.setState({ userInput: newUser }); } App.js(参考元) handleInputChange(itemName, e) { const newUser = Object.assign({}, this.state.userInput) newUser[itemName] = e.target.value; this.setState({ userInput: newUser }); } 2.FastAPIの調整 FastAPIの戻り値にUser.idがなかったため、FastAPI側を修正した。 router.py # usersをidで検索して「UserSelect」をjsonにして返します。 @router.get("/users/find", response_model=UserSelect) async def users_findone(id: int, database: Database = Depends(get_connection)): query = users.select().where(users.columns.id==id) return await database.fetch_one(query) @router.post("/users/create", response_model=UserSelect) async def users_create(user: UserCreate, database: Database = Depends(get_connection)): # validatorは省略 query = users.insert() values = get_users_insert_dict(user) ret = await database.execute(query, values) # もう一回取り直すので、ここがダサい。 return await users_findone(ret, database) # return {**user.dict()} # オリジナル Delete 1.FastAPIの調整 FastAPIのdeleteは送信する側のjsonのpropertyが必要なので、passwordを抜いたものを作った。(今思えばidだけでも良い。) schema.py # update用のrequest model class UserUpdate(BaseModel): id : int username: str email: str password: str # updateにはpasswordがある is_active: bool is_superuser: bool # delete用のrequest model class UserDelete(BaseModel): id : int username: str # 今改めて考えたら不要 email: str # 今改めて考えたら不要 is_active: bool # 今改めて考えたら不要 is_superuser: bool # 今改めて考えたら不要 2.RESTのPath調整 RailsはとAPIのRESTが微妙に違うので調整。 this.axios.delete(`/users/${id}`) // for Rails this.axios.post("/users/delete/", userJson) // for FastAPI Update Modalはやめた。 所感 createやアップデートで作った分だけReact側のデータを更新するのは面白いなと思った。 App.tsx handleUserSubmit(e: { preventDefault: () => void; }) { e.preventDefault(); const userJson = JSON.stringify(this.state.userInput); this.axios.post("/users/create", userJson) .then((res: { [x: string]: any; }) => { // ここのブラウザ側の情報更新を一部だけで済ましているのは賢いなと思った。 const users = this.state.users.slice(); users.push(res["data"]); this.setState({ users: users, userInput: new User(), }); }) .catch((data: any) => { console.log(data) }); } やっぱりViewは好きじゃない。 予定より2日ほど遅れたので、このまま進むか悩み中。 免責事項(言い訳) 上記の技術については初心者なので、あてにしないように!!! https://zenn.dev/tatsurom/articles/1bf9bf6f24e14532c58b ↩ https://zenn.dev/tatsurom/articles/0b212d24c80c334d70c9 ↩ https://github.com/tatsuro-m/react_rails_api_frontend ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む