20210608のReactに関する記事は7件です。

Reactで作る手書きコンポーネント

ポイント canvasはDOMオブジェクトを直接操作する必要があるためuseRefフックで利用する canvasの大きさはCSSではなく属性で設定する。 コンポーネントは描画完了イベント(onUpdateCanvas)をコールバックする 呼び出し元で、画像のダウンロードや、タグへ画像のコピー(表示)をすることが可能 機能 手書きでお絵描きをするコンポーネントです。マウスドラッグ中に線を引きます。 propsで下記機能を利用できます 線の太さ、色を変更可能(lineHeigh, lineColor) コンポーネントのサイズを指定する(width, height) クリアプロパティ(clear: boolean)を変更すると領域をクリアします(親コンポーネントのから子コンポーネントの機能を呼び出す方法がこれしか思いつかなかった。functionコンポーネントでやり方あるんですかね?) 1本ごと線描画完了時に、onUpdateCanvas(canvas)をコールバックします。 ⇒ 引数canvasでDOM自体を引き渡すため、画像をダウンロードするなど出来ます。 コンポーネントの仕組み 手書きコンポーネントソース全体 import React, { useRef, useState, useEffect } from "react"; /** * 手書きコンポーネントprops定義 */ export type HandWritingAttribute = { width?: number, height?: number, lineWidth?: number, lineColor?: string, lineCap?: CanvasLineCap, clear?: boolean, onUpdateCanvas?: (e: HTMLCanvasElement) => void, } /** * 手書きコンポーネント(ドラッグ中線を描画する) * ・props経由で、線の太さ、色を指定 * ・onUpdateCanvas()で線描画毎にコールバック * ・props.clearの値をトグルする毎にクリア * @param props * @returns */ const HandWriting: React.FC<HandWritingAttribute> = (props) => { // canvasはDOMを直接操作するためuseRef()経由で操作する const canvas = useRef(null); // ドラッグ中判断フラグ(マウスを離すか、canvas外へ出たらfalse) const [drawing, setDrawing] = useState(false); // 領域クリア用。親コンポーネントでclearの値を変更するとcanvasをクリアする(toggle時常にクリア) useEffect(() => { const ctx = (canvas.current as HTMLCanvasElement).getContext('2d'); if( ctx ) { ctx.clearRect(0, 0, props.width, props.height); if (props.onUpdateCanvas) props.onUpdateCanvas(canvas.current); } }, [props.clear]); // 描画に必要なcontextを取得し、線の色、幅をセットする const getContext = () => { const ctx = (canvas.current as HTMLCanvasElement).getContext('2d'); ctx.lineWidth = props.lineWidth; ctx.lineCap = props.lineCap; ctx.strokeStyle = props.lineColor; return ctx; } // 線描画開始処理。beginPath()で新しいパスを開始する(開始しないと色や太さが変更できない) const mouseDown: React.MouseEventHandler = (e) => { const { offsetX: x ,offsetY: y } = e.nativeEvent; setDrawing(true); const ctx = getContext(); ctx.beginPath(); ctx.moveTo(x, y); } // マウスの動きに合わせて線を描画する const mouseMove: React.MouseEventHandler = (e) => { if (!drawing) return; const { offsetX: x ,offsetY: y } = e.nativeEvent; const ctx = getContext(); ctx.lineTo(x, y); ctx.stroke(); } // 線描画完了(canvas更新イベントコールバックを行う) const endDrawing = () => { setDrawing(false); if (props.onUpdateCanvas) props.onUpdateCanvas(canvas.current); } // canvas return ( <> <canvas ref={canvas} width={props.width} height={props.height} onMouseDown={mouseDown} onMouseMove={mouseMove} onMouseUp={endDrawing} onMouseLeave={endDrawing} /> </> ); }; // propsのデフォルト値を設定 HandWriting.defaultProps = { width: 500, height: 300, lineWidth: 10, lineColor: "rgb(100, 100, 100)", lineCap: "round", }; export default HandWriting; 利用方法 利用サンプルソース全体 import React from 'react'; import HandWriting from './HandWrinting'; function App() { const [dataUrl, setDataUrl] = React.useState(null); const [settings, setSettings] = React.useState({lineWidth:7, lineColor: '#5555bb', clear: false}) // 画像コンポーネント更新コールバック const onUpdateCanvas = (e: HTMLCanvasElement) => { // 画像をstateに保存し下記の用途で利用する // ⇒<img>タグに表示 // ⇒画像のダウンロード setDataUrl(e.toDataURL('image/png')); } // 画像ダウンロード const downloadCanvasImage = () => { const dlLink = document.createElement("a"); dlLink.href = dataUrl; dlLink.download = 'handwriting.png'; dlLink.click(); dlLink.remove(); } // 手書きコンポーネント線の幅変更 const setLineWidth = (e: React.ChangeEvent<HTMLSelectElement>) => { setSettings( prev => { return {...prev, lineWidth: parseInt(e.target.value, 10)} }); } // 手書きコンポーネント線の色変更 const setColor= (e: React.ChangeEvent<HTMLInputElement>) => { setSettings( prev => { return {...prev, lineColor: e.target.value} }); } // 手書きコンポーネントクリア const clearComponent= () => { setSettings( prev => { return {...prev, clear: !prev.clear} }); } const lineWidth = [1,2,3,5,7,10,14,20]; return ( <> <h1>HandWritingコンポーネント利用サンプル</h1> <div> <h2>HandWritingコンポーネント</h2> <div> <label htmlFor="lineWidth">線の太さ:</label> <select name="lineWidth" value={settings.lineWidth} onChange={setLineWidth}> {lineWidth.map((i) =><option key={i} value={i}>{i}</option>)} </select> &nbsp;&nbsp; <label htmlFor="lineColor">色:</label> <input name="lineColor" type="color" value={settings.lineColor} onChange={setColor}></input> &nbsp;&nbsp; <button onClick={clearComponent}>クリア</button> </div> <HandWriting onUpdateCanvas={onUpdateCanvas} {...settings} /> </div> <div> <h2>HandWritingの更新イベントサンプル(画像更新時にimgタグに反映)</h2> <img id="newImg" alt="" src={dataUrl} /> </div> <div> <button type="button" onClick={downloadCanvasImage}> HandWriting画像のダウンロード </button> </div> </> ); } export default App; 画像をimgタグに反映する const onUpdateCanvas = (e: HTMLCanvasElement) => { setSrc(e.toDataURL('image/png')); } <img id="newImg" alt="" src={dataUrl} /> 画像のダウンロード const downloadCanvasImage = () => { const dlLink = document.createElement("a"); dlLink.href = src; dlLink.download = 'handwriting.png'; dlLink.click(); dlLink.remove(); } 参考にしたサイト Reactからcanvasを使って絵を書く classコンポーネントをfunctionに書き換え、イベント処理を追加しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 公式チュートリアルを TypeScript + Hooks でリファクタリング

はじめに React を基礎から勉強する。最近の React は Hooks を用いることでクラスコンポーネントを扱わなくても開発ができるという耳障りのいい言葉を受け取った状態で React 公式チュートリアルを眺めてみると、なんとクラスコンポーネントを使っている。(チュートリアルのソースコードは数年以上更新されていないらしい。) クラスコンポーネントなんて触りたくないとチュートリアルを敬遠していたのだが、基礎が疎かで目も当てられない状態だったので割り切って一通り試してみた。たしかにわかりやすかったが、流行りの Hooks などは使用されていないので、TypeScript + Hooks という流行りについていけるようにリファクタリングを行った。 Hooks + TypeScriptでReact公式チュートリアルをリファクタ、Reactのチュートリアルをhooks + TypeScriptでモダンな仕様にリファクタしてみたを参考にした。 React + TypeScript 環境の導入 ローカルに開発環境を入れたくないので Docker コンテナで実行環境を作成する。概要はDocker 開発環境構築 (WSL2 + VSCode)を参照。VSCode が提供している Node.js & TypeScript のサンプルを元に作成した devcontainer.json および Dockerfile は以下。 devcontainer.json // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node { "name": "Node.js & TypeScript", "build": { "dockerfile": "Dockerfile", // Update 'VARIANT' to pick a Node version: 12, 14, 16 "args": { "VARIANT": "16" } }, // Set *default* container specific settings.json values on container create. "settings": { "editor.formatOnSave": true, "editor.tabSize": 2, "editor.wordWrap": "on", // "terminal.integrated.shell.linux": "/bin/bash" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "formulahendry.auto-close-tag", "formulahendry.auto-rename-tag", "mrmlnc.vscode-duplicate", "dsznajder.es7-react-js-snippets", "dbaeumer.vscode-eslint", "eamodio.gitlens", "xabikos.javascriptsnippets", "esbenp.prettier-vscode", "shardulm94.trailing-spaces", "msjsdiag.debugger-for-chrome", "visualstudioexptteam.vscodeintellicode", ], // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "yarn install", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "node" } Dockerfile ARG VARIANT="16-buster" FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 公式チュートリアルのコード準備 上記をビルドして VSCode 上で接続する。問題なく接続できたら、VSCode 上のターミナルにて、npx create-react-app my-app --typescript を実行。作成された my-app/src 内のファイルをすべて削除して新たに index.css と index.tsx を作成し、それぞれに公式チュートリアルに載っている以下コードをコピペする。 index.css body { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; } ol, ul { padding-left: 30px; } .board-row:after { clear: both; content: ""; display: table; } .status { margin-bottom: 10px; } .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; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } class Game extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => this.handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } } // ======================================== ReactDOM.render(<Game />, document.getElementById("root")); 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; } TypeScript 形式へのリファクタリング 上記の準備を終えた状態だと、index.tsx にて多くのエラーが吐かれているので順に修正していく。 typescript, @types/react まず yarn add typescript で typescript をインストールする。次に import React from "react"; で以下のようなエラーが出ているのを修正する。 Could not find a declaration file for module ‘react’. ... TypeScript に対応した React となっていないことが(たぶん)原因と思われる。エラーコードに書いてあるように npm i --save-dev @types/react を実行することで解消できる。(コンテナのビルド時にあらかじめこのあたりを解消できるように Dockerfile を修正したかったが、パッとできなかったのでわかる方教えて下さい。) リファクタリング 「はじめに」に載せたサイトを参考にリファクタリングをした。TypeScript の特徴である「静的型付け」や 「React Hooks」 の基本的な知識は、TypeScript: Documentation - Everyday Types や React.js&Next.js超入門 第2版で事前に勉強した。 以下にリファクタリング後のコードを載せた。もともと1つのファイルで構成されていたものをコンポーネントごとに分け、以下のようなディレクトリ構成になっている。 src ├── components │   ├── Board.tsx │   ├── Game.tsx │   └── Square.tsx ├── index.css ├── index.tsx └── interface.ts 各ファイルのコードは以下。(index.css は変更していないため省略。) index.tsx import ReactDOM from "react-dom"; import './index.css'; import Game from "./components/Game"; ReactDOM.render(<Game />, document.getElementById("root")); interface.ts export type ISquare = "X" | "O" | null; export type History = { squares: ISquare[]; position: number; }; components/Square.tsx import React from "react"; import {ISquare} from "../interface"; interface SquareProps { value: ISquare; onClick: () => void; } const Square: React.FC<SquareProps> = ({value, onClick}) => { return ( <button className="square" onClick={onClick}> {value} </button> ); } export default Square; components/Board.tsx import React from "react"; import {ISquare} from "../interface"; import Square from "./Square"; interface BoardProps { squares: ISquare[]; onClick: (i: number) => void; } const Board: React.FC<BoardProps> = ({squares, onClick}) => { const renderSquare = (i: number) => { return ( <Square value={squares[i]} onClick={() => onClick(i)} /> ); } return ( <div> <div className="board-row"> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> <div className="board-row"> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className="board-row"> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </div> ); } export default Board; components/Game.tsx import React, {useState} from "react"; import {ISquare, History} from "../interface"; import Board from "./Board"; const Game: React.FC = () => { const [history, setHistory] = useState<History[]>([{squares: Array(9).fill(null), position: -1}]); const [stepNumber, setStepNumber] = useState<number>(0); const [xIsNext, setXIsNext] = useState<boolean>(true); const handleClick = (i: number) => { const _history = history.slice(0, stepNumber + 1); const current = _history[_history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory(_history.concat([{squares: squares, position: i}])); setStepNumber(_history.length) setXIsNext(!xIsNext); } const jumpTo = (step: number) => { setStepNumber(step); setXIsNext((step % 2) === 0); } const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map(({squares, position}, move) => { const x = position % 3 + 1; const y = Math.floor(position / 3) + 1; const desc = move ? 'Go to move #' + move + ` (${x}, ${y})`: 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i: number) => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } const calculateWinner = (squares: Array<ISquare>) => { 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 Game; 上記で問題なく、チュートリアルアプリが動く。 おわりに TypeScript + Hooks でのリファクタリングをした。かなり参考サイトにおんぶにだっこな状態で進めたので、自身の理解を深めるべくチュートリアル末尾に記載されている改良のアイデアを試そうと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactメモ

functionComponent 文法 //定数で宣言する const App = () => { return ( <> <div>HTMLのように書けます</div> </> ) } //export忘れない export default App useState文法 // useState関数をインポート import React, {useState} from 'react' // 宣言 // state変数名, state変更関数名 = state初期値 const [isPublished, togglePublished] = useState(false); useEffect文法 // クリーンアップ関数 useEffect(() => { console.log('Render') return () => { console.log('Unmouting') } }) ・Callbackはレンダーごとに呼ばれる ・returnするCallback関数はアンマウント時に呼ばれる // マウント時のみ実行される useEffect(() => { console.log('Render') }, []) ・第二引数に空の配列を渡してあげると、最初の一回のみ実行される。 useCallback コンポーネント内で使用するコールバック関数はrender毎に生成されるが、useCallbackを使うと、コールバック関数の生成を抑止できる。 クラスコンポーネントのbind()と同じ役割をする。 文法 // 一つ目の引数にコールバック関数、二つ目の引数に配列で値を渡す(再描画する為の条件)。 useCallback(() => {},[hoge]); 子コンポーネントにpropsで関数を渡すときに使う。 this.handleClickOpen = this.handleClickOpen.bind(this); handleClickOpen = () => { this.setState({open:true}); };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】You should not use <Link> outside a <Router>の対処法

症状 react-router-domのLinkを使用としたとき、下記エラーが発生してしまいました。 翻訳すると 「 を の外で使用しないでください」でした。 error Error: Invariant failed: You should not use <Link> outside a <Router> App.jsx import React,{ Fragment } from "react"; import './App.css'; import { BrowserRouter as Router, Switch, Route, } from "react-router-dom"; import HogeIndex from "HogeIndex"; import Hoge from "Hoge"; import { Link } from "react-router-dom"; function App() { return ( <Fragment> <Link to={`/`}> <Router> <Switch> <Route exact path="/"> <HogeIndex /> </Route> } > </Route> <Route exact path="/hpge"> <Hoge /> </Route> } > </Route> </Switch> </Router> </Fragment> ); } export default App; 解決策 Linkタグを内部に入れたら、解決しました。 構成上Router外部から、ページ遷移させたい場合はhistoryを使ったりするようです。 下記参照 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ開発に手間かけすぎていませんか?React+Amplify+AppSync+TypeScript【スターターキットNo.3】高速プロトタイピングのすすめ

