CodePenを使ってReact.jsを学ぶ練習log
2020-11-16
18:18:10 ここで作業している
これcode保持されるのか?
上書きすると消えるのかな?
わからん
18:23:19 これで動いた
jsxconst element =
<p id="the-text"
className="text">
Hello world
</p>;
ReactDOM.render(element, document.getElementById('app'));
18:24:23 引き続き
ここを参考にいろいろやってみる
18:32:56 こうなった
jsxconst Element = () =>
<p id="the-text"
className="text">
Hello world
</p>;
const Text = props =>
<p id={props.id}
className="text">
{props.children}
</p>;
const App =
<>
<Text id="the-text">Hello, React</Text>
<Element />
</>;
ReactDOM.render(App, document.getElementById('app'));
18:35:39 2つの違うHTML風objectがある
↑正式名称わからん
jsxconst App = <h1>Hello, world</h1>;
ReactDOM.render(App, document.getElementById('app'));
と
jsxconst App = () => <h1>Hello, world</h1>;
ReactDOM.render(<App />, document.getElementById('app'));
18:43:12 練習問題を解いてみる
問題1
>以下の HTML に表される DOM を出力する React コードを書いてください。
> <img src="https://media.giphy.com/media/33OrjzUFwkwEg/giphy.gif" alt="" />
18:46:06 簡単だった
jsxconst Text = props =>
<p id={props.id}
className="text">
{props.children}
</p>;
const Gif = ({id, src}) => <img id={id} src={src} />;
const App = () =>
<>
<Text id="the-text">Hello, React</Text>
<Gif id="the-gif" src="https://media.giphy.com/media/33OrjzUFwkwEg/giphy.gif" />
</>;
ReactDOM.render(<App />, document.getElementById('app'));
問題2
>以下の HTML に表される DOM を出力する React コードを書いてください。
html<section id="react" class="box">
<h1 class="title">React</h1>
<dl class="definition">
<dt class="definition-title">Initial release</dt>
<dd class="definition-content">2013/5</dd>
<dt class="definition-title">Github stars</dt>
<dd class="definition-content">147,940</dd>
</dl>
</section>
<section id="vue" class="box">
<h1 class="title">Vue.js</h1>
<dl class="definition">
<dt class="definition-title">Initial release</dt>
<dd class="definition-content">2014/2</dd>
<dt class="definition-title">Github stars</dt>
<dd class="definition-content">163,165</dd>
</dl>
</section>
<section id="angular" class="box">
<h1 class="title">Angular</h1>
<dl class="definition">
<dt class="definition-title">Initial release</dt>
<dd class="definition-content">2016/9</dd>
<dt class="definition-title">Github stars</dt>
<dd class="definition-content">60,571</dd>
</dl>
</section>
こうかな?
jsxconst items = [
{
title: "react",
info: [
{ title: "Initial release", content: "2013/5" },
{ title: "Github stars", content: "147,940" }
]
},
{
title: "vue",
info: [
{ title: "Initial release", content: "2014/2" },
{ title: "Github stars", content: "163,165" }
]
},
{
title: "angular",
info: [
{ title: "Initial release", content: "2016/9" },
{ title: "Github stars", content: "60,571" }
]
}
];
const Title = ({ children }) => <h1 className="title">{children}</h1>;
const Definition = (props) => (
<dl className="definition">
{props.info.map(({ title, content }) => (
<React.Fragment key={title}>
<dt className="definition-title">{title}</dt>
<dd className="definition-content">{content}</dd>
</React.Fragment>
))}
</dl>
);
const Box = ({ id, title, info }) => (
<section id={title} className="box">
<Title>{title}</Title>
<Definition info={info} />
</section>
);
const App = () =>
items.map((item) => (
<Box id={item.title} title={item.title} info={item.info} />
));
ReactDOM.render(<App />, document.getElementById("app"));
19:06:52 時間かかった
20minか
scrapboxで書くとlinter効かないからつらい
今度は最初から
で書く
19:16:59 できた
jsx// GIF 共有サイト GIPHY から持ってきた GIF ID
const gifIds = [
'10dU7AN7xsi1I4', 'tBxyh2hbwMiqc', 'ICOgUNjpvO0PC',
'33OrjzUFwkwEg', 'MCfhrrNN1goH6', 'rwCX06Y5XpbLG'
];
// 上記配列の要素をランダムに返す
function getGifId() {
const max = gifIds.length;
const index = Math.floor(Math.random() * Math.floor(max));
return gifIds[index];
}
const Gif = ({ id }) => <img id={id} src={`https://media.giphy.com/media/${id}/giphy.gif`} />;
const App = () => {
// ??? GIF ID を表す state を生成する
// 初期値として gifIds[0]を渡す
const [id, setId] = React.useState(gifIds[0]);
// ??? ボタンが押されると GIF 画像が切り替わる
const handleClick = () => setId(getGifId());
return (
<>
<p>
<button onClick={handleClick}>play</button>
</p>
<Gif id={id} />
</>
);
};
ReactDOM.render(<App />, document.getElementById('app'));
便利だな
使ったもの
すっごい便利なweb serviceが作られるようになったなー
21:15:31 つくった
21:15:57 抜粋
tsximport * as React from "react";
const input = document.createElement("input");
export const FormText = () => {
const [name, setName] = React.useState("John");
EventHandlerの型がnative DOMのwrapperになっていることに注意
tsx const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setName(e.target.value);
return (
<>
<h1>Hello, {name}</h1>
<input value={name} onChange={handleChange} />
</>
);
};
logicが絡んできた
これは今度やるか
一度hintに沿ってやった後、もっといい感じにlogicを分離させたい
21:56:41 sand box
2020-11-19 16:30:13 できた
questionaire.tsximport * as React from "react";
import { options } from "./options";
export const Questionaire = ({ question }: { question: string }) => {
const [val, setVal] = React.useState(options[0].value);
const [text, setText] = React.useState("");
const getAnswer = () => (val === "" ? text : val);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setVal(e.target.value);
const onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
setText(e.target.value);
return (
<>
<p>{question}</p>
<p>
{options.map((option) => (
<label>
<input
type="radio"
value={option.value}
checked={val === option.value}
onChange={handleChange}
/>
{option.label}
</label>
))}
</p>
<p hidden={val !== ""}>
<label>
自由回答欄:<span> </span>
<input type="text" value={text} onChange={onTextChanged} />
</label>
</p>
<hr />
<p>
回答: <span>{getAnswer()}</span>
</p>
</>
);
};
options.tsexport const options = [
{ value: 'js', label: 'JavaScript' },
{ value: 'py', label: 'Python' },
{ value: 'rb', label: 'Ruby' },
{ value: '', label: 'その他' },
];
refactoringしたほうが良い箇所はあるが、やらないで次に行く
refactoringするより先の問題をときたい
password入力欄とボタンをセットにするとよさそう
16:56:20 できた
PasswordForm.tsximport * as React from "react";
export const PasswordForm = () => {
const [password, setPassword] = React.useState("");
const [visible, setVisible] = React.useState(false);
return (
<>
<input
type={visible ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="button"
children={"view"}
onClick={() => setVisible(!visible)}
/>
</>
);
};
event handlerに直接関数を書き込めば、 e: React.ChangeEvent<HTMLInputElement>
と書かずに済む
2020-11-20 01:44:42 password情報を外部に置く
このままだとpasswordの取り出しがしにくい
てかViewとLogicが分離できていない
Logic: passwordの保持と変更処理
View: passwordの入力・表示
伏せ字に切り替えるか否かも
いわゆる
依存性の注入をやって、ViewからLogicを分離する
01:59:10 できた
lint errorを取り除くために、型定義を作る必要がある
PasswordForm.tsxtype Props = {
password: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
PasswordForm.tsxexport const PasswordForm = ({ password, onChange }: Props) => {
const [visible, setVisible] = React.useState(false);
return (
<>
<input
type={visible ? "text" : "password"}
value={password}
onChange={onChange}
/>
<button
type="button"
children={"view"}
onClick={() => setVisible(!visible)}
/>
</>
);
};
App.tsximport * as React from "react";
import "./styles.css";
import { PasswordForm } from "./PasswordForm";
export default function App() {
const [password, setPassword] = React.useState("");
return (
<div className="App">
<p>password</p>
<PasswordForm
password={password}
onChange={(e) => setPassword(e.target.value)}
/>
<p>{password.length}characters</p>
</div>
);
}
02:31:27 入力欄はこれでいいかな?
Guess_old.tsx// 入力欄
import * as React from "react";
type Props = {
prediction: number;
setPrediction?: (prediction: number) => void;
};
これだと数値が変更されない
Guess_fail.tsxexport const Guess = ({ prediction, setPrediction }: Props) => {
return (
<>
<input type="number" value={prediction} />
<button
type="button"
children={"predict"}
onClick={() => setPrediction(prediction)}
/>
常に prediction
で固定されてしまう
onChange
を指定する必要がある
とすると、 useState
で状態を持つ必要が出てくるのか。
03:41:24 buttonを押すまで <App />
に値を伝えられないので、代わりに <Guess />
で保持しておく必要がある、とも考えられる
02:41:33 変えた
02:46:26 onChange
に直接 setValue(e.target.valueAsNumber)
を入れると、無効な値を入力したときに入力欄が空になってしまう。
それを防ぐために e.target.checkValidity()
で入力値を検証し、無効な値のときは何もしないようにする
errorを表す赤枠が表示されなくなる
無効な値をそもそも入力できなくなるため
Guess.tsx const [value, setValue] = React.useState(prediction);
return (
<>
<input
type="number"
value={value}
onChange={(e) =>
e.target.checkValidity()
? setValue(e.target.valueAsNumber)
: undefined
}
/>
<button
type="button"
children={"predict"}
onClick={() => setPrediction?.(value)}
/>
02:52:34 最小値と最大値を外から設定できるようにした
あと placefolder
も設定した
Guess.tsx// 入力欄
import * as React from "react";
type Props = {
prediction: number;
onSubmit?: (prediction: number) => void;
max?: number;
min?: number;
};
export const Guess = ({
prediction,
onSubmit: setPrediction,
max,
min
}: Props) => {
const [value, setValue] = React.useState(prediction);
return (
<>
<input
type="number"
value={value}
max={max}
min={min}
placeholder={`from ${min} to ${max}`}
onChange={(e) =>
e.target.checkValidity()
? setValue(e.target.valueAsNumber)
: undefined
}
/>
<button
type="button"
children={"predict"}
onClick={() => setPrediction?.(value)}
/>
</>
);
};
03:21:40 本体完成
要件から少し変えた
英語にした
勝敗が決まった後のメッセージを追加した
App.tsximport * as React from "react";
import "./styles.css";
import { Guess } from "./Guess";
import { random } from "./random";
const max = 50;
const initialCount = 5;
export default function App() {
const [prediction, setPrediction] = React.useState(0);
const [answer, setAnswer] = React.useState(random(max));
const [message, setMessage] = React.useState("");
const [count, setCount] = React.useState(initialCount);
↓勝敗が確定していない状態を表現するために undefined
を使った
本当は専用の型を使うのがbest
enum JudgeState = {Playing, Won, Lost};
など
App.tsx const [isWinner, setIsWinner] = React.useState<boolean | undefined>(
undefined
);
const judge = (num: number) => {
if (count === 0 || isWinner !== undefined) {
setMessage(
`You already ${
isWinner ? "won" : "lost"
}. To replay, please click the "replay" button below.`
);
return;
}
setCount(count - 1);
if (num === answer) {
setMessage("correct!");
setIsWinner(true);
} else if (count === 1) {
setMessage(`You lose! The answer is ${answer}`);
setIsWinner(false);
} else if (num > answer) {
setMessage(`The answer is less than ${num}`);
} else if (num < answer) {
setMessage(`The answer is more than ${num}`);
}
};
const replay = () => {
setAnswer(random(max));
setCount(initialCount);
setMessage("");
setIsWinner(undefined);
};
return (
<div className="App">
<Guess
prediction={prediction}
onSubmit={(value) => {
setPrediction(value);
judge(value);
ここで judge
に prediction
を渡してはならない
この段階の prediction
はまだ更新前の値が入っている
setXX
で更新されるのは、次のrenderingのとき。すぐには更新されない
App.tsx }}
max={max}
min={0}
/>
<p hidden={message === ''}>{message}</p>
<p>You can challenge predict {count} times.</p>
<button children={'replay'} onClick={replay} />
</div>
);
}
一旦ご飯食べる
style.css
でimportした
04:35:48 App.tsx
つくった
仕方ないので別な関数を使う
05:21:38 やめた
async/awaitが必要
async/awaitはReactの属性内で使えない
もろに副作用扱い
05:31:54 できた
Todo.tsximport * as React from "react";
import { ToDoData } from "./TodoData";
export const Todo = () => {
const [items, setItems] = React.useState([
new ToDoData("Learn JavaScript"),
new ToDoData("Learn React"),
new ToDoData("Get some good sleep")
]);
return (
<div className="panel">
<div className="panel-heading">
<span>⚛</span>️ React ToDo
</div>
{items.map((item) => (
<label key={item.key} className="panel-block">
<input type="checkbox" />
{item.text}
</label>
))}
<div className="panel-block">{items.length} items</div>
</div>
);
};
TodoData.tsexport class ToDoData {
constructor(text: string, done: boolean = false) {
this._object = { text: text, done: done };
this._key = "";
this.updateKey();
}
get text() {
return this._object.text;
}
get done() {
return this._object.done;
}
get key() {
return this._key;
}
private updateKey() {
this._key = Math.random().toString(32).substring(2);
}
private _key: string;
private _object: { text: string; done: boolean };
}
これで最低限度を表示できた
05:34:27 componentsにばらしていく
まずはTodoを表示する項目から
05:55:19 できた
TodoItem.tsximport * as React from "react";
import { TodoData } from "./TodoData";
type Props = {
item: TodoData;
onCheck: (item: TodoData, checked: boolean) => void;
};
export const TodoItem = ({ item, onCheck }: Props) => {
return (
<label className="panel-block">
<input
type="checkbox"
checked={item.done}
onChange={(e) => onCheck(item, e.target.checked)}
/>
<span className={item.done ? "has-text-grey-light" : ""}>
{item.text}
</span>
</label>
);
};
Todo.tsximport * as React from "react";
import { TodoData } from "./TodoData";
import { TodoItem } from "./TodoItem";
export const Todo = () => {
const [items, setItems] = React.useState([
new TodoData("Learn JavaScript"),
new TodoData("Learn React"),
new TodoData("Get some good sleep")
]);
const onCheck = (item: TodoData, checked: boolean) => {
// 該当するitemを更新
const newItems = items.map((value) => {
if (value.key !== item.key) return value;
return new TodoData(value.text, checked);
});
setItems(newItems);
};
これ毎描画ごとにTodoDataを全部精査して値を変更することになるのか
dataが多くなったら更新が大変そうだ
まあ、実際には全部
とかに任せるだろうから、そんなに気にすることでもないんだと思うけど。
Todo.tsx
return (
<div className="panel">
<div className="panel-heading">
<span>⚛</span>️ React ToDo
</div>
{items.map((item) => (
<TodoItem key={item.key} item={item} onCheck={onCheck} />
))}
<div className="panel-block">{items.length} items</div>
</div>
);
};
06:11:07 Todoの入力部分を実装する
06:22:18 できた
Input.tsximport * as React from "react";
type Props = { onAdd: (text: string) => void };
export const Input = ({ onAdd }: Props) => {
const [text, setText] = React.useState("");
const sendText = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== "Enter") return;
onAdd(text);
setText("");
};
return (
<div className="panel-block">
<input
className="input"
type="text"
placeholder="Enter to add a task"
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={sendText}
/>
</div>
);
};
onChange
はそとに出したほうが良いかも?
logicとUIを分離させる
06:23:30 もうこの時点で簡単なTodo appは完成している
すごいな
見た目は
が全部やってくれている
あとtutorial形式だから、予め設計済みなのもあるかも
その設計通り、もしくはそこにアレンジを加えた形でコードを書いていけばいい
06:28:33 Filterを作る
設計を変更する
Filter TodoList
内に TodoItem
を表示するようにする
filter方法の切り替え欄はFilterToggle Filter
に表示させる
07:13:21 できた
これで完成
Filter.tsximport * as React from "react";
const filterState = ["All", "Done", "Todo"] as const;
export type FilterState = typeof filterState[number];
type Props = {
value: FilterState;
onChange: (value: FilterState) => void;
};
export const Filter = ({ value, onChange }: Props) => {
const handleClick = (
key: FilterState,
e: React.MouseEvent<HTMLAnchorElement>
) => {
e.preventDefault();
onChange(key);
};
return (
<div className="panel-tabs">
{filterState.map((state) => (
なぜだかはわからない
Filter.tsx <a
href="#"
onClick={(e) => handleClick(state, e)}
className={state === value ? "is-active" : ""}
>
TodList.tsximport * as React from "react";
import { TodoData } from "./TodoData";
import { TodoItem } from "./TodoItem";
import { Filter, FilterState } from "./Filter";
type Props = {
items: TodoData[];
onCheck: (item: TodoData, checked: boolean) => void;
};
export const TodoList = ({ items, onCheck }: Props) => {
const [state, setState] = React.useState<FilterState>("All");
const displayItems = items.filter((item) => {
if (state === "All") return true;
if (state === "Done" && item.done) return true;
if (state === "Todo" && !item.done) return true;
return false;
});
return (
<>
<Filter value={state} onChange={(key) => setState(key)} />
{displayItems.map((item) => (
<TodoItem key={item.key} item={item} onCheck={onCheck} />
))}
<div className="panel-block">{displayItems.length} items</div>
</>
);
};
<Todo>
→ <TodoList>
→ <TodoItem>
へと、 <TodoList>
を経由して値と更新関数を渡している
更新関数はそのまま素通し
値は表示内容に応じてfilteringしている
つまり中抜き
Todo.tsximport * as React from "react";
import { TodoList } from "./TodoList";
import { TodoData } from "./TodoData";
import { Input } from "./Input";
export const Todo = () => {
const [items, setItems] = React.useState([
new TodoData("Learn JavaScript"),
new TodoData("Learn React"),
new TodoData("Get some good sleep")
]);
const onCheck = (item: TodoData, checked: boolean) => {
// 該当するitemを更新
const newItems = items.map((value) => {
if (value.key !== item.key) return value;
return new TodoData(value.text, checked);
});
setItems(newItems);
};
const onAdd = (text: string) => setItems([...items, new TodoData(text)]);
return (
<div className="panel">
<div className="panel-heading">
<span>⚛</span>️ React ToDo
</div>
<Input onAdd={onAdd} />
<TodoList items={items} onCheck={onCheck} />
</div>
);
};
大量にコード書いているからだんだんページが重くなってきた……
分けるか?
07:31:49 分けた