KAEDE Hack blog

JavaScript 中心に ライブラリなどの使い方を解説する技術ブログ。

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で置き換えて使用しようと思う

f:id:kei_s_lifehack:20200414071901p:plain

教材としてはYoutubeでwesのTutorialをベースとして使ったが
databaseからfirestoreに変えるためにDjamWareを参考にした.

www.youtube.com

環境構築

まずはnpmで汎用ツールをぶち込む

npm i -g create-react-app
create-react-app reactnotes

結構時間がかかる

cd reactnotes
npm start

でテスト,browserで動けばOK

あとnpm -i --save firebaseでbackendのfirebase 入れる

f:id:kei_s_lifehack:20200410150928p:plain

作成されたここのsrc/にYoutubeの通りにjsxを書いていく.

github.com

code

Component

React Component の記事に移設

, , , に分割して, AppでNoteとNoteFormを呼び出し,indexで総括する.


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,

github.com

ここのCSSをpaste.

使用する'./Static/img/2.jpeg'をpaste....
なかったの

代わりにpexelsのこれを使わせてもらいStatic/img/作ってぶち込んだ

https://www.pexels.com/photo/when-will-you-return-signage-1749057/

すると見栄えがよくなった

f:id:kei_s_lifehack:20200414070810p:plain

add Google Font

Parmanent Markerを選択 f:id:kei_s_lifehack:20200414071106p:plain

Latoも選択してimportのlinkをもらう f:id:kei_s_lifehack:20200414071342p:plain

そして,index.cssを全て消してこのimportだけに書き換える! これで筆っぽいheadingは表示される.

add Note.css

お次はNote.cssをpaste....

完璧に組まれた!!!

f:id:kei_s_lifehack:20200414071901p:plain

次にFirebaseの処理に移る.

Back End

config.js

Firebaseを使う.configはsrc/Config/config.jsに記述.
ここにFirebase Console のsettingからadd web app, Configでcopyする

f:id:kei_s_lifehack:20200418202100p:plain

これを先ほど作った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してる?エラーが出る

kei-s-lifehack.hatenablog.com

別の記事にまとめた.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()までだす!

https://www.djamware.com/post/5bc50ea680aca7466989441d/reactjs-firebase-tutorial-building-firestore-crud-web-application

ここに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 |             )

タグを複数返すとエラーが出た.

だそうだ,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>
                )
              }
            )
          }

f:id:kei_s_lifehack:20200426080618p:plain

ようやくfirestoreからhtmlに出せた.

なおhtmlのclassを壊してるからレイアウトがない

Youtubeではに渡してNoteでrenderしてたから,それを真似る必要がある

また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だそうだ.下記のリンクを見てみると

fb.me

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の値が綺麗に表示された!!

f:id:kei_s_lifehack:20200426080618p:plain

f:id:kei_s_lifehack:20200428151925p:plain

次は追加の処理を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にした

f:id:kei_s_lifehack:20200429160255p:plain

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のみ表示する

f:id:kei_s_lifehack:20200503114225p:plain

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)}>
                      &times;
                </span>
    handleRemoveNote(id){
        this.props.removeNote(id);
    }

idを渡してremoveNoteする

database.on(child_removed...になっているので,dJamの テキストを参考に...わからん

firebase公式だと

firebase.google.com

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 |         &times;
  26 |     </span>

わからない, idが渡せない