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

Reactの型定義

Reactの基礎とTypeScriptの基礎をある程度学んだが、Reactのどのような場面でTypeScriptを使うのかがいまいちよくわからないので、そこら辺を理解するために書き殴ります。 React.VFC ファンクションコンポーネントを定義するための型。以下のように記述する const App: React.VFC = () => { return( <div></div> ) } export default App; 「この関数はReactのファンクションコンポーネントですよ」 みたいな意味。 propsの型定義 propsの型定義は、React.VFCの型にジェネリクスを指定する形で定義する。 //propsの型 type Props = { text: string } export const Child: React.VFC<Props> = (props) => { return( <div>{props.text}</div> ) } //親のファイル import { Child } from "./Child"; const App: React.VFC = () => { return ( <div> <Child text="子供に渡すテキスト"/> </div> ); } export default App 「この関数はReactのファンクションコンポーネントで、propsとしてstring型の値を受け取りますよ」 みたいな意味。 上の例で言えばComponentのpropsにstring型以外の値を受け取るとエラーが出る。 export const Child: React.VFC<{text: string}> = (props) => { return( <div>{props.text}</div> ) } そんなに型が多くない時はこんな感じでもかけるらしい。 useStateの型定義 useStateの型定義はuseStateを宣言する時に型を指定する。 useStateには初期値を入れると型推論が効くので、初期値から方が明確にわかる時は無理にアノテーションをする必要はなく import { useState } from 'react' type UserData = { id: number; name: string; } export const StateSample: React.VFC = () => { //初期値から型が明確なので型推論に任せる const [count, setCount] = useState(0) //stateをnullでリセットする必要がある時はnullでもとれるようジェネリクスを用いて型を指定する const [count2, setCount2] = useState<number | null>(0); /*stateにオブジェクトを持たせる場合は、 予め型エイリアスなどで型を定義しておき、その型をジェネリクスで指定する 初期値はその型の構造を満たしておく必要がある*/ const [user, setUser] = useState<UserData>({ id: 1111, name: "aiko" }); return( <div> <h1>{count}</h1> <h1>{user.name}</h1> </div> ) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのCSS記述の各選択肢をShadowDOM + CSS modules(web標準)の時代への移行コストから考える