この記事は以下の記事を参考にさせて頂き、最新のバージョンに対応しました。 https://qiita.com/G-awa/items/a5b2cc7017b1eceeb002 https://qiita.com/otanu/items/2c522a652e5843a5e2c5 モダンフロントエンド技術の重要性 本チュートリアルはモダンフロントエンド技術概論を語る一貫として用意しました。 (これについての記事も後日ちゃんと書こうと思います。) 高速プロトタイピング アイデアを最速でカタチにするために高速プロトタイピングのスキルセットが必要です。 そのためには、効率的なアーキテクチャと、テンプレートが必要です。 今回のチュートリアルではテンプレートを立ち上げ、デプロイする手順について説明しています。 高速プロトタイピングのベースコードとして利用してみてください。 Githubはこちら https://github.com/makotunes/starter-kit-react-gq-amplify 今からたった数コマンドで、アイデアを最速でカタチにする、上記のアプリをデプロイします。 コマンドをcreate-react-appとamplifyを入れておきます。 npm i -g create-react-app npm install -g @aws-amplify/cli 筆者環境 $ create-react-app --version 4.0.3 $ node -v v16.1.0 $ npm -v 7.11.2 $ amplify --version 4.49.0 まずは雛形を作っていきます。 create-react-app speedy-app-starter-kit --template typescript cd speedy-app-starter-kit AWSにおけるGraphQLのマネージド・サービスがAppSyncという位置づけです。 そしてマネージド・サービスを容易にデプロイするためのCI/CDサービスとして、Amplifyが使用できます。 構成を対話形式のコマンドで設定していきます。AWSのクレデンシャル等お好みの設定で。 amplify init ? Enter a name for the project (speedyappstarterkit) ? Initialize the project with the above configuration? (Y/n) ? Select the authentication method you want to use: (Use arrow keys) ? Please choose the profile you want to use (Use arrow keys) この時点でamplifyには空のアプリがデプロイされます。 ここでGraphQLのAPIを設定します。 amplify add api ? Please select from one of the below mentioned services: (Use arrow keys) > GraphQL ? Provide API name: > speedyappstarterkit ? Choose the default authorization type for the API (Use arrow keys) > API key ? Enter a description for the API key: > test ? After how many days from now the API key should expire (1-365): > 90 ? Do you want to configure advanced settings for the GraphQL API > No, I am done. ? Do you have an annotated GraphQL schema? No > No ? Choose a schema template: (Use arrow keys) > Single object with fields (e.g., “Todo” with ID, name, description) ? Do you want to edit the schema now? > No 以下のファイルにデータ型の定義が記述されたGraphQLスキーマファイルが作成されます。 今回はこれをこのまま使っていきます。 amplify/backend/api/speedyappstarterkit/schema.graphql type Todo @model { id: ID! name: String! description: String } GraphQLはサービスにスキーマを登録する必要がありますが、 ユーザーが設定する情報はミニマムでこれだけです。 amplifyのCLIでこのデータ型定義に応じて、 AppSyncに与えるスキーマと フロントエンドに必要な基本的なクエリパターンを自動で生成してくれます。 amplify push ? Are you sure you want to continue? (Y/n) > Y ? Do you want to generate code for your newly created GraphQL API (Y/n) > Y ? Choose the code generation language target > typescript ? Enter the file name pattern of graphql queries, mutations and subscriptions (src/graphql/**/*.ts) > src/graphql/**/*.ts ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions (Y/n) > Y ? Enter maximum statement depth [increase from default if your schema is deeply nested] (2) > 2 ? Enter the file name for the generated code (src/API.ts) > src/API.ts 最低限必要なモジュールをインストールしておきます。 yarn add aws-amplify # yarn add aws-amplify-react yarn add react-router-dom yarn add @types/react-router-dom エンジニアでもイケてるデザインを作れるように material-uiを導入しておきます。 yarn add @material-ui/core @material-ui/icons @material-ui/data-grid src/App.tsx import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import SimpleTodoPage from "./components/pages/SimpleTodoPage"; const App: React.FC = () => { return ( <Router> <Switch> <Route path="/" component={SimpleTodoPage} exact /> </Switch> </Router> ); }; export default App; src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import Amplify from "aws-amplify" // 追加 import config from "./aws-exports" // 追加 Amplify.configure(config) // 追加 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); src/components/pages/SimpleTodoPage.tsx import GenericTemplate from "../templates/GenericTemplate"; import { makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableContainer from "@material-ui/core/TableContainer"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import { ThemeProvider } from '@material-ui/styles'; import { createMuiTheme } from '@material-ui/core/styles'; import Box from "@material-ui/core/Box"; import TextField from "@material-ui/core/TextField"; import Button from "@material-ui/core/Button"; import React, { useEffect, useState } from "react"; import { API, graphqlOperation } from "aws-amplify"; import { listTodos } from "../../graphql/queries"; import { createTodo } from "../../graphql/mutations"; import { onCreateTodo } from "../../graphql/subscriptions"; import { ListTodosQuery, OnCreateTodoSubscription, CreateTodoMutationVariables } from "../../API"; const theme = createMuiTheme({ palette: { primary: { main: '#00838f' }, secondary: { main: '#e0f7fa' }, }, }); type Todo = { id: string; name: string; description?: string | null | undefined; createdAt: string; updatedAt: string; }; type FormState = { name: string; description: string; }; type TodoSubscriptionEvent = { value: { data: OnCreateTodoSubscription } }; const useTodos = () => { const [todos, setTodos] = useState<Todo[]>([]); useEffect(() => { (async () => { // 最初のTodo一覧取得 const result = await API.graphql(graphqlOperation(listTodos)); if ("data" in result && result.data) { const todos = result.data as ListTodosQuery; if (todos.listTodos) { setTodos(todos.listTodos.items as Todo[]); } } // Todo追加イベントの購読 const client = API.graphql(graphqlOperation(onCreateTodo)); if ("subscribe" in client) { client.subscribe({ next: ({ value: { data } }: TodoSubscriptionEvent) => { if (data.onCreateTodo) { const todo: Todo = data.onCreateTodo; setTodos(prev => [...prev, todo]); } } }); } })(); }, []); return todos; }; const useStyles = makeStyles({ table: { minWidth: 650, }, }); const ProductPage: React.FC = () => { const classes = useStyles(); const [input, setInput] = useState<FormState>({ name: "", description: "" }); const todos = useTodos(); const onFormChange = ({ target: { name, value } }: React.ChangeEvent<HTMLInputElement>) => { setInput(prev => ({ ...prev, [name]: value })); }; const onTodo = () => { if (input.name === "" || input.description === "") return; const newTodo: CreateTodoMutationVariables = { input: { name: input.name, description: input.description } }; setInput({ name: "", description: "" }); API.graphql(graphqlOperation(createTodo, newTodo)); }; return ( <GenericTemplate title="TODO簡易版"> <ThemeProvider theme={theme}> <Box p={2} bgcolor="primary.main" color="primary.contrastText"> 新規登録   </Box> <Box p={2} bgcolor="secondary.main" color="primary.main"> <form action="/users" acceptCharset="UTF-8" method="post"> <div><TextField id="name" type="text" name="name" label="名前" style={{ width: 500 }} value={input.name} onChange={onFormChange} /></div> <div><TextField id="description" type="text" name="description" label="詳細" style={{ width: 500 }} value={input.description} onChange={onFormChange} /></div> <Button variant="contained" color="primary" name="commit" onClick={onTodo}>登録</Button> </form> </Box> </ThemeProvider> <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> <TableCell>名前</TableCell> <TableCell>詳細</TableCell> </TableRow> </TableHead> <TableBody> {todos.map((row) => ( <TableRow key={row.id}> <TableCell component="th" scope="row"> {row.name} </TableCell> <TableCell>{row.description}</TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> </GenericTemplate> ); }; export default ProductPage; Webサービスを実行して操作を確認します。 ポートの競合がない限りhttp://localhost:3000/ でアクセス可能です。 yarn start 今回は簡易版のコードをのせていますが、 完全版もGithubで利用できます。 https://github.com/makotunes/starter-kit-react-gq-amplify 完全版では、閲覧追加に加え、削除と変更をサポートしてます。 フロントエンド概論についてのスライドも用意しています。 https://speakerdeck.com/makotunes/modanhurontoendoji-shu-gai-lun
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【apollo client】mutation後にキャッシュを書き換える

