React環境構築
- brewインストール
- nodebrewインストール
- パスを通す(bashじゃない人がいたら言ってください)
- nodejsのインストール
ここまで記事通り
yarnのインストール
> npm -v
6.9.0
npmが入っていれば
> npm i -g yarn
create-react-appのインストール
> yarn global add create-react-app
...
...
success Installed "create-react-app@3.1.1" with binaries:
- create-react-app
✨ Done in 11.25s.
プロジェクトを作成してみる
> create-react-app my_app
> cd my_app
> yarn start
ローカルサーバーが立ち上がってhttp://localhost:3000 にアクセスすれば
この画面が出てくるはずです
もうかっこいい
React入門
stateとprops
Reactを始めるのに欠かせないstateとpropsについて
state
props
コンポーネント間で共有される変数
他のコンポーネントのstateをpropsとして受け取ることもできるし
他のコンポーネントにstateをpropsとして渡すこともできます
書き換えながら理解しましょう
今回はReact16.8で導入されたHooksを使用して書いていきます
これまで使用されてきた書き方との比較はhttps://ja.reactjs.org/docs/hooks-state.html を参照してください
App.jsx
を以下のように変更します
import React, { useState } from 'react';
import './App.css';
import Todo from './pages/Todo';
function App() {
// 第1変数がstate, 第2変数がstateを変化させる関数
const [input, setInput] = useState('');
const [todoList, setTodoList] = useState([]);
const addTodo = () => {
setTodoList([...todoList, input]);
setInput('');
}
const deleteTodo = (index) => {
setTodoList(todoList.filter((_, idx) => idx !== index))
}
return (
<div className="App">
<input onChange={(e) => setInput(e.target.value)} value={input}/>
<button onClick={() => addTodo()}>追加</button>
{/* todoListという変数とdeleteTodoという関数をpropsとしてTodoコンポーネントに渡している*/}
<Todo todoList={todoList} deleteTodo={deleteTodo}/>
</div>
);
}
export default App;
> cd src
> mkdir pages
> cd pages
> mkdir Todo
> cd Todo
> touch index.jsx
でsrc/pages/Todo
配下にindex.jsx
を作成してください
src/pages/Todo/index.jsx
を以下のように書き換えます
import React from 'react';
// todoListという変数とdeleteTodoという関数をpropsとして受け取る
const Todo = ({todoList, deleteTodo}) => (
<div>
{/*受け取ったtodoListを使って表示する*/}
{todoList.map((todo, idx) => (
<div>
{todo}
<button onClick={() => deleteTodo(idx)}>削除</button>
</div>
))}
</div>
)
export default Todo;
ここまで書けばこんな感じ(↓)になるはずです
stateとprops若干わかりました??
ちょっと寄り道
styled-component
htmlのタグのスタイルをカスタムした状態でコンポーネントを定義できます
下は<p>
タグを<Title>
という名前で定義してスタイルを当てている例
const Title = styled.p`
font-size: 26px;
color: #0097a7;
letter-spacing: 2.8px;
font-weight: 200;
`;
> yarn add styled-components
コンポーネントを有効活用してみましょう
先ほど作成したTodoコンポーネントを利用して完了済みTodoリストも同時に表示してみましょう
さらに完了済みボタンと戻すボタンも追加して、Todoの状態を変更できるようにしてみましょう
App.jsx
を以下のように書き換える
import React, { useState } from 'react';
import './App.css';
import Todo from './pages/Todo';
import styled from 'styled-components'
function App() {
// 第1変数がstate, 第2変数がstateを変化させる関数
const [input, setInput] = useState('');
const [todoList, setTodoList] = useState([]);
const [finishedList, setFinishedList] = useState([]);
const addTodo = () => {
if (!!input) {
setTodoList([...todoList, input]);
setInput('');
}
}
const deleteTodo = (index) => {
setTodoList(todoList.filter((_, idx) => idx !== index))
}
const deleteFinishTodo = (index) => {
setFinishedList(finishedList.filter((_, idx) => idx !== index))
}
const finishTodo = (index) => {
deleteTodo(index)
setFinishedList([...finishedList,todoList.find((_, idx) => idx === index)])
}
const reopenTodo = (index) => {
deleteFinishTodo(index)
setTodoList([...todoList,finishedList.find((_, idx) => idx === index)])
}
return (
<div className="App">
<Title>Todoリスト</Title>
<input onChange={(e) => setInput(e.target.value)} value={input}/>
<button onClick={() => addTodo()}>追加</button>
<TodoContainer>
{/* todoListという変数とdeleteTodoという関数をpropsとしてTodoコンポーネントに渡している*/}
<SubContainer>
<SubTitle>未完了</SubTitle>
<Todo todoList={todoList} deleteTodo={deleteTodo} changeTodoStatus={finishTodo} type="todo"/>
</SubContainer>
<SubContainer>
<SubTitle>完了済み</SubTitle>
<Todo todoList={finishedList} deleteTodo={deleteFinishTodo} changeTodoStatus={reopenTodo} type="done"/>
</SubContainer>
</TodoContainer>
</div>
);
}
export default App;
const Title = styled.p`
font-size: 26px;
color: #0097a7;
letter-spacing: 2.8px;
font-weight: 200;
`;
const SubTitle = styled.p`
font-size: 22px;
color: #5c5c5c;
`;
const SubContainer = styled.div`
width: 400px;
`;
const TodoContainer = styled.div`
display: flex;
flex-direction: row;
width: 80%;
margin: 0 auto;
justify-content: space-between;
`;
src/pages/Todo/index.jsx
もちょっと変更
import React from 'react';
import styled from 'styled-components'
// todoListという変数とdeleteTodoという関数をpropsとして受け取る
const Todo = ({todoList, deleteTodo, changeTodoStatus, type}) => (
<div>
{/*受け取ったtodoListを使って表示する*/}
{todoList.map((todo, idx) => (
<Container>
{todo}
<button onClick={() => deleteTodo(idx)}>削除</button>
<button onClick={() => changeTodoStatus(idx)}>{type === "todo" ? "完了済みにする" : "戻す"}</button>
</Container>
))}
</div>
)
export default Todo;
const Container = styled.div`
color: #5c5c5c;
letter-spacing: 1.8px;
`;
ついでにindex.css
も変更
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #fff2cc; /*ここ追加*/
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
ここまで行けば
完了済みボタンを押せばTodoが完了済みリストに追加され
戻すボタンを押せばTodoが未完了リストに戻るようになるはずです
firestoreを使ってデータを永続化してみる
ここまではデータをstateで管理していたがstateはブラウザをリロードすると初期化されてしまいます
そこでデータをdbに保存して管理して上げる必要があります
今回は簡単にデータを保管できるfirebaseのcloud firestoreを使用してTodoのデータを保持してみましょう
firebaseにプロジェクトを作成してみる
プロジェクトを追加で新しいプロジェクトを作成します
適当に名前をつけて続行
アナリティクスは設定しないを押せばプロジェクトが作られます
プロジェクトの作成が完了したら
</>
のマークを押します
適当にニックネームをつけてHostingは設定しなくていいです(あとでやります)
var firebaseConfig = {
...
}
firebase.initializeApp(firebaseConfig);
をコピーして次へ進んでください
一旦ターミナルに戻って先ほど作成したプロジェクトの配下で
> yarn global add firebase-tools
> yarn add firebase
コマンドラインでfirebaseにログインします
> firebase login
ログインができたらsrc/index.js
の先頭に先ほどコピーしたfirebaseのConfigを貼り付けます
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import firebase from 'firebase';
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "xxxxxxxxxxxxxxxxxxxxxxx",
authDomain: "xxxxxxxxxxxxxxxxxxxxxxxx",
databaseURL: "xxxxxxxxxxxxxxxxxxxxxxx",
projectId: "xxxxxxxxxxxxxx",
storageBucket: "",
messagingSenderId: "xxxxxxxxxxxx",
appId: "xxxxxxxxxxxxxxxxxxxxxx"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
ReactDOM.render(<App />, document.getElementById('root'));
// 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();
firestoreでデータベースを作成してみる
Database
を選択し、画面が遷移したらデータベースの作成
を選択します
モーダルが表示されたらテストモードで開始
を選択して次へ進みます
ロケーションはどこでもいいので完了を押します
これでfirestoreの設定は終了
画面が移動したらコレクションの開始を選択してtodoList
と入力して次へ
ドキュメントIDとフィールドとタイプを以下のように設定して保存
今回は簡単に文字列の配列で保存します
次にドキュメントの追加を選択してドキュメントIDとフィールドとタイプを設定して保存
reactからfirestoreを操作してみる
App.jsx
を以下のように書き換えてみましょう
import React, { useState, useEffect } from 'react'; // 修正
import './App.css';
import Todo from './pages/Todo';
import styled from 'styled-components';
import firebase from 'firebase'; // 追記
import 'firebase/firestore'; // 追記
function App() {
// 第1変数がstate, 第2変数がstateを変化させる関数
const [input, setInput] = useState('');
const [todoList, setTodoList] = useState([]);
const [finishedList, setFinishedList] = useState([]);
// Loadingを判定する変数
const [isLoading, setIsLoading] = useState(true);
// 未完了のTodoが変化したかを監視する変数
const [isChangedTodo, setIsChangedTodo] = useState(false);
// 完了済みのTodoが変化したかを監視する変数
const [isChangedFinished, setIsChangedFinished] = useState(false);
const db = firebase.firestore(); // 追記
// 追記 一番最初にfirestoreからデータを取ってきてstateに入れる
useEffect(() => {
(async () => {
const resTodo = await db.collection("todoList").doc("todo").get();
// stateに入れる
setTodoList(resTodo.data().tasks);
const resFinishedTodo = await db.collection("todoList").doc("finishedTodo").get();
// stateに入れる
setFinishedList(resFinishedTodo.data().tasks);
// Loading終了
setIsLoading(false);
})()
}, [db])
useEffect(() => {
if (isChangedTodo) {
(async () => {
// 通信をするのでLoadingをtrue
setIsLoading(true)
const docRef = await db.collection('todoList').doc('todo');
docRef.update({ tasks: todoList })
// Loading終了
setIsLoading(false)
})()
}
}, [todoList, isChangedTodo, db])
useEffect(() => {
if (isChangedFinished) {
(async () => {
// 通信をするのでLoadingをtrue
setIsLoading(true)
const docRef = await db.collection('todoList').doc('finishedTodo');
docRef.update({ tasks: finishedList })
// Loading終了
setIsLoading(false)
})()
}
setIsChangedFinished(false)
}, [db, finishedList, isChangedFinished])
const addTodo = async () => {
if (!!input) {
// 追記 Todoが変化したのでtrue
setIsChangedTodo(true);
setTodoList([...todoList, input]);
setInput('');
}
}
const deleteTodo = (index) => {
// 追記 Todoが変化したのでtrue
setIsChangedTodo(true);
setTodoList(todoList.filter((_, idx) => idx !== index))
}
const deleteFinishTodo = (index) => {
// 追記 完了済みTodoが変化したのでtrue
setIsChangedFinished(true);
setFinishedList(finishedList.filter((_, idx) => idx !== index))
}
const finishTodo = (index) => {
// 追記 Todo、完了済みTodoがともに変化したのでtrue
setIsChangedTodo(true);
setIsChangedFinished(true);
deleteTodo(index)
setFinishedList([...finishedList,todoList.find((_, idx) => idx === index)])
}
const reopenTodo = (index) => {
// 追記 Todo、完了済みTodoがともに変化したのでtrue
setIsChangedTodo(true);
setIsChangedFinished(true);
deleteFinishTodo(index)
setTodoList([...todoList,finishedList.find((_, idx) => idx === index)])
}
return (
<div className="App">
<Title>Todoリスト</Title>
<input onChange={(e) => setInput(e.target.value)} value={input}/>
<button onClick={() => addTodo()}>追加</button>
{isLoading ?
<Loading>loading</Loading>
:
<TodoContainer>
{/* todoListという変数とdeleteTodoという関数をpropsとしてTodoコンポーネントに渡している*/}
<SubContainer>
<SubTitle>未完了</SubTitle>
<Todo todoList={todoList} deleteTodo={deleteTodo} changeTodoStatus={finishTodo} type="todo"/>
</SubContainer>
<SubContainer>
<SubTitle>完了済み</SubTitle>
<Todo todoList={finishedList} deleteTodo={deleteFinishTodo} changeTodoStatus={reopenTodo} type="done"/>
</SubContainer>
</TodoContainer>
}
</div>
);
};
export default App;
const Title = styled.p`
font-size: 26px;
color: #0097a7;
letter-spacing: 2.8px;
font-weight: 200;
`;
const SubTitle = styled.p`
font-size: 22px;
color: #5c5c5c;
`;
const SubContainer = styled.div`
width: 400px;
`;
const TodoContainer = styled.div`
display: flex;
flex-direction: row;
width: 80%;
margin: 0 auto;
justify-content: space-between;
`;
const Loading = styled.div`
margin: 40px auto;
`;
これでリロードしてもデータが消えなくなりました😇
やったね😂
firebaseにデプロイしてみる
今のままではローカル環境でしか動かないのでfirebaseにホスティングさせて外部からも見れるようにしてみましょう
プロジェクトの配下で操作してください
> firebase init
hosting
を選択してスペースを押す→Enter
Use an exsisting Project
を選択
先ほど作成したプロジェクトを選択
What do you want to use as your public directory?
→ build
と入力してEnter
Configure as a single-page app (rewrite all urls to /index.html)?
→y
と入力してEnter
こうなればOK
あとはビルドしてデプロイするだけ!
> yarn build
> firebase deploy
URLが表示されるのでアクセスすれば完成!!
お疲れさまでした
https://todolist-bc9ed.firebaseapp.com/
firebaseのログイン機能を使用してユーザー登録をすればユーザー毎のTodoを保存できるようになるのでぜひ挑戦してみてください〜!
質問&カスタマイズタイム
おまけ
Material UIを使ってちょっとマシなデザインにしてみよう
> yarn add @material-ui/core
> yarn add @material-ui/icons
import React, { useState, useEffect} from 'react'; // 修正
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import './App.css';
import Todo from './pages/Todo';
import styled from 'styled-components';
import firebase from 'firebase'; // 追記
import 'firebase/firestore'; // 追記
function App() {
// 第1変数がstate, 第2変数がstateを変化させる関数
const [input, setInput] = useState('');
const [todoList, setTodoList] = useState([]);
const [finishedList, setFinishedList] = useState([]);
// Loadingを判定する変数
const [isLoading, setIsLoading] = useState(true);
// 未完了のTodoが変化したかを監視する変数
const [isChangedTodo, setIsChangedTodo] = useState(false);
// 完了済みのTodoが変化したかを監視する変数
const [isChangedFinished, setIsChangedFinished] = useState(false);
const db = firebase.firestore(); // 追記
// 追記 一番最初にfirestoreからデータを取ってきてstateに入れる
useEffect(() => {
(async () => {
const resTodo = await db.collection("todoList").doc("todo").get();
// stateに入れる
setTodoList(resTodo.data().tasks);
const resFinishedTodo = await db.collection("todoList").doc("finishedTodo").get();
// stateに入れる
setFinishedList(resFinishedTodo.data().tasks);
// Loading終了
setIsLoading(false);
})()
}, [db])
useEffect(() => {
if (isChangedTodo) {
(async () => {
// 通信をするのでLoadingをtrue
setIsLoading(true)
const docRef = await db.collection('todoList').doc('todo');
docRef.update({ tasks: todoList })
// Loading終了
setIsLoading(false)
})()
}
}, [todoList, isChangedTodo, db])
useEffect(() => {
if (isChangedFinished) {
(async () => {
// 通信をするのでLoadingをtrue
setIsLoading(true)
const docRef = await db.collection('todoList').doc('finishedTodo');
docRef.update({ tasks: finishedList })
// Loading終了
setIsLoading(false)
})()
}
setIsChangedFinished(false)
}, [db, finishedList, isChangedFinished])
const addTodo = async () => {
if (!!input) {
// 追記 Todoが変化したのでtrue
setIsChangedTodo(true);
setTodoList([...todoList, input]);
setInput('');
}
}
const deleteTodo = (index) => {
// 追記 Todoが変化したのでtrue
setIsChangedTodo(true);
setTodoList(todoList.filter((_, idx) => idx !== index))
}
const deleteFinishTodo = (index) => {
// 追記 完了済みTodoが変化したのでtrue
setIsChangedFinished(true);
setFinishedList(finishedList.filter((_, idx) => idx !== index))
}
const finishTodo = (index) => {
// 追記 Todo、完了済みTodoがともに変化したのでtrue
setIsChangedTodo(true);
setIsChangedFinished(true);
deleteTodo(index)
setFinishedList([...finishedList,todoList.find((_, idx) => idx === index)])
}
const reopenTodo = (index) => {
// 追記 Todo、完了済みTodoがともに変化したのでtrue
setIsChangedTodo(true);
setIsChangedFinished(true);
deleteFinishTodo(index)
setTodoList([...todoList,finishedList.find((_, idx) => idx === index)])
}
return (
<div className="App">
<Title>Todoリスト</Title>
<TextField onChange={(e) => setInput(e.target.value)} value={input}/>
<Button variant="contained" color="primary" onClick={() => addTodo()}>追加</Button>
{isLoading ?
<Loading>loading</Loading>
:
<TodoContainer>
{/* todoListという変数とdeleteTodoという関数をpropsとしてTodoコンポーネントに渡している*/}
<SubContainer>
<SubTitle>未完了</SubTitle>
<Todo todoList={todoList} deleteTodo={deleteTodo} changeTodoStatus={finishTodo} type="todo"/>
</SubContainer>
<SubContainer>
<SubTitle>完了済み</SubTitle>
<Todo todoList={finishedList} deleteTodo={deleteFinishTodo} changeTodoStatus={reopenTodo} type="done"/>
</SubContainer>
</TodoContainer>
}
</div>
);
};
export default App;
const Title = styled.p`
font-size: 26px;
color: #0097a7;
letter-spacing: 2.8px;
font-weight: 200;
`;
const SubTitle = styled.p`
font-size: 22px;
color: #5c5c5c;
`;
const SubContainer = styled.div`
width: 400px;
`;
const TodoContainer = styled.div`
display: flex;
flex-direction: row;
width: 80%;
margin: 0 auto;
justify-content: space-between;
`;
const Loading = styled.div`
margin: 40px auto;
`;
Todo/index.jsx
import React from 'react';
import styled from 'styled-components';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from '@material-ui/icons/Delete';
// todoListという変数とdeleteTodoという関数をpropsとして受け取る
const Todo = ({todoList, deleteTodo, changeTodoStatus, type}) => (
<div>
{/*受け取ったtodoListを使って表示する*/}
{todoList.map((todo, idx) => (
<Container key={todo}>
{todo}
<IconButton aria-label="delete">
<DeleteIcon fontSize="small" onClick={() => deleteTodo(idx)} />
</IconButton>
<Button variant="outlined" color={type === "todo" ? "primary" : "secondary"} onClick={() => changeTodoStatus(idx)}>{type === "todo" ? "完了済みにする" : "戻す"}</Button>
</Container>
))}
</div>
);
export default Todo;
const Container = styled.div`
color: #5c5c5c;
letter-spacing: 1.8px;
`;
ちょっとマシになったよね?笑
ちょっと頑張ればこんな感じにも(まだダサいのは許して)

完成品
https://todolist-bc9ed.web.app/
ソースはここにおいておきます
ログイン機能とか作りたくてもわからんって人は気軽に言ってね😊
https://github.com/mr04vv/ReactTodoHandsOn/tree/custom