# 『Learn React In 30 Minutes』をやってみた
このブログはVuePressで作っていて、Algolia検索を導入する時とかでVue.jsに触れたし、AlgoliaのGeo検索機能とvue2-leafletを使って地図にプロットしたり、ちょっと馴染んできた感があるのだけど、AlgoliaのUnified InstantSearch E-Commerceの中身をみてたら、PreactっていうReactの軽い版的なのを使っていて、こっちも勉強しなきゃいけないな、と思っていたところでした。
# Learn React In 30 Minutes
今回お題に選んだのはコレ👇。Youtubeで検索して出てきたヤツ。
ではさっそくやっていきましょう。
# Visual Studio Codeでブランクなプロジェクトから
learn-react-in-x-minutesっていう空のディレクトリを作って、VS CodeのTerminalでそこを開いてから、nodeが入っていることを確認して、
$ node -v
v12.12.0
npxでプロジェクトを作っていきます。 👇を叩くと、
npx create-react-app .
👇のようにディレクトリの中に色々なファイルが作成されます。
で、👇のようにコマンドも用意されるわけですが、自分の場合は他のプロジェクトでyarnを選択していたからか、諸々yarnになっています。(個人的にはそれぞれの違いもあんまりよく分からないくらいなのでそのままでいきます。笑)
とりあえずプロダクションにデプロイとかするわけではないので、yarn startだけかなぁ、今回使うの。
# プロジェクトの中のファイルの解説とローカルでの実行
とりあえず、index.htmlにrootという名前のdivがあることを確認して、
<div id="root"></div>
それからsrcフォルダをみていく。大事なのはindex.jsの👇のところで、Appというコンポーネントを指定しつつ、document.getElementByIdで上記のrootという名前のdivを指定しているところ。
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
(React.StrictModeって先生の画面には出てないけど…んま、先進みます。笑)
App.jsにはベーシックなボイラープレートが書かれている。
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
まずはstartしてコレを見ていきましょう。
ところが、yarn startしたら👇のように怒られた…笑
There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.
The react-scripts package provided by Create React App requires a dependency:
"babel-loader": "8.1.0"
Don't try to install it manually: your package manager does it automatically.
However, a different version of babel-loader was detected higher up in the tree:
/Users/eijishinohara/node_modules/babel-loader (version: 8.0.6)
Manually installing incompatible versions is known to cause hard-to-debug issues.
If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That will permanently disable this message but you might encounter other issues.
どうもbabel-loaderのバージョンが、、っていう話らしいので👇をやっていきます。
Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder. → package-lock.jsonは無かったのでyarn.lockだけ消しました
Delete node_modules in your project folder. → 消しました
Remove "babel-loader" from dependencies and/or devDependencies in the package.json file in your project folder. → 特にpackage.jsonにはbabel-loaderの記述は無かったです
Run npm install or yarn, depending on the package manager you use. → yarn installしました
んま、それでもダメで、どうもこのプロジェクトディレクトリ外のnode_modulesを参照してしまってるっぽくて、今度はwebpackのバージョンが、、とか言われたので、その外部のnode_modulesディレクトリを削除しました。
ってことで、無事起動したかと思ったら、今度は3000番ポートを既に使ってね?と。 👉確かにUnified InstantSearchで使ってる。。
ってことで👇のように3001番ポートで起動しました。
Compiled successfully!
You can now view learn-react-in-x-minutes in the browser.
Local: http://localhost:3001
On Your Network: http://10.0.1.3:3001
Note that the development build is not optimized.
To create a production build, use yarn build.
ってことでようやく👇こんなんでました。
例えば、Learn Reactの後にToday!!とか付けると
Learn React Today!!
👇画面にも反映されます
# 不要箇所を削除していく
まずはApp.jsのボイラープレートを削除。
👇こんな風にnullをreturnするだけにすると画面には何も表示されなくなります。
function App() {
return null
}
👇のスタイルシートとかSVGも削除
import logo from './logo.svg';
import './App.css';
App.test.jsも、App.cssも、index.cssも、logo.svgも、serviceworker.jsも、setupTests.jsも、全部消してスッキリしつつ、index.jsも余計な部分を削ぎ落としていきます。
import './index.css';
〜略〜
import * as serviceWorker from './serviceWorker';
〜略〜
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
# Appコンポーネントに実装を追加していく
App.jsでreturn nullだったところを👇こんな風に。後から別のTodoListっていうコンポーネントを作っていく。これはJSXというReactにおいてHTMLのタグを書いていくようなヤツ
function App() {
return (
<TodoList />
)
}
TotoList.jsというファイルを作ってコーディングしていく前にコードスニペット用のプラグインをVS Codeに入れていきます。
そしたら、rfcってタイプして出てきたヤツを選択するだけで👇こんなのを作ってくれる。便利。
ということで、App.jsにてTodoListをインポート。
import TodoList from './TodoList'
そして、TodoListコンポーネントに皆んな大好きHello Worldを追加していくと
export default function TodoList() {
return (
<div>
Hello World
</div>
)
}
👇出ました出ました(ちなみに、faviconは消さずにそのままにしておいてある)
TodoListに続いて、App.jsにinputタグを追加するとコンパイルで怒られる。
<TodoList />
<input type="text" />
2つのHTML要素(この場合はJSX要素)をreturnの中に書くのは文法的にNGだそうで、空のカッコでこれらを括ってやる。フラグメントって呼ぶらしい。
<>
<TodoList />
<input type="text" />
</>
ということで👇のように、入力ボックスが表示できました。
続いてボタンを足したりして
function App() {
return (
<>
<TodoList />
<input type="text" />
<button>Add Todo</button>
<button>Clear Complete</button>
<div>0 left to do</div>
</>
)
}
最終的に👇こんな感じ。
# State(状態の管理)
まずはアプリケーションにState(状態)の設定をしていく。状態に変更があったら画面にレンダリングする感じ。
TodoはStateで保持して、変更があったら(追加、変更、削除)レンダリングし直すよ、と。
ということで、App.jsにて useState フックを使っていきます。👇のようにインポートしてあげて、
import React, { useState } from 'react';
function App()の中では空の配列として定義(元々Todoには1件も登録されていないので)
useState([])
からの👇のように変数化してオブジェクトのdestructuring(配列から値を取り出して別の変数に格納)。
const [todos, setTodos] = useState([])
そして、デフォルトのデータとしてTodo 1とTodo 2をば。
const [todos, setTodos] = useState(['Todo 1', 'Todo 2'])
そして、このtodosをTodoListコンポーネントに渡してやる。これはPropsと呼ばれていて、コンポーネント間の値の受け渡しを行えるようにする機構。
<TodoList todos={todos} />
# TodoListコンポーネントで todos のレンダリング
TodoList.jsの方では todos が受け取れるように👇のようにしてあげて、それを画面表示できるようにしていきましょう。
export default function TodoList({ todos }) {
まずはdivの中で{todos.lengsh}と記述して、画面にtodosの要素数を表示してみます。{}の間はJavaScriptが記述できます。
return (
<div>
{todos.length}
</div>
)
👇画面では 2 が表示されています。
そして、Todo.js というコンポーネントを作っていきます。
まずはrfcで👇をズコっと。
import React from 'react'
export default function Todo() {
return (
<div>
</div>
)
}
そして、ここでは todo を受け取れるようにしておきます。
export default function Todo({ todo }) {
呼び出し元のTodoList.jsの方では、このTodo.jsをインポートしてあげて、
import Todo from './Todo'
一つ一つのtodoの処理はTodo.jsに丸投げするように。
export default function TodoList({ todos }) {
return (
todos.map(todo => {
return <Todo todo={todo} />
})
)
}
Todo.jsで単純に渡ってきたtodoを画面に描画してみます。
export default function Todo({ todo }) {
return (
<div>
{todo}
</div>
)
}
👇無事に表示されていますね。
とは言え、Dev Toolsで見てみるとエラーが出ています。それぞれの要素にはkeyがあるべきです、と。
そうじゃないとReactはこのプロパティが正しく更新されたのか分からなくなってしまう。何かTodos配列に変更があったら毎度レンダリングし直すけど、変更があったものだけを更新したいとした時にkeyがあると便利。
ということで👇こんな感じ(今の所todosの要素はユニークだから)にしてやると、エラーは無くなりました。
export default function TodoList({ todos }) {
return (
todos.map(todo => {
return <Todo key={todo} todo={todo} />
})
)
}
そして、App.jsに戻って、配列を key(id) と name な形にしてやって、、、complete: false ってなんじゃラホイ?
const [todos, setTodos] = useState([{ id: 1, name: 'Todo 1', complete: false}])
んま、進んで、TodoList.jsで👇todo.idにしてやって、
return <Todo key={todo.id} todo={todo} />
Todo.jsで👇todo.nameを表示するように。
return (
<div>
{todo.name}
</div>
)
これで特に何か変わったわけではないですが、終わったかどうかをチェックしていきたいわけで、ここにチェックボックスを付けていきます(だからcomplete: falseだったのね、と)
ってことでTodo.jsの中身は👇
export default function Todo({ todo }) {
return (
<div>
<label>
<input type="checkbox" checked={todo.complete} />
{todo.name}
</label>
</div>
)
}
👇のようにチェックボックスが表示されています。
# 状態をやりくりしていく
まずはApp.jsでデフォルトで定義していた配列を空にする。
const [todos, setTodos] = useState([{ id: 1, name: 'Todo 1', complete: false}])
👇👇👇
const [todos, setTodos] = useState([])
そして、Add Todoボタンが押された時に、この配列に値が入るようにしていきます。
まずはボタンにonClickリスナーを仕掛けて handleAddTodo というファンクションが呼ばれるように
<button onClick={handleAddTodo}>Add Todo</button>
そして、handleAddTodoファンクションを作っていく。
function handleAddTodo(e) {
}
とはいえ、インプットに入力された値を取得する方法がないので、useRefというフックを使っていく。
import React, { useState, useRef } from 'react';
inputタグには ref={todoNameRef} を追加してあげて、
<input ref={todoNameRef} type="text" />
👇のようにしてやると入力値が取れるようになるらしい。
const todoNameRef = useRef()
ってことで、取得した値をconsole.logしてみます。
const todoNameRef = useRef()
function handleAddTodo(e) {
const name = todoNameRef.current.value
if (name === '') return
console.log(name)
}
👇こんな感じで出力されています。
そして、ようやく登場してきたsetTodosに値を追加します。
function handleAddTodo(e) {
const name = todoNameRef.current.value
if (name === '') return
setTodos(prevTodos => {
return [...prevTodos, {id: 1, name: name, complete: false}]
})
todoNameRef.current.value = null
}
...(配列を展開)とか一見難しいけど、やってることは配列のケツに新しい要素を追加してるだけです。要素は仮でidを1にして値をそのままnameに突っ込んで、完了はしていないタスクなのでfalse、と。
ってことで、画面から操作すると👇とりあえず追加は出来ていますが、keyが毎回1なので重複しているよエラーがコンソールに出ています。
Warning: Encountered two children with the same key, `1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
in TodoList (at App.js:20)
in App (at src/index.js:7)
in StrictMode (at src/index.js:6)
# UUIDを使ってkeyを生成する
yarn add uuidをしてあげて
$ yarn add uuid
yarn add v1.19.1
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
warning " > @testing-library/user-event@7.2.1" has unmet peer dependency "@testing-library/dom@>=5".
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ uuid@8.1.0
info All dependencies
└─ uuid@8.1.0
✨ Done in 4.42s.
これをインポートしてあげて(Youtubeのとimportの仕方が異なるけど、uuidのGitHubのReadme読んでたら、ECMAScript Module syntaxのところに👇こう書いてあった)
import { v4 as uuidv4 } from 'uuid';
👇こんな風にしてkeyをランダムなIDにする
return [...prevTodos, {id: uuidv4(), name: name, complete: false}]
ってことで、Keyが重複しているエラーは出なくなりました。
とは言え、今の所、どこにも保存しないので、ページをリロードすると毎回ゼロ件になってしまいます。
# ローカルストレージに値を保存する
useEffectというフックを使っていく。
import React, { useState, useRef, useEffect } from 'react';
そして、todosに何か変更があったら必ず👇のようにlocalStorageに保存する。
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
LOCAL_STORAGE_KEYってのは単なる定数で、今回は'todoApp.todos'ってのがついてて、JSON.stringifyってのはJSONオブジェクトを文字列するメソッド。
これで、画面からの入力はローカルストレージに保存されるようになったものの、保存したデータをロードしてやる君も必要だよねってことで、もう一個useEffectを。
👇もし、保存されているtodosがあるのであれば、それをsetTodosすることでロードしてあげる。文字列で保存されているのでParseしてJSONにするのを忘れずに。
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY)
if (storedTodos) setTodos(storedTodos)
}, [])
そんなこんなで画面をリフレッシュしてもローカルに保存された値が表示されるようになりました。
# チェックボタンの実装
現状はチェックしても何もできない状態なのでtoggleTodoというファンクションを作っていく。
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
元のオブジェクトを操作するのではなく、今回のnewTodosのように新しいオブジェクトにコピーしてから操作を行うこと、と。
そして、newTodosの中でidが一致するものがあったら、completeの状態を逆にしてやる(!todo.completeのところ)。でもってnewTodosをsetしてめでたし。
そして、このtoggleTodoファンクションをTodoListコンポーネントに渡してやる。
<TodoList todos={todos} toggleTodo={toggleTodo} />
TodoList.jsの方ではtoggleTodoを受け取って、
export default function TodoList({ todos, toggleTodo }) {
更にTodoコンポーネントの呼び出しに渡してあげて
return (
todos.map(todo => {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
Todo.jsの方でもtoggleTodoを受け取って、
export default function Todo({ todo, toggleTodo }) {
ようやく最終的にチェックボックスにonChangeをしかけてあげる。ここでのファンクションは新しく作成したものになり、handleTodoClickっていう名前にする。
<input type="checkbox" checked={todo.complete} onChange={handleTodoClick} />
そのhandleTodoClickファンクションの中では該当のtodoのidをtoggleTodoに渡してやる。
function handleTodoClick() {
toggleTodo(todo.id)
}
これでチェックできるようになったし、画面をリフレッシュしても状態は保たれたままです。
# あと何個タスクが残っているか表示する
これは簡単で、App.jsの中で complete でないものでフィルタしてやって、その要素数を表示してやればいい。
<div>{todos.filter(todo => !todo.complete).length} left todo</div>
👇こんな感じで残りが表示されるようになりました。
もちろん、新しく要素が追加されると最初はcompeteがfalseなので残りタスクは積み上がっていきます。
# 最後にClear Completeの実装
これもサクっと、ボタンにonClickでhandleClearTodosというファンクションを仕掛けてあげて、
<button onClick={handleClearTodos}>Clear Complete</button>
handleClearTodosの中では完了したタスクは消すっていうか、完了していないヤツだけフィルタリングしてsetTodosしてやるっていう流れ。
function handleClearTodos() {
const newTodos = todos.filter(todo => !todo.complete)
setTodos(newTodos)
}
👇のように選択したものが消えていきます。
# いやー、とてもよくわかりました。ありがとうございました!
今回はエントリレベルな感じということで、Web Dev Simplifiedのコースをチェックしてくれよな!と。
Twitterアカウントもありそうですね👉DevSimplified