React + Firebase Note App Tutorial(Wes's) dev log
Did React + Firebase Note App Tutorial
why
jQとFirebaseで家計簿アプリ作ってたらG-stylussの逆求人もくもくのイベントで
話した学生全員にReact使って書けって言われた
cakeのledgerで改善点探してたらだいたいUIだったからメンテしやすいらしいReactというFront End Frame workを試してみる.
ledgerはdeployできてないので,簡単にできると聞いたfirebaseと相性良さそうなreactを選択,
とりあえずはYoutube Premirem契約してるし,この動画の通りにやって,自分の言葉で解説をかく学習方法を取ってみる.
しかし現在は推奨されていないRDBを使っていたので,Firestoreで置き換えて使用しようと思う
教材としてはYoutubeでwesのTutorialをベースとして使ったが
databaseからfirestoreに変えるためにDjamWareを参考にした.
環境構築
まずはnpmで汎用ツールをぶち込む
npm i -g create-react-app create-react-app reactnotes
結構時間がかかる
cd reactnotes npm start
でテスト,browserで動けばOK
あとnpm -i --save firebaseでbackendのfirebase 入れる
作成されたここのsrc/にYoutubeの通りにjsxを書いていく.
code
Component
React Component の記事に移設
props
Noteのconstructorに直接propsとして値を埋め込む方法がある.
super(props); this.message = 'Hello from Note Component'; this.noteContent = props.noteContent; this.noteId = props.noteId;
super(props)の下にnoteContent, noteIdを定義してpropsの下から this(このclass?)のに付け足す.
CSS, className
render(props) { return( <div className = 'note fade-in'> <p className="noteContent"> {this.noteContent} </p> </div> ) }
ReactではhtmlのclassはclassNameと書く必要がある.
render配下にさっき作ったthis.noteContentをうめ込む. これでNote Componentの中のnoteContentがrenderされる
propTypes
importしてあるProp Typesで型のチェックができる
Note.propTypes = { noteContent: PropTypes.string }
これで中身がstringでなければ警告が出る
参考
qiita.com
propsはコンポーネント作成時に値を指定することでコンポーネントで表示させたいデータを指定できます。 React.jsでコンポーネントを定義する時に、PropTypesを指定することでpropsにおける引数の入力チェックを行えます。 数値や文字列、配列などのバリデーションを行いたい時に便利です。
add App.css, No Static img,
ここのCSSをpaste.
使用する'./Static/img/2.jpeg'
をpaste....
なかったの
代わりにpexelsのこれを使わせてもらいStatic/img/作ってぶち込んだ
https://www.pexels.com/photo/when-will-you-return-signage-1749057/
すると見栄えがよくなった
add Google Font
Parmanent Markerを選択
Latoも選択してimportのlinkをもらう
そして,index.cssを全て消してこのimportだけに書き換える! これで筆っぽいheadingは表示される.
add Note.css
お次はNote.cssをpaste....
完璧に組まれた!!!
次にFirebaseの処理に移る.
Back End
config.js
Firebaseを使う.configはsrc/Config/config.js
に記述.
ここにFirebase Console のsettingからadd web app, Configでcopyする
これを先ほど作ったconfig.jsにpasteして,
const firebaseConfig = { apiKey: "hoge"; ...... };
のconst firebaseCofig = をexport const DB_CONFIG =
に書き換える
これでimportしたDB_CONFIG
をAppで
import { DB_CONFIG } from './Config/config';
して使用する.
firebase/app
import firebase from 'firebase/app';
そしてfirebaseもimportするが,これだけでは
index.js:1 ./src/App.js Module not found: Can't resolve 'firebase/app' in '/Users/kaede/code/note-app/reactnotes/src'
のerrが出てしまう.
Youtuberのgithubのpackage.jsonとくらべたらfirebaseをnpm i --saveしてなかった!
したら
Line 5:10: 'DB_CONFIG' is defined but never used no-unused-vars Line 6:8: 'firebase' is defined but never used no-unused-vars
の「DBもfirebaseのAPI?も使われてないよ〜」のwarnしか出なくなった!!
Appのconstructorのナカに
this.app = firebase.initializeApp(DB_CONFIG); this.db = this.app.database().ref().child('notes');
DB_CONFIGの内容でappをinitして,そこからDBのnotesテーブルを読みこむ.
component mount
ここが一番わからない
componentWillMount() { const previousNotes = this.state.notes; }
componentWillMountのナカにaddNoteに入れていたprev notesを入れる
this.database.on('child_added', snap => { previousNotes.push({ id: snap.key, noteContent: snap.val().noteContent, }); this.setState({ notes: previousNotes, }) })
さらにdbに子供が追加された時に,一時変数snapをとって
event drivenで関数を実行する処理を書く.
idには引数snapのkey, contentには引数の中身のnoteContentを
jQでよく使うvalを使って入れて,prevNotesにpushする.
そしてprevNotesをstateのnotesに組み込む.
変数prevNotesを噛ませるのは最初は冗長に見えたが,こうして処理が複雑に
なってくるとstateへ渡す処理をこうして分離するのは合理的に見えてくる.
duplicate err
なおこの段階でimport firebaseをした時に,
Firebase: Firebase App named '[DEFAULT]' already exists (app/duplicate-app).
とimportがduplicateしてる?エラーが出る
別の記事にまとめた.configl fileのやり方が変わっていたらしい.
その上でjQのアプリの時のようにfirestoreの解決策を取ろう.
add note
さっきのcomponentWillMountの処理で既存のaddNoteの処理は空になったので
そこに書き足していく.
databaseがあった時のやり方だと.on
でevent drivenができたが,firestoreには
.onが存在しないのでerrになる.
db.pushもfirestoreではerrになる....
collection
なので
let db = firebase.firestore();
db.collection("notes").get(). then(function(querySnapshot) {
のようにfirestoreの解決策を取る.
なお.get()までではcllで出しても
Promise {<pending>} __proto__: Promise [[PromiseStatus]]: "resolved" [[PromiseValue]]: t
promiseが帰ってきてしまう.
thenでsnapShotを返しても
lm rm ...
とかが帰ってくるので
this.db.collection('notes').get() .then(snapShot => { snapShot.forEach(doc => { console.log(doc.data(),doc.id); }); });
ここまでやらないとtableの配列は入手できない!!!
tableからgetして,thenでpromiseをforeachした後にdoc.data()までだす!
ここにcollectionを用いたcrudが書いてあった。 ここを参考にすると下記のようになる
Appのconstructor内に
this.ref = firebase.firestore().collection('notes'); this.unsubscribe = null; this.state = {notes: []};
を書きnotesのpromse?をrefに入れる.
unsubscribeは不明.更新用の場所?
そしてstateに空のnotes配列を作成
constructorの後に
onCollectionUpdate = (querySnapShot) => { const notes = []; querysnapShot.forEach(doc =>{ const noteContent = doc.data(); notes.push({noteContent: doc}) }); this.setState({notes}); }
というupdateの関数を作り,querySnapshotを引数にとり,
componentDidMount() { this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate); }
componentDidMountでthis.unsubscribeにさっきのrefがsnapShotされる時?をとる。そこで onCollectionUpdateでは boards配列を作って、doc.data()を title, desc, auther, の変数に入れdocとkeyもいれ boardsにpushしてsetStateする。
このunsubからのonSnapshotでのonCollectionUpdateが,前回のon child addedを 代用しているように見える.
しかしsetStateのところで
Error: Objects are not valid as a React child (found: object with keys {id, noteContent}). If you meant to render a collection of children, use an array instead. in h1 (at App.js:69) in div (at App.js:64) in div (at App.js:58) in App (at src/index.js:9) in StrictMode (at src/index.js:8) ▶ 25 stack frames were collapsed. App.onCollectionUpdate [as next] src/App.js:28 25 | noteContent, 26 | }); 27 | }); > 28 | this.setState({notes}); | ^ 29 | } 30 | componentWillMount() { 31 | this.unsubscribe = this.ref.onSnapshot(
のエラーが出る. このままでは使えないようだ
https://reactjs.org/docs/lists-and-keys.html
をみてさらにフォロワー様に教えてもらって直せた
react public document
function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => <li key={number.toString()}> {number} </li> ); return ( <ul>{listItems}</ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
として<li key = {hoge.toString() } >{ hoge } </li>
でtagのナカに入れて,stringにしてる.
また,mapの結果自体を変数に入れて,returnは変数一つにしている
とりあえず
this.state.notes.map( (note) => { return( <h1>{note.key}</h1> ) } )
Line 70:19: Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>? 68 | return( 69 | <h1 key= {note.key}>{note.key}</h1> > 70 | <h2 key= {note.key}>{note.noteContent}</h2> | ^ 71 | ) 72 | } 73 | )
タグを複数返すとエラーが出た.
1つのコンポーネントが返せる Element の親は1つだけです
— やぎちゃん (@ygkn35034) April 25, 2020
複数返したいときは
<>
<Hoge />
<Fuga />
</>
のように <> </> (React.Framentの糖衣構文)で囲ってあげなきゃいけません
だそうだ,returnするもの自体にdivで括るsyntax sugarらしい
これで出せたが,console.logしてみるとnoteContentのナカにnoteContentが入った
{key: "notes", doc: n, noteContent: {…}} key: "notes" doc: n {lm: t, um: t, Rm: n, Am: false, Pm: false, …} noteContent: {id: 1, noteContent: "hoge"} __proto__: Object
入れる際の階層構造を正した
querySnapshot.forEach(doc =>{ const noteContent = doc.data().noteContent; notes.push({ id: doc.id, doc, noteContent, }); });
ただこれでもidがnotesになってるから,idにもdoc.data()のさらに下の
doc.data().idを入れる.(実装としておかしいか?)
これでとりあえず,firestoreの値をReactのstateに入れてcllすることができた.
ここでもkeyがないerrが出るが,return で返しているのはdivなので,divに入れたら 解決した
correct code (render)
{ this.state.notes.map( (note) => { console.log(note); return( <div key= {note.id.toString()}> <h1> {note.id}, {note.noteContent} </h1> </div> ) } ) }
ようやくfirestoreからhtmlに出せた.
なおhtmlのclassを壊してるからレイアウトがない
Youtubeでは
またid順に読み込まれたりはしていないからfilterも必要
componentWillMountで書いていると警告がでた
react-dom.development.js:88 Warning: componentWillMount has been renamed, and is not recommended for use. See https://fb.me/react-unsafe-component-lifecycles for details. * Move code with side effects to componentDidMount, and set initial state in the constructor. * Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder. Please update the following components: App
not recommendだそうだ.下記のリンクを見てみると
Unsafeになってるから
npx react-codemod rename-unsafe-lifecycles
でrenameできるらしい.だが新しいのどう使うか書いてないし,全然わからん放置.
まぁ最終的にはhookで書き直すからヨシ!
return( <div key= {note.id.toString()}> <Note noteContent= {note.noteContent} noteId={note.id} key={note.id} /> </div> )
Noteにkeyを渡す方式でいく
無事にfirestoreの値が綺麗に表示された!!
次は追加の処理をfirebase/databaseから ../firestoreに置き換える
ADD
constructorの作成
dJam
import firebase from '../Firebase'; ... class Create extends Component { constructor() { super(); this.ref = firebase.firestore().collection('boards'); this.state = { title: '', description: '', author: '' }; } .........
djamではCreateとして作成している, propsは渡していない.
さらにstateにtitle, desc ,author,をカラで初期化.
Wes
class NoteForm extends Component { constructor(props) { super(props); this.state = { newNoteContent: 'New Note Content', } this.handleUserInput = this.handleUserInput.bind(this); this.writeNote = this.writeNote.bind(this); } ...
一方Wesのではpropsを渡して,newNoteContentのみを初期化し
handleとwriteをbindしている
propsを渡しているのはthis.props.addNoteを受け取るためか.
kaede
Appと同じようにここNoteFormでもthis.ref...でnotes tableから持ってきた.
writeNoteは置いておいて,AppのaddNoteだけ書き換える.
Wes
Wesのはrenderに
<input ... onChange = {this.handleUserInput} /> <btn ... onClick = {this.writeNote} >...
と指定ElementでにonChange, onClick時に関数を発火させているが
dJam
<input type="text" class="form-control" name="title" value={title} onChange={this.onChange} placeholder="Title" />
dJamではonHogeで直接処理を書き込んでいる.なのでbindする必要がないバウ.だからonChangeにthis.onChangeを入れている
dJam
handle input関数,onChange
dJam
onChange = (e) => { const state = this.state state[e.target.name] = e.target.value; this.setState(state); }
stateの指定のものに入れるのではなく,stateの配列名を指定して, そこにtarget.valueを入れて,それをsetStateしている
Wes
handleUserInput(e) { this.setState({ newNoteContent : e.target.value, }) }
userが入れるところはnoteの中身だけなので,変数噛ませないで直接 this.setStateで入れるところを指定して入れている.
よく見るとonChange(e)でとる方が再利用せいが高い.
writeNote, onSubmit
Wes
writeNote(){ this.props.addNote(this.state.newNoteContent); this.setState({ newNoteContent: '', // empty input box }) } ......... addNote(note) { this.db.push().set( { noteContent: note } ) }
writeNoteではaddNoteに値を渡すのと,inputをemptyにするだけの処理になっている
dJam
onSubmit = (e) => { e.preventDefault(); const { title, description, author } = this.state; this.ref.add({ title, description, author }).then((docRef) => { this.setState({ title: '', description: '', author: '' }); this.props.history.push("/") }) .catch((error) => { console.error("Error adding document: ", error); }); }
一方dJamではstateの各値をtitle, desc, author,に分割代入し,
this.refに全て追加してhistoryに/を追加している,謎
またerr字の処理も書いている
render() { const { title, description, author } = this.state;
renderでも同様にしてstateから取り出している
kaede
wesの作品ではnoteにはnoteContentのdataしかなくて,idはarrの大きさ+1にした
DBへの追加完了!!
UX的にはGoogle DocsやkeepみたいにonChangeでDB自体の変更もやってみると 面白いかもしれない
またaddNoteでpush処理をしていて,NoteFormはaddNoteにnewNoteContentを 渡すだけなので,NoteFormにはthis.refは必要がなかった.
ここでloginしたuserごとにpushできないと完成にならない...と頭を抱えたが,とりあえずはlogin無視で一つのtableでRead, Create, Update, Edit, Deleteできるようにする.
連番idとcreated_at
arrのlength + 1を追加する分のidとしていたら
idやキーを配列の長さで取ると削除機能実装時に死ぬ
と教えてもらって代わりにcreated_atをkeyとする方式をとった
App.js > render
<div className="notesBody"> { this.state.notes.map( (note) => { console.log(note); return( <Note noteContent= {note.noteContent} key={note.created_at} /> ) } ) } </div>
でこれでstateのcreated_atをkeyとして渡して
Note.jsx > render
<p className="noteContent"> {this.noteContent} </p>
これでnoteContentのみ表示する
firestoreで const created_at = new Date(); で送ると このようにtimestampで保存される
delete
ШесのYoutubeでは
this.database.on('child_removed', snap => { for(var i=0; i < previousNotes.length; i++){ if(previousNotes[i].id === snap.key){ previousNotes.splice(i, 1); } }
と
removeNote(noteId){ console.log("from the parent: " + noteId); this.database.child(noteId).remove(); }
と
<Note noteContent={note.noteContent} noteId={note.id} key={note.id} removeNote ={this.removeNote}/>
になっている. removeNoteのbtnをcontentと共に出して
<span className="closebtn" onClick={() => this.handleRemoveNote(this.noteId)}> × </span>
handleRemoveNote(id){ this.props.removeNote(id); }
idを渡してremoveNoteする
database.on(child_removed...になっているので,dJamの テキストを参考に...わからん
firebase公式だと
db.collection("cities").doc("DC").delete().then(function() { console.log("Document successfully deleted!"); }).catch(function(error) { console.error("Error removing document: ", error); });
docのidを渡せばいいことがわかる.
このためにnotesの中身だけじゃなくてdoc自体もrefに入れておく必要があったのか
idは
this.state.notes.map( (note) => { console.log(note.doc.id);
で
3YMpsV2NxfXWYWThpCl4 App.js:78 AS5v0YCUxqHX5cjeCeUR App.js:78 Kds0WUyMGROVwV55frEE App.js:78 zVFi3oudXwQG3vGXsVGa App.js:78 3YMpsV2NxfXWYWThpCl4 App.js:78 AS5v0YCUxqHX5cjeCeUR App.js:78 Kds0WUyMGROVwV55frEE App.js:78 zVFi3oudXwQG3vGXsVGa
consoleにidが出てくる.担っている担っている
states.notesをmapのあと
でNoteにstateのidを渡す
Noteのconstructorで受け取る
this.id = props.id;
× TypeError: Cannot read property 'doc' of undefined removeNote src/App.js:62 59 | } 60 | removeNote(id) { 61 | console.log(id); > 62 | this.ref.doc(id).delete.then(function() { | ^ 63 | console.log('deleted'); 64 | }).catch(function(error) { 65 | console.error('error!!!!',error); View compiled Note.handleRemoveNote src/Note/Note.jsx:16 13 | this.handleRemoveNote = this.handleRemoveNote.bind(this); 14 | } 15 | handleRemoveNote(id) { > 16 | this.props.removeNote(id); | ^ 17 | console.log('remove',id); 18 | } 19 | render(props) { View compiled onClick src/Note/Note.jsx:23 20 | return( 21 | <div className = 'note fade-in'> 22 | <span className='clsebtn' > 23 | onClick={()=> this.handleRemoveNote(this.id)} | ^ 24 | > 25 | × 26 | </span>
わからない, idが渡せない