はじめに ReactのCSSどれで書けばいいの問題をIE11のサポート終了もあって今後一気に普及と各JSライブラリーへの組み込みが進むであろうWeb標準のShadowDOMによるスコープ付きCSSへの移行コストという観点から考えてみます。 なお、ShadowDOMのザックリした概要については手前味噌になりますが、凄いよShadow DOM! Web標準によるスコープ付きCSSのあるコンポーネント制作術という記事を事前にお読みいただくと、手早く把握していただけますと思います。 めちゃくちゃざっくりいうと、 CSS modules(web標準)…import styles from "styles.css"; みたいな形でES Modulesのようにjs内でcssファイルを読み込めるようになる組み込み予定の標準技術(ステータスがintent to shipとなっておりもうすぐブラウザに実装予定?) Shadow DOM…DOMのカプセル化(完全に切り離されたCSSスコープ付き)を実現するweb標準のコンポーネント技術 Custom Elements…<news-card></news-card>みたいなセマンティックなオリジナルのタグを定義する標準技術 これらはまとめてWeb Componentsとして仕様策定されブラウザのAPI群に実装されています。 今回、比較するのは、CSS modules(css-loader)、style-components、Tailwind CSSの3つになります。 これらの選択肢にはそれぞれ異なるメリットがあると考えられるため、今回の記事では、『どれがベストか』といった判断は差し控えさせていただきたいと思います。 CSS modules(css-loader)の場合 最初に、注意事項としてCSS modules(css-loader)とはwebpackとcss-loaderなどのプラグインを通して、コンパイル時にクラス名にハッシュをつけてスコープ安全性を確保する技術で、CSS modules(web標準)とは異なる技術になります。next.jsなどのフレームワークでbuilt-inでサポートされています。 CSS modules(css-loader)で実装されたreactコンポーネントのスタイルをshadowDOMのスコープ付きcssに移行するコストは‥‥おそらく0だと思われます。 というのも、現在webpackとcss-loaderによるハッシュ化によって実現されているスコープがweb標準のshadowDOMのスコープに変わるだけで同様のcssファイルをそのままインポートできるからです。 shadowDOM.js import styles from './some.module.css'; class SomeShadowElement extends HTMLElement { constructor() { super(); let shadowRoot = this.attachShadow({ mode: "closed" }); this.shadowRoot.adoptedStyleSheets = [styles]; } } 最終的には↑のようにshadowDOM内で読み込まれる形になりますが、next.jsなどのフレームワークを使っていた場合は実装者は内部の事情を気にせず、コンポーネントに shadow=true みたいなオプションを付与するだけであとはよしなにやってくれそうな気がします。 styled-compoentsの場合 styled-componentsもweb標準への移行はスムーズに進みやすいと思われます。 styled-components.jsx const OriginalButton = styled.a` /* This renders the buttons above... Edit me! */ display: inline-block; border-radius: 3px; padding: 0.5rem 0; margin: 0.5rem 1rem; width: 11rem; background: transparent; color: white; border: 2px solid white; /* The GitHub button is a primary button * edit this to target it specifically! */ ${props => props.primary && css` background: white; color: black; `} ` render( <div> <OriginalButton href="https://github.com/styled-components/styled-components" target="_blank" rel="noopener" primary > GitHub </OriginalButton> </div> ) 公式の例から拝借したものを少しだけイジっただけの↑の例のような場合ですと、 ${props => props.primary && css` background: white; color: black; `} のように、propsの値によってcssの出力を動的に変えているような部分に関しては、クラス名の付け替えによってcssを変化させるオーセンティックな方法に書き換える必要が出てきそうです。 それ以外は、ほぼ移行コストはかからないはずです。 styled-componentsの利点としては、<OriginalButton>のようなCustom Elements的なタグ名をそのままWeb ComponentsのCustome Elementにうまいこと移行できそうなところです。 jsを利用して動的にcssを出力するような処理さえ書かなければ、一番そのまま次世代のweb標準のベストプラクティス形に移行できそうなのはstyled-componentsかもしれません。 ただし、ランタイムのパフォーマンス性能でcss-modules(css-loader)には劣りますので、そこらへんの選択は案件の要件によって変わってくるかもしれません。メディア系サイトなどパフォーマンスがクリティカルになる案件ではcss-modules(css-loader)がベストかもしれません。 TailwindCSSの場合 TailwindCSSでは、cssはフレームワークによって定義されたユーティリティクラスをクラス名を通して各要素に適用するので、問題は、カプセル化されたShadowDOM内に必要なcssだけをpurgeしてどうインジェクトするかというbuildツールのところの問題になります。 TailwindCSSをshadowDOMで使うとなると、かなりbuildが重たくなりそうな気はしますが、そこらへんは技術の進歩によって解決しそうな気はします。 よってTailwindCSSで実装されたcssをshadowDOMに移行する際の実装者のコストも将来的にはほぼ0になると思われます。 ただ概念的なレベルでいうと、ユーティリティファーストというグローバル定義されたcssクラスをレゴのようにあてこんでいってスタイルをつくるTailwindCSSは、カプセル化されたcss空間をつくるshadowDOMという概念とはそもそもかけ離れたものであるような気もしますが‥。 結論? 今回、CSS modules(css-loader)とstyled-componentsそしてTailwindCSSの3つで書かれたcssを次世代のweb componentsの仕様に移行する際に必要になるかもしれない移行コストを考えてみました。 ペインポイントになりそうなのは、jsによって動的にcssを生成し、あてこんでいる部分ということになります。 よってcss-in-jsライブラリーなどを用いてcssを記述する際、propsの値を見て動的にcssを書き換えるというような処理は出来るだけ控えて、propsの値によってクラス名を差し替えて当てるcssを切り替えるというようなオーセンティックな方法で実装した方が良さそうです。 結論 css-modulesはほとんど何も考えなくても、そのまま移行できそう。 styled-componentsは動的にjsでcssを切り替える処理を書かなければ、一番、web components時代のベストプラクティスに近づける。 TailwindCSSはビルドツールさえうまいことやってくれれば、特に問題なさそう。(あとM2チップなどの進化にも期待) という感じに、私は今、考えました。 間違えているところや理解の浅いところなどがあるかもしれませんので、なにか気になるところがありましたら、是非、コメントやtwitterの方へご意見いただけますと幸いです。 最後までお読みいただきありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[TypeScript] ReturnTypeで関数の戻り値の型を取得する。

Formikの型を付けたかったのですが、 useFormikの戻り値の型が見当たらなかったので、 関数の戻り値から型をつけるようにしました。 ReturnTypeを使ってこんな感じで取得できます。 ReturnType<typeof useFormik> 公式だと以下のページに記載がありました。 TypeScript | Utility Types Utility Typesには他にも便利そうな型が多いですね。どんどん使っていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Setting a timer for a long period of time, i.e. multiple minutes がうざい

