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

Reactで TODOアプリを作る(アウトプット)

ReactでTODOアプリ作る!(アウトプット) React HooksとTypeScriptを使ってTODOアプリを作ったので、それのアウトプットを本記事で書こうと思います! まだまだReactを勉強し始めて間もないので至らない点があると思いますが、コメントでどんどんご指摘ください! 参考記事 React Hooks と TypeScript で簡単 TODO アプリ コード import { useState } from "react"; export type Todo = { value: string; readonly id: number; checked: boolean; removed: boolean; }; export type Filter = "all" | "checked" | "unchecked" | "removed"; export const App = () => { const [text, setText] = useState(""); const [todos, setTodos] = useState<Todo[]>([]); const [filter, setFilter] = useState<Filter>("all"); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, removed: false, }; setTodos([newTodo, ...todos]); setText(""); }; const handleOnEdit = (id: number, value: string) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); setTodos(newTodos); }; const handleOnCheck = (id: number, checked: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.checked = !checked; } return todo; }); setTodos(newTodos); }; const handleOnRemove = (id: number, removed: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.removed = !removed; } return todo; }); setTodos(newTodos); }; const handleOnEmpty = () => { const newTodos = todos.filter((todo) => !todo.removed); setTodos(newTodos); }; const filteredTodos = todos.filter((todo) => { switch (filter) { case "all": return !todo.removed; case "checked": return todo.checked && !todo.removed; case "unchecked": return !todo.checked && !todo.removed; case "removed": return todo.removed; default: return todo; } }); return ( <div> <select defaultValue="all" onChange={(e) => setFilter(e.target.value as Filter)} > <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">現在のタスク</option> <option value="removed">ごみ箱</option> </select> {filter === "removed" ? ( <button onClick={handleOnEmpty} disabled={todos.filter((todo) => todo.removed).length === 0} > ゴミ箱を空にする </button> ) : ( <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} disabled={filter === "checked"} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" disabled={filter === "checked"} onSubmit={handleOnSubmit} /> </form> )} <ul> {filteredTodos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" disabled={todo.checked || todo.removed} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? "復元" : "削除"} </button> </li> ); })} </ul> </div> ); }; TODOアプリの仕様 ・タスクを追加できる ・タスクを一覧できる ・登録済みタスクを編集できる ・タスクの完了/未完了を操作できるようにする ・登録済みの todo を削除可能にする ・タスク (Todo) を既済・未済・削除済みなどの状態によってフィルタリングできる ・削除済みアイテムを「ごみ箱」フィルタから完全に削除できる 解説(アウトプット) 機能ごとに分けて解説します タスクを追加する import { useState } from 'react'; type Todo = { // typeを使ってタスクの型定義をする value: string; // 内容をvalueとし型をstringとする }; export const App = () => { const [text, setText] = useState(''); // useState使ってstate変数のtext、関数のsetTextを定義、初期値は"" const [todos, setTodos] = useState<Todo[]>([]); // useState使ってstate変数のtodos、関数のsetTodosを定義、型はTodo初期値は[] const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { // コールバック関数 handleOnChangeを定義 setText(e.target.value); // inputタグのvalueを受け取る }; const handleOnSubmit = () => {  // todosステートを更新する関数を定義 if (!text) return; // textがなかったらreturnを返す const newTodo: Todo = {  //イミュータブルを保つために新しいTodoを作成 value: text, }; setTodos([newTodo, ...todos]); // setTodosを使ってnewTodoをtodosのコピーに追加する setText(''); // フォームへの入力をクリアする }; return ( <div> <form onSubmit={(e) => { e.preventDefault(); // Enterキーを押したときにページがリロードされるのを防ぐ handleOnSubmit(); // 関数のhadleOnSubmitを呼び出す }} > <input type="text" value={text} onChange={(e) => handleOnChange(e)} /> // 関数のhandleOnChangeを呼び出す <input type="submit" value="追加" onSubmit={handleOnSubmit} /> // onSubmitにコールバック関数としてhandleOnSubmitを渡す </form> </div> ); }; タスクを一覧できる import { useState } from 'react'; type Todo = { value: string; readonly id: number; // keyの設定のためにreadonlyプロパティとしてidをnumber型として定義する }; export const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), // idとしてタスクを追加した時間を使う }; setTodos([newTodo, ...todos]); setText(''); }; return ( <div> <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" onSubmit={handleOnSubmit} /> </form> <ul> {todos.map((todo) => { // mapメソッドを使ってtodosステートを展開する return <li key={todo.id}>{todo.value}</li>; // liタグにkeyを設定する })} </ul> </div> ); }; 登録済みタスクを編集できる import { useState } from 'react'; type Todo = { value: string; readonly id: number; }; export const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), }; setTodos([newTodo, ...todos]); setText(''); }; const handleOnEdit = (id: number, value: string) => { // 関数handleOnEditを定義し、引数にidとvalueを受け取る const deepCopy = todos.map((todo) => ({ ...todo })); // deepコピーをする const newTodos = deepCopy.map((todo) => { // deepコピーであるnewTodosをmapメソッドで展開する if (todo.id === id) { // todoステートのidとhandleOnEditに渡されたidが等しいかを判別 todo.value = value; } return todo; }); setTodos(newTodos); // setTodosを使ってtodosステートを更新 }; return ( <div> <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" onSubmit={handleOnSubmit} /> </form> <ul> {todos.map((todo) => { return ( <li key={todo.id}> <input type="text" value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} // todoステートのidと等しいかを判別するためにidを渡す /> </li> ); })} </ul> </div> ); }; タスクの完了/未完了を操作できるようにする import { useState } from 'react'; type Todo = { value: string; readonly id: number; checked: boolean; // 完了済みかどうかを示すフラグを追加 }; export const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, }; setTodos([newTodo, ...todos]); setText(''); }; const handleOnEdit = (id: number, value: string) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); setTodos(newTodos); }; const handleOnCheck = (id: number, checked: boolean) => { // チェックボックスが押された際に呼ばれるコールバック関数を定義 const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.checked = !checked; // checkedの値を反転させる } return todo; }); setTodos(newTodos); }; return ( <div> <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" onSubmit={handleOnSubmit} /> </form> <ul> {todos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)}  /> <input type="text" disabled={todo.checked} // checkedがtrueだったら入力できないようにする value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> </li> ); })} </ul> </div> ); }; 登録済みの todo を削除可能にする import { useState } from 'react'; type Todo = { value: string; readonly id: number; checked: boolean; removed: boolean; // 削除済みかどうかを示すフラグを定義 }; export const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, removed: false, }; setTodos([newTodo, ...todos]); setText(''); }; const handleOnEdit = (id: number, value: string) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); setTodos(newTodos); }; const handleOnCheck = (id: number, checked: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.checked = !checked; } return todo; }); setTodos(newTodos); }; const handleOnRemove = (id: number, removed: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.removed = !removed; // removedの値を反転させる } return todo; }); setTodos(newTodos); }; return ( <div> <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" onSubmit={handleOnSubmit} /> </form> <ul> {todos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} // removedがtrueならチェックボックスを使えないようにする checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" disabled={todo.checked || todo.removed} // checkedがtrueまたは、removedがtrueだったら入力できないようにする value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? '復元' : '削除'} </button> </li> ); })} </ul> </div> ); }; タスクをフィルタリングする機能を追加する import { useState } from 'react'; type Todo = { value: string; readonly id: number; checked: boolean; removed: boolean; }; type Filter = 'all' | 'checked' | 'unchecked' | 'removed'; // Filter型を定義 export const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const [filter, setFilter] = useState<Filter>('all'); // filterステートとそれを更新するsetFilterを定義、初期値はall const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, removed: false, }; setTodos([newTodo, ...todos]); setText(''); }; const handleOnEdit = (id: number, value: string) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); setTodos(newTodos); }; const handleOnCheck = (id: number, checked: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.checked = !checked; } return todo; }); setTodos(newTodos); }; const handleOnRemove = (id: number, removed: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.removed = !removed; } return todo; }); setTodos(newTodos); }; const filteredTodos = todos.filter((todo) => { // フィルタリングをしたtodo配列を定義 switch (filter) { case 'all': return !todo.removed; // allの場合、removedがfalseのタスクを返す case 'checked': return todo.checked && !todo.removed; // checkedの場合、checkedがtrueでremovedがfalseのタスクを返す case 'unchecked': return !todo.checked && !todo.removed; // uncheckedの場合、checkedがfaklseでremovedがfalseのタスクを返す case 'removed': return todo.removed; // removedの場合、removedがtrueのタスクを返す default: return todo; } }); return ( <div> <select defaultValue="all" // defaultのfilterにallを設定する onChange={(e) => setFilter(e.target.value as Filter)} > <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">現在のタスク</option> <option value="removed">ごみ箱</option> </select> <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} disabled={filter === 'checked' || filter === 'removed'} // filterがcheckedまたは、removedの場合、入力が出来ないようにする onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" disabled={filter === 'checked' || filter === 'removed'} // filterがcheckedまたは、removedの場合、追加が出来ないようにする onSubmit={handleOnSubmit} /> </form> <ul> {filteredTodos.map((todo) => { // filterを通したtodoを使う return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" disabled={todo.checked || todo.removed} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? '復元' : '削除'} </button> </li> ); })} </ul> </div> ); }; ごみ箱を空にする機能を追加する import { useState } from 'react'; type Todo = { value: string; readonly id: number; checked: boolean; removed: boolean; }; type Filter = 'all' | 'checked' | 'unchecked' | 'removed'; export const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const [filter, setFilter] = useState<Filter>('all'); const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const handleOnSubmit = () => { if (!text) return; const newTodo: Todo = { value: text, id: new Date().getTime(), checked: false, removed: false, }; setTodos([newTodo, ...todos]); setText(''); }; const handleOnEdit = (id: number, value: string) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.value = value; } return todo; }); setTodos(newTodos); }; const handleOnCheck = (id: number, checked: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.checked = !checked; } return todo; }); setTodos(newTodos); }; const handleOnRemove = (id: number, removed: boolean) => { const deepCopy = todos.map((todo) => ({ ...todo })); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.removed = !removed; } return todo; }); setTodos(newTodos); }; const handleOnEmpty = () => { // ゴミ箱を空にするコールバック関数を作成 const newTodos = todos.filter((todo) => !todo.removed); // todosの中からremovedがfalseのタスクのみを取り出して展開する setTodos(newTodos); }; const filteredTodos = todos.filter((todo) => { switch (filter) { case 'all': return !todo.removed; case 'checked': return todo.checked && !todo.removed; case 'unchecked': return !todo.checked && !todo.removed; case 'removed': return todo.removed; default: return todo; } }); return ( <div> <select defaultValue="all" onChange={(e) => setFilter(e.target.value as Filter)} > <option value="all">すべてのタスク</option> <option value="checked">完了したタスク</option> <option value="unchecked">現在のタスク</option> <option value="removed">ごみ箱</option> </select> {filter === 'removed' ? ( // filterがremovedの時にbuttonを表示する <button onClick={handleOnEmpty} disabled={todos.filter((todo) => todo.removed).length === 0} // todosにremovedがtrueのタスクがなかったらボタンを押せなくする > ゴミ箱を空にする </button> ) : ( <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input type="text" value={text} disabled={filter === 'checked'} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" disabled={filter === 'checked'} onSubmit={handleOnSubmit} /> </form> )} <ul> {filteredTodos.map((todo) => { return ( <li key={todo.id}> <input type="checkbox" disabled={todo.removed} checked={todo.checked} onChange={() => handleOnCheck(todo.id, todo.checked)} /> <input type="text" disabled={todo.checked || todo.removed} value={todo.value} onChange={(e) => handleOnEdit(todo.id, e.target.value)} /> <button onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? '復元' : '削除'} </button> </li> ); })} </ul> </div> ); }; 終わりに 今回作ったTODOアプリをuseReducerとuseContextを使ってさらに機能を増やす記事もあったのでそれを参考にまた作っていきたいです!! 今回作ったコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最低限のCSSでフロントエンド開発を乗り切る

この記事について CSS書けないしデザインセンスも皆無な自分が個人開発で画面を作る際、どのようにしてそれなりの見た目のものを作ったかについてまとめた記事 読者は↓のような人を想定 CSSはmarginくらいしか知らない デザインなんてやったことない Reactの基本的なこと(コンポーネントの作り方など)はわかる 戦略 最低限のCSSで見栄えのいい画面を作る戦略の紹介 結論から言うとCSSには出来合いのコンポーネントを使えば書かなくていいものと絶対に逃れられないものの2種類あるので、前者はライブラリを使うことで作業量を削減しようという作戦 部品の見た目を決めるCSSと配置を決めるCSS CSSは大きく分けて2つに分けられる(と思っている) 部品の見た目を決めるCSS 例えばこういうおしゃれなボタン ボタンの角を丸くしたり影をつけたり色を変えたり...といったことをして実装されている このようなCSS(borderとかbackground-colorとか)がこれに該当する 部品の配置を決めるCSS Googleのログインフォーム ロゴやテキストボックス、ボタンなどの各部品をいい感じに並べている marginやdisplayなどがこれに該当する ライブラリを使ってCSSの記述量を減らそう 上記のCSSのうち部品の見た目を決めるCSSに関してはこれらのライブラリを用いることで記述する必要がなくなる 部品の配置を決めるCSSに関してはどうあがいても逃れられないので記述する必要はあるが、アーキテクチャを工夫したりライブラリを利用することでCSS初心者でも比較的書きやすくしたり見通しをよくしてメンテナンスしやすくしたりはできる ※(詳細は後述)Material-UIやBootstrapなどのライブラリには部品の配置を決めるためのコンポーネントが用意されているのでさらにCSSの記述量を減らすことが可能 ライブラリ/フレームワーク紹介 今回はReactをベースにし、以下の2種類のデザイン用ライブラリを組み合わせて実装する おしゃれにスタイリングされたコンポーネントを提供するライブラリ(コンポーネントライブラリ) 部品の見た目を決めるCSSはこれに一任する ReactのコンポーネントのCSSを書くためのライブラリ(CSSフレームワーク) 部品の配置を決めるCSSはこれを使って記述する それぞれいくつか候補をあげていくので適当に見繕ってインストールしてほしい ちなみに自分が選択したのはMaterial-UIとMaterial-UIのsx props コンポーネントライブラリ ざっと触った感じ機能に大きな差があるわけではないので見た目の好みで決めていいと思う 特にこだわりがなければコミュニティが大きく更新も頻繁なMaterial-UIかBootstrapがよさそう Material-UI マテリアルデザインに沿って実装されたコンポーネントを提供 歴史が長くドキュメントが豊富 Bootstrap フラットデザインのコンポーネントを提供 こちらも古株でドキュメントには困らない Semantic-UI Bootstrapと似た感じのコンポーネント 上2つと比べるとマイナーでドキュメントも少ない CSSフレームワーク styled-components Reactのコンポーネント単位でのスタイリング機能を提供するフレームワーク const Button = styled.button` background: ${props => props.primary ? "palevioletred" : "white"}; color: ${props => props.primary ? "white" : "palevioletred"}; font-size: 1em; margin: 1em; padding: 0.25em 1em; border: 2px solid palevioletred; border-radius: 3px; `; render( <div> <Button>Normal</Button> <Button primary>Primary</Button> </div> ); CSSを文字列で表現しているためテンプレートリテラルで変数を埋め込むことも可能 コンポーネント単位なのでどうしても↓2つに比べて記述量が多くなってしまうのが玉にキズ ただしこちらの記事のようにDOM、スタイリング、依存性注入とコンポーネントを細かく分けて実装する方法だとかなり便利 Tailwind CSS UtilityFirst(汎用的なCSSのクラスを作りそれらを組み合わせてスタイリングする手法)なCSSフレームワーク shadow-sm shadow-lgのようにスタイルの大きさをenum的に表現してくれているのがgood ちょっと凝ったデザインにしようものならクラス名が肥大化してしまうのが難点 MUI.sx props 単体のフレームワークではなくMaterial-UIのコンポーネントのsxというpropsに直接CSSを渡す方法 CSSをjavascriptのObjectで表現するので何かと融通が利く 若干話は逸れるがMaterial-UIなどにはこちらのように部品の配置を決めるためのコンポーネントが用意されているため、それらを活用すればsx propsで記述するCSSの量もグッと減らせる コード例 import * as React from 'react'; import Box from '@mui/material/Box'; export default function BoxSx() { return ( <Box sx={{ width: 300, height: 300, backgroundColor: 'primary.dark', '&:hover': { backgroundColor: 'primary.main', opacity: [0.9, 0.8, 0.7], }, }} /> ); } 実装方針 ReactのプロジェクトにMaterial-UIをインストールしてコンポーネントを実装する コンポーネントをどういった粒度で分割していくかについてはAtomic Designに従って決定する Atomic Design UI設計の方法論の1つ 詳細についてはわかりやすい記事がたくさんあるので各自参照していただきたい ここではAtomic Designのざっくりとした解説とCSSのコーティング規約(っぽいもの)を各階層ごとに書き連ねていく Atoms Atomic Designの最小単位となる階層 ↑のLABEL、INPUT、BUTTONのようにこれ以上細かく分解できないような粒度のコンポーネントをこの階層に実装する CSSは自身の見た目を決めるもののみ記述するべきで、自身の配置や大きさについては関心を持たないようにするべき 具体的には↓ // OK .btn { text-align: center; background-color: blue; padding: 8px; border-width: 2px; } // NG .btn { margin-top: 8px; width: 200px; } Molecules Atomsを組み合わせて実装されるコンポーネント これも自身の見た目に関するCSSのみ記述可能 Organisms AtomsやMoleculesを組み合わせて実装されるコンポーネント これも自身の見た目(ロゴと検索フォームを横並びにするなど、自身の子コンポーネントの配置が主)に関するCSSのみ記述可能 Templates Organismsを組み合わせてページ全体を構成するコンポーネント ただし実際にTemplatesコンポーネント内でOrganismsコンポーネントを呼び出すわけではなく、ヘッダーはここで動画があそこで...という風にレイアウトを決めているだけ CSSは↑にある通りOrganismsコンポーネントをどのように配置するか、サイズはどれくらいかなどを記述していく Pages Templatesコンポーネントに対して必要なOrganismsコンポーネントを渡すことでページを表現するためのコンポーネント CSSはこのコンポーネントでは書かない 実践 以下のコマンドでプロジェクト作成 npx create-react-app {プロジェクト名} --template typescript cd {プロジェクト名} npm install @mui/material @emotion/react @emotion/styled ディレクトリ構成 src/ ├ components/ │ ├ atoms/ │ ├ molecules/ │ ├ organisms/ │ ├ templates/ │ └ pages/ └ styles/theme.ts components以下にAtomicDesignに従ってコンポーネントを実装していく また、グローバルなスタイル定義はstyles/theme.tsに書く ThemeProvider Material-UIのコンポーネントのCSSをグローバルに管理できるプロバイダ 以下のようにテーマオブジェクトを作成して<ThemeProvider />に渡してやることで、全子コンポーネントのCSSを変更できる const theme = createTheme({ status: { danger: orange[500], }, }); <ThemeProvider theme={theme}> ... </ThemeProvider> 以下ではThemeProviderでカスタムできる項目のうちよく使いそうなものだけ抜粋して紹介 Color コンポーネントの色を変更できる const theme = createTheme({ palette: { primary: { light: '#0066ff', main: '#0044ff', dark: '#0022ff', }, secondary: { main: '#ff4400', // light, darkを省略した場合mainの値に応じて自動で決定される }, }, }); カラーコードを自分で作るのが面倒な場合は↓を使うと便利 Material-UIのカラーパレット メインの色から補色を自動で算出してくれるツール Spacing marginやpaddingなどの大きさをカスタマイズできる コンポーネント実装 今回はよくあるログインページを実装してみる Atoms 基本的にMaterial-UIのコンポーネントを少しカスタマイズするだけ src/components/atoms/Button.tsx import * as mui from "@mui/material"; type ButtonProps = { label: string; type?: "submit" | "button"; color?: "primary" | "secondary"; onClick?: () => void; }; export function Button(props: ButtonProps) { return ( <mui.Button variant="contained" color={props.color} onClick={() => props.onClick && props.onClick()} type={props.type} fullWidth={true} > {props.label} </mui.Button> ); } その他Typography、TextFieldのコンポーネントも作っておく Molecules 今回は対象となるコンポーネントがないので省略 Organisms ※今回の主題はデザインなので関数などは適当 この階層ではAtomsのコンポーネントを呼び出し、それらの配置を決めるコンポーネントを実装する 以下はログインフォームのコンポーネント src/components/organisms/LoginForm.tsx import { Paper, Stack } from "@mui/material"; import { Button } from "../atoms/Button"; import { Textbox } from "../atoms/Textbox"; import { Typography } from "../atoms/Typography"; export function LoginForm() { return ( <Paper sx={{ padding: 4, }} > <Stack spacing={4}> <Typography value="Login" variant="h3" /> <Textbox value="test@example.com" onChange={() => null} label="Email" type="email" /> <Textbox value="password" onChange={() => null} label="Password" type="password" /> <Button label="LOGIN" color="primary" /> </Stack> </Paper> ); } 新たに登場した<Paper />、<Stack />について簡単に補足 Paper 画用紙っぽく浮き出た感じに装飾された<div> Stack コンポーネントを1次元に並べるコンポーネント directionをcolumn(デフォルト)にすると垂直方向に、rowにすると水平方向に子コンポーネントを1列に並べる 他にも2次元に並べるための<Grid>や中央揃えするための<Container>などが用意されている ついでにヘッダーのコンポーネントも実装する src/components/organisms/Header.tsx import { AppBar } from "@mui/material"; import { Typography } from "../atoms/Typography"; export function Header() { return ( <AppBar position="relative"> <Typography value="My App" variant="h1" /> </AppBar> ); } Templates Organismsのコンポーネントの配置を決めるためのコンポーネント src/components/templates/AuthenticationTemplate.tsx import { Box, Container } from "@mui/material"; type AuthenticationTemplateProps = { header: JSX.Element; form: JSX.Element; }; export function AuthenticationTemplate(props: AuthenticationTemplateProps) { return ( <Box sx={{ width: "100vw", height: "100vh", }} > <Box>{props.header}</Box> <Container sx={{ marginTop: 10, }} > {props.form} </Container> </Box> ); } Pages Templatesに対してOrganismsのコンポーネントを流し込むためのコンポーネント (今回は省略したものの)各コンポーネントで使用するコールバック関数や引数などもこのコンポーネントで注入する形になる src/components/pages/LoginPage.tsx import { Header } from "../organisms/Header"; import { LoginForm } from "../organisms/LoginForm"; import { AuthenticationTemplate } from "../templates/AuthenticationTemplate"; export function LoginPage() { return <AuthenticationTemplate header={<Header />} form={<LoginForm />} />; } 実装完了したらnpm startで実行
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Qiita始めました!

目次 1.はじめに 2.自己紹介 3.おわりに 1. はじめに 初めまして! 日報アプリgamba!(ガンバ)でエンジニアをしている池田です! この度、日報アプリgamba!のメンバーでブログを始…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

styled-component

component selector styledでラップされてるコンポーネントしか使えない 要素にclassNameを渡さないと、styledでラップしてもスタイル効かない 特にmoleculesとかでハマりそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React]Material UIでレスポンシブ対応

本記事の目的 Material UIが@mui/materialからインポートするようになってから、 useStyles,makeStylesの組み合わせの使用が非推奨になりました。 それに伴い、レスポンシブ対応をするには、以前まではtheme.breakpointsを使用するケースが大半だったと思うのですが、それ以外のやり方でレスポンシブ対応させるやり方を調べましたのでまとめます。 やり方は以下の通りです。 ①media queryを使用する場合 ②useMediaQueryを使用する場合 ①media queryを使用する場合 box.tsx <Box component="div" sx={{ fontSize: "1rem", "@media screen and (min-width:600px)": { width: "40%", }, }} > sample box </Box> box.tsx import { styled } from "@mui/material/styles" import Box from "@mui/material/Box const StyledBox = styled(Box)(() => ({ fontSize: "1rem", "@media screen and (min-width:600px)": { width: "40%", }, })) sx内に直接書く、ないしはstyledのなかに上記のように書き記すやり方です。 ②useMediaQueryを使用する場合 box.tsx import useMediaQuery from "@mui/material/useMediaQuery" const SampleBox = () => { const matches: boolean = useMediaQuery("(min-width:577px)") {matches ? ( <Box component="div"> Sample Box </Box> ) : ( <Box></Box> )} } また、Material UIのブレイクポイントを使用しても記載ができます。 const matches: boolean = useMediaQuery(() => theme.breakpoints.up("sm")); ただ、このやり方だと記述量が非常に多くなってしまうため、おすすめではありません。 参考記事 Material UIでのレスポンシブサイト作成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】Reactのprettier設定

.prettierrc.json { "arrowParens": "avoid", //単一引数時に()の有無 "bracketSpacing": true, // 単語間の空白スペース有無 "htmlWhitespaceSensitivity": "css", //cssの設定 "insertPragma": false, "jsxBracketSameLine": false, "jsxSingleQuote": false, "printWidth": 80, //80文字を超えたら改行する。 "proseWrap": "preserve", "quoteProps": "as-needed", "requirePragma": false, "semi": false, //セミコロンなし "singleQuote": true, //シングルクオート "tabWidth": 2, //tab幅はスペース2 "trailingComma": "all", "useTabs": false }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub Pages に Remix をデプロイする

無駄があるかもしれませんが、 まずはRemixアプリを作成する ↑の公式ページより $ npx create-remix@latest ? Welcome to Remix! Let's get you set up with a new project. ? Where would you like to create your app? sample-app ? What type of app do you want to create? Just the basics ? Where do you want to deploy? Choose Remix if you're unsure, it's easy to change deployment targets. Remix App Server ? TypeScript or JavaScript? TypeScript ? Do you want me to run `npm install`? Yes ... > postinstall > remix setup node Successfully setup Remix for node. とりあえずローカルで動かしてみる $ cd sample-app $ npm run dev 以下にアクセス Github リポジトリを作成する リポジトリ名: ユーザー名.github.io を作成する 作成したプロジェクトでgit初期設定 git init git add . git commit -m "first commit" git branch -M main git remote add origin git@github.com:ユーザー名/リポジトリ名.git git push -u origin main 以下にアクセスしてtokenを作成しておく ↑のようにチェックをつけたら画面下部の[Generate token]を押す デプロイ設定 プロジェクト直下に.github/workflows/gh-pages.ymlを作成して以下を記入 .github/workflows/gh-pages.yml name: Remix on Github Pages on: push: branches: - main pull_request: branches: - main jobs: build-deploy: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v2 with: node-version: '16' - name: cache dependencies uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: install dependencies run: npm install - name: Start server and download static content with wget run: | npm run dev & sleep 10 && wget --mirror http://localhost:3000 -P out --no-host-directories --page-requisites --adjust-extension - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: personal_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./out external_repository: ユーザー名/ユーザー名.github.io git add .github git commit git push origin main プッシュ後、actionが完了したら、リポジトリ→settings→Github Pagesよりブランチを変更する https://ユーザー名.github.io にアクセスするとlocalhost:3000で確認した画面と同じ画面が表示される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む