はじめに 最近apolloの学習をしています。 apolloにはキャッシュの機能があり、これによって リストにデータを追加/削除した時にリストが更新されない問題が起きる事があります。 本記事では、mutation後にキャッシュを書き換える方法について書いていきます。 キャッシュを書き換える2つの方法 refetchを使う 自分で直接書き換える 他にもあるかも知れませんが、この記事では初学者の自分が知ってる方法2つを挙げます 「useGetListQuery()」などcodegenで生成したhooksを使っていますが、 useQuery(GET_LIST)などと同じです。 refetchを使う List.tsx const { data, refetch } = useGetListQuery(); hooksのresultとしてrefetchという関数を受け取る事ができます。 このrefetchを実行することで、もう一度同じリクエストをする事ができます。 (今回ならuseGetListQuery()を実行するのと同じ) もう一度リクエストするので、data/キャッシュが更新されます。 アイテムを追加する処理の後で、refetchすることでキャッシュを更新する事ができます。 やり方① Item.tsx await addItem() // リストにアイテムを追加するmutation refetch() ※どちらも非同期処理なのでawaitをつけないと、mutationが終わる前にrefetchすることになってしまいます。 やり方② onCompletedを使います。 Item.tsx const [addItem] = useAddItemMutation({ onCompleted() { props.refetch(); }, }) (propsとしてrefetchを親から渡してきています。) addItemが完了したタイミングで実行される様です。 しかし、自分がやったところ、できる時とできない時がありました。(謎い。。。) 自分で直接書き換える キャッシュを自分で直接書き換えることもできます。 データを追加/削除したけどキャッシュが変わっていないという事は、 サーバーとクライアントのデータが違う状況です。 追加や削除したデータを使って、キャッシュに変更を加えて同じ状況にします。 (キャッシュの変更はサーバ側に影響しません。) リストにデータを追加したい時 Item.tsx const [addItem] = useAddItemMutation({ update(cache, data) { const newData = data.data?.addItem; cache.modify({ fields: { getList(existing = []) { const newItemRef = cache.writeFragment({ data: newData, fragment: gql` fragment AddItem on ItemModel { id name } `, }); return [...existing, newItemRef]; }, }, }); }, }); updateオプションを使います。 cacheにはキャッシュの情報が入ります。 dataには、addItemを実行したデータが入ります。 (任意の名前をつける事ができます。) 一度console.logでcacheやdataを見てみることをオススメします。 existingには、getListの配列が入ります。 ここには、参照が入っています (existingにも任意の名前をつける事ができます。) 追加したアイテムの参照を作りexistingと一緒にreturnしています returnの値がgetListのキャッシュになります。 Item.tsx return [...existing, newData]; 元のキャッシュのデータにaddItemのデータを追加することで、キャッシュに追加する事に成功しました。 リストからデータを消したい時 Item.tsx const [deleteItem] = useDeleteItemMutation({ update(cache, data) { const newData = data.data?.addItem; cache.modify({ fields: { getList(existing = [], { readField }) { const newItemList = existing.filter((item: any) => { return readField("id", item) !== data.data?.deleteItem?.id; }); return [...newGroupList]; }, }, }); }, }); データの追加とほとんど同じです。 データの削除では、削除したアイテムと同じidを持つデータをキャッシュのリストから削除しています。 readFieldを使うことで、キャッシュされてるアイテムのフィールドにアクセスできます。 filterを使って順番に確認をしていき、同じidを持たないListを新しく作成し、それをreturnしています。 これで、キャッシュの更新をする事ができました。 もし間違っている所、おかしな所があれば教えていただけると幸いです。 参考文献 https://www.apollographql.com/docs/react/caching/cache-configuration/ https://www.apollographql.com/docs/react/data/mutations/ https://www.apollographql.com/docs/react/data/queries/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでテトリスをつくる⑨_バグ修正

テトラミノが画面にめり込んだり、ブロック同士が重なるバグが発生 原因 ブロックが回転したときに、縦と横の幅が変わるので、そのまま描画すると他のセルと重なっている場合があるため 対処 ブロックが回転した時に、ぶつかり判定を繰り返し、X位置Y位置を補正する。 tetris.js const [X, setX] = useState(4) const [Y, setY] = useState(0) ...途中略 switch(e.which){ //Enter case 13: minoT.rotate() if(checkCrush(base, minoT.val(X, Y))) { if(!checkCrush(base, minoT.val(X+1, Y))){ setX(X+1) break; } if(!checkCrush(base, minoT.val(X-1, Y))){ setX(X-1) break } if(!checkCrush(base, minoT.val(X, Y-1))){ setX(Y-1) break } if(!checkCrush(base, minoT.val(X+2, Y))){ setX(X+2) break } if(!checkCrush(base, minoT.val(X-2, Y))){ setX(X-2) break } if(!checkCrush(base, minoT.val(X, Y-2))){ setX(Y-2) break } } setDisp(minoT.val(X, Y)) break; ...以下略 これでバグは解消した
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む