無視しちゃおう import { LogBox } from 'react-native'; LogBox.ignoreLogs(['Setting a timer']); Setting a timer for a long period of time, i.e. multiple minutes, is a performance and correctness issue on Android as it keeps the timer module awake, and timers can only be called when the app is in the foreground. See https://github.com/facebook/react-native/issues/12981 for more info
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + Electron製の絵コンテエディタアプリ「Mizutama Conte」を支える技術

リポジトリ Web版 絵コンテはアニメ制作における設計図です。カット割り、セリフ、トランジション、カメラワーク等、アニメ制作を進める上で必要な情報がぎっしり詰まっています。従来(そして多くの場合現在も)絵コンテは紙とペンで作られてきました。 アニメ制作では各工程において、タイムシート、脚本、字幕、レイアウト等、様々な形式のデータを使用しますが、これらのデータは全て絵コンテを元に作られます。何度も再利用されるデータの管理は、デジタルの最も得意とすることろです。そこで、スタジオみずたまではアニメ制作の経験を生かし、デジタル絵コンテエディタアプリを開発することにしました。 先行アプリ Storyboard Pro 新海誠さんも使ってるプロプライエタリアプリです。あまりにも高額で自主制作への導入はハードルが高いです。 Storyboarder 無料で使える海外のOSSです。Electron製です。高機能ですが、カメラワークやトランジションを設定できない等、痒いところに手が届かない部分があります。 e-Conte Board iPadアプリです。日本の映像制作において必要十分な機能が揃っていますが、iPad専用のため、データの共有が難しい部分があります。 Griffith Webベースのアプリです。一般公開はされていないようです。 Mizutama Conteで実現したいこと いつでもどこでもプレビューできる 絵コンテはいろんな工程でいろんな立場の人が参照します。専用ソフトがなくてもブラウザだけでプレビューできるよう、jsonファイルとpsdファイルで絵コンテファイルを設計しました。 作画は使い慣れた外部アプリで CLIP STUDIO PAINTやPhotoshop等、使い慣れたペイントアプリは人それぞれです。誰もが満足する描き心地を実装することは不可能であり、また、絵コンテ制作のためだけに普段と異なる環境で作画することはストレスになり得ます。そこで、このアプリでは敢えてペイント機能は実装せず、作画はPSD形式に対応した好みの外部アプリを連携させる方針にしました。 バージョン管理 コアとなる情報をjsonファイルに記述することで、Gitによるバージョン管理が可能になります。 開発を支える技術の紹介 React Spectrum Adobeのデザインシステム「Spectrum」のReactコンポーネントです。 基本的に既存のコンポーネントを使いましたが、足りない部分は独自にコンポーネントを作っています。 例1:アプリフレーム const BackGround = styled.div` width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0; background-color: var(--spectrum-alias-appframe-border-color); `; const ToolArea = styled.div<{ gridArea: string }>` background-color: var(--spectrum-alias-toolbar-background-color); grid-area: ${({ gridArea }) => gridArea}; height: 100%; overflow: hidden; `; const GlobalGrid: React.FC = ({ children }) => ( <Provider theme={defaultTheme}> <GlobalStyle /> <BackGround> <Grid areas={['header header header', 'toolbar content sidebar']} columns={['size-600', 'auto', 'size-3600']} rows={['size-500', 'auto']} height="100vh" gap="size-25" > {children} </Grid> </BackGround> </Provider> ); 例2:アコーディオンメニュー const LabelHover = styled.div` width: 100%; margin: var(--spectrum-global-dimension-size-50, var(--spectrum-alias-size-50)) 0; padding: var(--spectrum-global-dimension-size-50, var(--spectrum-alias-size-50)) 0; :hover { background-color: var(--spectrum-alias-highlight-hover); border-radius: var(--spectrum-global-dimension-size-50, var(--spectrum-alias-size-50)); } `; const Toggle = styled.input` display: none; `; const Ul = styled.ul` padding-left: var(--spectrum-global-dimension-size-300, var(--spectrum-alias-size-300)); margin: 0; `; const Label: React.FC<{ labelFor: string }> = ({ labelFor, children }) => ( <LabelHover> <label htmlFor={labelFor}>{children}</label> </LabelHover> ); export const Accordion: React.FC<{ labelName: string }> = ({ labelName, children }) => { const [toggle, setToggle] = useState(false); return ( <> <Toggle type="checkbox" id={labelName} checked={toggle} onClick={() => setToggle(!toggle)} /> <Label labelFor={labelName}> <Flex direction="row" gap="size-100" alignItems="center"> {toggle ? <ChevronDown size="S" /> : <ChevronRight size="S" />} <Text>{labelName}</Text> </Flex> </Label> <Ul style={{ display: `${toggle ? 'block' : 'none'}` }}>{children}</Ul> </> ); }; ag-psd PSDファイルを読むためのJavaScriptライブラリです。Psd型のオブジェクトとしてPSDファイルを扱うことができます。 { "width": 300, "height": 200, "channels": 3, "bitsPerChannel": 8, "colorMode": 3, "children": [ { "top": 0, "left": 0, "bottom": 200, "right": 300, "blendMode": "normal", "opacity": 1, "transparencyProtected": false, "hidden": true, "clipping": false, "name": "Layer 0", "canvas": [Canvas] }, { "top": 0, "left": 0, "bottom": 0, "right": 0, "blendMode": "multiply", "opacity": 1, "transparencyProtected": true, "hidden": false, "clipping": false, "name": "Layer 3", "canvas": [Canvas] } ], "canvas": [Canvas] } canvas部分はHTML5のHTMLCanvasElementです。ReactのJSXにはそのまま書けないため、toDataURLしてからimg要素の中で表示させます。 <View gridArea="picture" width="100%" height="auto"> {cut.picture?.children ?.filter((child: Psd['children'], layerindex: number) => layerindex !== 0) .map((child: Layer) => { const src = child.canvas?.toDataURL('image/png', 0.4); return ( <div style={{ height: `${child.canvas && child.canvas.height * 0.12}px`, width: `${child.canvas && child.canvas.width * 0.12}px`, backgroundColor: '#fff', position: 'relative', }} > <img style={{ transform: 'scale(0.12)', transformOrigin: 'left top' }} src={src} alt="cut" /> ); })} </View> ReactN useGlobalでグローバル状態管理を可能にするエクステンションです。使い方はuseStateとほぼ同じです。 index.tsx import React, { setGlobal } from 'reactn'; import ReactDOM from 'react-dom'; import App from 'App'; import reportWebVitals from 'reportWebVitals'; import { Psd } from 'ag-psd'; const prtPsd: Psd = { width: 1, height: 1 }; const prtCut: Cut = { picture: prtPsd, }; setGlobal({ mode: 'Edit', tool: new Set(['Select']), cut: prtCut, globalCuts: [prtCut], globalPsds: [prtPsd], globalFileName: '', }); index.tsxでsetGlobalを宣言します。 global.d.ts import 'reactn'; declare module 'reactn/default' { export interface State { mode: string; tool: Set<string> | undefined; cut: Cut; globalCuts: Cut[]; globalPsds: Psd[]; globalFileName: string; } } global.d.tsで型情報を記述します。 const CutContainer: React.FC = () => { const prtPsd: Psd = { width: 1, height: 1 }; const prtCut: Cut = { picture: prtPsd, }; const [cuts, setCuts] = useState([prtCut]); const globalCuts = useGlobal('globalCuts')[0]; const globalPsds = useGlobal('globalPsds')[0]; //略 } あとはuseStateのようにコンポーネント内で記述するだけです。 react-hotkeys-hook useHotKeysでキーボードショートカットを簡単に実装できます。 const [selected, setSelected] = useGlobal('mode'); useHotkeys('e', () => { setSelected('Edit'); }); useHotkeys('p', () => { setSelected('Preview'); }); react-typescript-electron-sample-with-create-react-app-and-electron-builder 今回のアプリの雛形として使用しました。 メイン・レンダラーいずれのプロセスもホットリロード対応で、最高のDXです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで<input type="file" webkitdirectory />が動かない時の対処法

<input type="file" webkitdirectory />は、Chromium系ブラウザでディレクトリごとファイルを選択するタグですが、Reactで使うのに一工夫必要だったのでメモします。 普通にそのまま書いた場合 <input type="file" webkitdirectory /> Type '{ type: string; webkitdirectory: true; }' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'. Property 'webkitdirectory' does not exist on type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'. does not existって怒られてしまいます。 解決策 <input type="file" directory="" webkitdirectory="" /> directory="" webkitdirectory=""と書くと動くようになります。 TypeScriptを使っている場合 <input type="file" /* @ts-expect-error */ directory="" webkitdirectory="" /> /* @ts-expect-error */で強制的に黙らせます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Unterminated regular expressionの対処法 【Javascript】

症状 reactでSPAアプリ開発中に以下のエラーが発生しました。 翻訳すると、「終了していない正規表現」でした。 error Unterminated regular expression import {homeURL,userCreateURL,signInURL} from "../urls/index"; import {useHistory} from "react-router-dom"; export const Hoge = () => { const history = useHistory(); function signUpHandle() { history.push(userCreateURL) } function signInHandle() { history.push(signInURL) } return( <Button color="inherit" onClick={() => signUpHandle()} btnLabel={新規登録}></Button> <Button color="inherit" onClick={() => signInHandle()} btnLabel={サインイン>サインイン</Button> ) } 解決方法 サインインのbtrLabelの{}の閉じかっこを追加したら、解決されました。 このエラーは、「文字列リテラルは単一引用符 (') または二重引用符 (") で囲む必要」があり、btnLabelの{}が閉じられていなかったため、後半のサインインまでもbtnlabelとして認識されていたようです。(なお、2重でサインインを書く必要はそもそもないです) import {homeURL,userCreateURL,signInURL} from "../urls/index"; import {useHistory} from "react-router-dom"; export const Hoge = () => { const history = useHistory(); function signUpHandle() { history.push(userCreateURL) } function signInHandle() { history.push(signInURL) } return( <Button color="inherit" onClick={() => signUpHandle()} btnLabel={新規登録}></Button> <Button color="inherit" onClick={() => signInHandle()} btnLabel={サインイン}>サインイン</Button> ) } 症状
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactステート管理ライブラリ Recoil入門(typescript)(UseContextの比較も)

Recoilとは RecoilはReact向けのステート管理ライブラリです。 Reactを開発したFacebook製のライブラリです。 Reactのステート管理ライブラリといったらReduxですが、 プロジェクト途中で導入する場合、非常にコストがかかります。 一方、RecoilはReact Hooksライクにコーディングが行えるため、学習コストも低く、 コード量も非常に軽量なので、導入が容易に行えるのが特徴です。 やること Recoilを用いて、メモ管理Webアプリを作ります。 また、類似ライブラリであるUseContextとの比較も行います。 前提条件 環境情報は以下になります。 端末環境 OS チップ Mac Big Sur Apple M1 各種バージョン % node -v v14.17.0 % yarn -v 1.22.4 % npx create-react-app --version 4.0.3 1. メモ管理Webアプリ作成 シンプルにメモの表示、追加、変更が行えるWebアプリになります。 プロジェクト作成 create-react-appにてプロジェクト作成を行います。 今回はtypescriptで行います。 % npx create-react-app react-recoil-demo --template typescript % cd react-recoil-demo Recoil インストール % yarn add recoil % yarn add @types/recoil % npm install recoil % npm install @types/recoil react-router-dom インストール % yarn add react-router-dom % yarn add @types/react-router-dom % npm install react-router-dom % npm install @types/react-router-dom tsconfig.json tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } RecoilRootの追加 RecoilRootをインポートし、ルートコンポーネントを囲います。 ここではAppコンポーネントを囲います。 UseContextではContext.Provider、 ReduxではProviderで、Appコンポーネントを囲うのと同じですね。 index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { RecoilRoot } from 'recoil'; ReactDOM.render( <React.StrictMode> <RecoilRoot> <App /> </RecoilRoot> </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(); App.tsxはルートの定義をしています。 App.tsx import React from "react"; import { BrowserRouter as Router, Route } from "react-router-dom"; import MemoList from "./pages/MemoList"; import MemoAdd from "./pages/MemoAdd"; import MemoEdit from "./pages/MemoEdit"; function App() { return ( <Router> <Route exact path="/" component={MemoList} /> <Route path="/add" component={MemoAdd} /> <Route path="/edit/:memoId" component={MemoEdit} /> </Router> ); } export default App; atomの定義 Recoilは、atomというステートの定義ファイルを作成します。 ステートの型や初期値を定義しましょう。 atom.ts import { atom } from "recoil"; export const memoAtomState = atom<string[]>({ key: "memoAtom", default: [], }); atomの読み書き atomは、コンポーネントから読み書きすることができます。 atomを読み取るコンポーネントは、暗黙的にサブスクライブという状態になり atomの値が変更されたら、対象のコンポーネントが再レンダリングされます。 よく使うコードの書き方はこちらです。 // 読み取りだけ const memos = useRecoilValue<string[]>(memoAtomState); // 書き込みだけ const setMemos = useSetRecoilState<string[]>(memoAtomState); // 読み込み書き込み両方 const [memos, setMemos] = useRecoilState<string[]>(memoAtomState); 読み取り専用、書き込み専用、読み書き両方の3種類の関数が用意されています。 1点注意が必要な点が、上記で述べたatomを読み取るコンポーネントは、再レンダリングの対象となることです。 つまり、atomに書き込みしかしないコンポーネントはサブスクライブされません。 そのため、書き込みしかしないコンポーネントは、useSetRecoilStateを使うことで、 不要なレンダリングを減らすことができます。 以下、残りの一覧画面、追加画面、変更画面のソースとなります。 pages/MemoList.tsx import React from "react"; import { useRecoilValue } from "recoil"; import { memoAtomState } from "../atom"; import { useHistory } from "react-router-dom"; const MemoList: React.FC = () => { const memos = useRecoilValue<string[]>(memoAtomState); const history = useHistory(); const handleLink = (path: string) => history.push(path); return ( <> {memos.map((memo, index) => ( <li key={index}> <label>{memo}</label> <button onClick={() => handleLink("/edit/" + index)}>編集</button> </li> ))} <button onClick={() => handleLink("/add")}>追加</button> </> ); }; export default MemoList; pages/MemoAdd.tsx import React, { useRef } from "react"; import { useSetRecoilState } from "recoil"; import { memoAtomState } from "../atom"; import { useHistory } from "react-router-dom"; const MemoAdd: React.FC = () => { const history = useHistory(); const textAreaReff = useRef<HTMLTextAreaElement>(null); const setMemos = useSetRecoilState<string[]>(memoAtomState); const handleClick = () => { if (textAreaReff.current !== null && textAreaReff.current!.value !== "") { setMemos((currVal: string[]) => { return [textAreaReff.current!.value, ...currVal]; }); history.push("/"); } }; return ( <> <textarea ref={textAreaReff}></textarea> <button onClick={handleClick}>追加</button> </> ); }; export default MemoAdd; pages/MemoEdit.tsx import React, { useEffect, useRef } from "react"; import { useRecoilState } from "recoil"; import { memoAtomState } from "../atom"; import { useParams, useHistory } from "react-router-dom"; const MemoEdit: React.FC = () => { const history = useHistory(); const textAreaReff = useRef<HTMLTextAreaElement>(null); const [memos, setMemos] = useRecoilState<string[]>(memoAtomState); const { memoId } = useParams<{ memoId?: string }>(); const handleClick = () => { if (textAreaReff.current !== null && textAreaReff.current!.value !== "") { setMemos((currVal: string[]) => { return [ textAreaReff.current!.value, ...currVal.filter((d, i) => i !== Number(memoId)), ]; }); history.push("/"); } }; useEffect(() => { textAreaReff.current!.value = memos[Number(memoId)]; }, [memoId, memos]); return ( <> <textarea ref={textAreaReff}></textarea> <button onClick={handleClick}>追加</button> </> ); }; export default MemoEdit; アプリ動作確認 デザインは置いておいて、このように動作します。 2. Recoil vs UseContext Recoilの説明はここまでとなりますが、 ここで思うことが、RecoilってUseContextにすごい似てる!!という点です。 ただ、UseContextはラップしたコンポーネントを全てレンダリングするため、 管理するステートが増えるとレンダリングコストが肥大化してしまうというデメリットがあります。 この点、Recoilはどうなのかを調査しました。 調査内容 RecoilとUseContextを使って、同じ動作をするコードを作成します。 動作時にレンダリングされるコンポーネントを観測します。 アプリは、ボタンをクリックするとカウントアップするという簡単なものです。 観測1.UseContext まずは、UseContextから観測を行います。 実行コードはこちら App.tsx import React, { createContext, useContext, useState } from "react"; const CountContext = createContext( {} as { count: number; setCount: React.Dispatch<React.SetStateAction<number>>; } ); function App() { const [count, setCount] = useState(0); console.log("rendered App."); return ( <CountContext.Provider value={{ count, setCount }}> <ShowCount /> <IncrementButton /> <ResetButton /> </CountContext.Provider> ); } const ShowCount = () => { const { count } = useContext(CountContext); console.log("rendered ShowCount."); return <div>{count}</div>; }; const IncrementButton = () => { const { setCount } = useContext(CountContext); console.log("rendered IncrementButton."); return ( <> <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button> </> ); }; const ResetButton = () => { const { setCount } = useContext(CountContext); console.log("rendered ResetButton."); return ( <> <button onClick={() => setCount(0)}>Reset</button> </> ); }; export default App; 結果1.UseContext UseContextの実行結果がこちらになります。 ボタンをクリックする度に、全てのコンポーネントがレンダリングされているのが分かります。 本来であれば、ボタンのレンダリングは不要です。 観測2.Recoil 続いて、Recoilの観測です。 実行コードはこちら App.tsx import React from "react"; import { useRecoilValue, useSetRecoilState } from "recoil"; import { countAtomState } from "./atom"; const App: React.FC = () => { console.log("rendered App."); return ( <> <ShowCount /> <IncrementButton /> <ResetButton /> </> ); }; const ShowCount: React.FC = () => { const count = useRecoilValue(countAtomState); console.log("rendered ShowCount."); return <div>{count}</div>; }; const IncrementButton: React.FC = () => { const setCount = useSetRecoilState(countAtomState); const handleOnClick = () => { setCount((prevValue) => prevValue + 1); }; console.log("rendered IncrementButton."); return <button onClick={handleOnClick}>+</button>; }; const ResetButton: React.FC = () => { const setCount = useSetRecoilState(countAtomState); const handleOnClick = () => { setCount(0); }; console.log("rendered ResetButton."); return <button onClick={handleOnClick}>Reset</button>; }; export default App; 結果2.Recoil Recoilの実行結果はこちらになります。 ボタンをクリックしても、カウントを表示するコンポーネントしかレンダリングされません。 さらに、注目したいのがResetボタンの挙動です。 Resetボタンの1回目クリックでは、Countステータスが変更されたので、レンダリングされますが、 2回目以降のクリックは、Countステータスは変わらないので、レンダリングされません。 このことから、Recoilのレンダリングは最小限で済むことが分かります。 結論 UseContextは、ステート変更時にラップされた全てのコンポーネントをレンダリングします。 一方、Recoilは、ステートを読み込んでいるコンポーネントのみレンダリングします。 このことから、レンダリングコストはRecoilの方が低いことがわかりました。 最後に Reactのステート管理ライブラリRecoilの紹介とUseContextの比較した調査結果の共有をさせていただきました。 読んでいただいた方のお力になれば幸いです。 宣伝 パーソルプロセス&テクノロジー株式会社(以下パーソルP&T)、システムソリューション(SSOL)事業部所属の堀江です。 私はモビリティソリューションデザインチームに所属しており、モビリティ(ここでは移動手段全般)に関するサービスを考えたり、アプリを構築したりしております。 いわゆる「MaaS」に取り組んでおります。 私たちが「MaaS」に取り組む中で、現在活用している、もしくは活用する予定の技術やサービスやとりあえず発信したいことなどなど、幅広くチームメンバーと共に執筆していきたいと思います。 メンバーごとに違った内容を発信していきますので、お楽しみに! また、「MaaS」について詳しく知りたい方は、チームメンバーの吉田が記事を掲載しておりますので、 ぜひそちらをご覧ください。 「MaaSとは」でたどり着いて欲しい記事 (1/3 前編) 「MaaSとは」でたどり着いて欲しい記事 (2/3 中編) 「MaaSとは」でたどり着いて欲しい記事 (3/3 後編) 最後まで読んでいただき、ありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jiraっぽいアプリをサクッと作ってみた!

はじめに みなさんこんにちは! 株式会社hokanでフロントエンドエンジニアをしている斉藤と申します!! 初投稿でございます。どうぞよろしくお願いします 今回はReactでJiraのスクラムボード機能の簡易版を作ったので紹介していこうと思います。 早速ですが成果物はこちらです! https://jira-clone-4e8c1.web.app 使った技術 react-hook-form react-beautiful-dnd Firebase chakra-ui react-hook-form react-hook-formはreactのhooksのような感じで使えるフォームライブラリで、react-hook-form側がなんでもやってくれるので、パフォーマンスもよくコードの記述量も減り、onChangeなどを発火させる必要もなくstate管理も楽なパーフェクトライブラリです。 今回はこんな感じで使いました。 import {useForm} from 'react-hook-form' const { register, formState: { errors, isSubmitting }, handleSubmit, } = useForm({ defaultValues: { ...initialValues }, }); <form onSubmit={onSubmit("create")}> <ModalBody> <FormControl id="stageItem" isInvalid={!!errors.title}> <FormLabel>タイトル</FormLabel> <Input id="title" {...register("title", { required: "タイトルは必須です", })} autoFocus /> <FormErrorMessage> {errors.title && errors.title.message} </FormErrorMessage> <Spacer h={4} /> <FormLabel>内容</FormLabel> <Textarea id="content" {...register("content")} /> <Spacer h={4} /> <FormLabel>ステータス</FormLabel> <Select {...register("statusId")}> {Object.values(DefaultTaskStatusMap).map((status) => ( <option key={status.id} value={status.id}> {status.title} </option> ))} </Select> </FormControl> </ModalBody> </form>   registerでコンポーネントをフックに登録したら、フォームフィールドの値のバリデーションと値を検知することができるようになり、handleSubmitの引数にhandleSubmit(customOnSubmit)みたいな感じで渡してあげるとcustomOnSubmitの引数にフォームの入力値が入ってきます。 エラーを検知した場合はformStateにエラーメッセージが入ってくるのでエラーハンドリングもピャピャッとできちゃいます。 簡単にフォームをコントロールできちゃいますね。 react-beautiful-dnd react-beatutiful-dndはドラッグ&ドロップが簡単にできるライブラリです。 <DragDropContext />でドラッグ&ドロップしたいエリアをラップしてあげて、 <Droppable />でラップしたエリアにドロップできるようになり、 <Draggable />でラップしたアイテムがドラッグできるようになります。 こちらのコードのようになるイメージですね! export const Content: React.FC<Props> = () => { const [items, setItems] = useState(_items); const onDragEnd = (result: DropResult) => { if (!result.destination) return; const startIndex = result.source.index; const endIndex = result.destination.index; const newItems = arrayMove(items, startIndex, endIndex); setItems(newItems); }; return ( <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId={"droppable"} direction={"vertical"}> {(provided) => ( <div ref={provided.innerRef} {...provided.droppableProps}> {items.map((item, index) => ( <Draggable key={index} index={index} draggableId={String(item.id)} > {(provided) => ( <p ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} > {item.title} </p> )} </Draggable> ))} {provided.placeholder} </div> )} </Droppable> </DragDropContext> ); }; Firebase firebaseは今回、firestoreとhostingとauthを利用しました。 詳細な説明は省きますが個人で軽くアプリを作りたい時などにおすすめです! authは今回はGoogle認証だけ使用しました! こちらはとても簡単でfirebaseのコンソールでGoogle認証をONにしていただいた後、このコードで簡単にGoogle認証できちゃいます! const signInWithGoogle = () => { const provider = new firebase.auth.GoogleAuthProvider(); firebase.auth().signInWithPopup(provider); }; firestoreはフロントだけでデータのやりとりができサクッとアプリを作りたいときにいいですね! hostingは 1. firebase cli をインストール 2. firebase init hosting 3. firebase deploy --only hosting の3ステップで完了するのでとても簡単ですね。 chakra-ui 今回一番感動したのはなんといってもこちら、chakra-uiです。 chakra-uiはReact用のコンポーネントライブラリで少ないコードで「いい感じのui」ができちゃう優れものです。 こんな感じでコンポーネントを<ChakraProvider><ChakraProvider/>でラップしてあげるとchakra-uiのテーマが使えるようになり、ダークテーマにも対応しています。 import Routes from "Routes"; import { ChakraProvider } from "@chakra-ui/react"; function App() { return ( <ChakraProvider> <Routes /> </ChakraProvider> ); } export default App; どれくらい簡単「いい感じのUI」ができるのかサンプルを用意しました! import { Modal as ChakraModal, ModalBody, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalFooter, Button, HStack, FormControl, FormLabel, Input, Spacer, Textarea, Select, useDisclosure, } from "@chakra-ui/react"; export const Modal: React.FC = () => { const { isOpen, onClose } = useDisclosure(); return ( <ChakraModal onClose={onClose} isOpen={isOpen} isCentered> <ModalOverlay /> <ModalContent> <ModalHeader>{"タスクを作成"}</ModalHeader> <ModalCloseButton /> <ModalBody> <FormControl id="stageItem"> <FormLabel>タイトル</FormLabel> <Input /> <Spacer h={4} /> <FormLabel>内容</FormLabel> <Textarea /> <Spacer h={4} /> <FormLabel>ステータス</FormLabel> <Select> {Object.values(DefaultTaskStatusMap).map((status) => ( <option key={status.id} value={status.id}> {status.title} </option> ))} </Select> </FormControl> </ModalBody> <ModalFooter> <HStack spacing={3}> <Button>{"タスクを作成"}</Button> <Button onClick={onClose}>キャンセル</Button> </HStack> </ModalFooter> </ModalContent> </ChakraModal> ); }; こちらのサンプルでこんな感じのモーダルができちゃいます! このモーダル、chakra-uiのコンポーネントだけで作っているのですがほとんどカスタムしてません。 const { isOpen, onClose, onOpen } = useDisclosure()でモーダルやダイアログ、ドロワーなどの状態を管理できたりします。 自分で作るとちょっと面倒なUIも作ってくれており、デフォルトでこんないい感じのUI作れちゃうのはとても気持ちがいいですね。 他にもaria属性の定義や、キーボードナビゲーションへの配慮もあるのでアクセシビリティも問題ありません。 chakra-uiは今後も積極的に使っていきたいと強く思いました! 所感 今回はやってみた系の記事でベテランエンジニアの方には特に学ぶこともなかったかと思いますが、駆け出しエンジニアの皆様やフロントやったことないエンジニアの方には役に立っていただけたら嬉しいです! 初めての記事でとても大変でしたがこれからも頑張って描いていこうと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"Warning: Received `true` for a non-boolean attribute"が出たときの対策

エラーメッセージ Warning: Received `true` for a non-boolean attribute 上記の様なエラーが発生。 対策 propsに$をつける 参考 https://github.com/styled-components/styled-components/issues/1198 https://styled-components.com/docs/api#transient-props
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む