【React】StateHooksを永続化してみる

画面をリロードしてもStateの内容が消えないように、ローカルストレージを利用して簡易メモアプリを作ってみました。

環境の準備

実行環境

  • VS code:v1.62.3
  • Node.js:v14.17.3
  • React:v17.0.2

プロジェクトの作成

React Create Appで作成します。

React Create Appで作成されたソースの変更

  • 15行目でbootstrapをCDNで読み込んでいます。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
    name="description"
    content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" crossorigin="anonymous" >
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
  • 18行目のMemoPageを読み込むだけです。
import './App.css';
import { MemoPage } from './memo_comp';

const App = () => {
  return (
    <div>
      <h1 className='bg-primary text-white display-4'>React</h1>
      <div className='container'>
        <h4 className='my-3'>Memo.</h4>
        <MemoPage />
      </div>
    </div>
  )
}

export default App

簡易メモアプリの作成

ローカルストレージにStateを永続化するJSファイルの作成

  • useStateを使わず、usePersistで定義するとローカルストレージに保存されるようになります。
import React, {useState} from 'react';

const usePersist = (ky, initVal) => {
    const key = 'hooks' + ky
    const value = () => {
        try {
            const item = window.localStorage.getItem(key)
            return item ? JSON.parse(item) : initVal
        } catch (err) {
            console.log(err)
            return initVal
        }
    }
    const setValue = val => {
        try {
            setSavedValue(val)
            window.localStorage.setItem(key, JSON.stringify(val))
        } catch (err) {
            console.log(err)
        }
    }
    const [savedValue, setSavedValue] = useState(value)
    return [savedValue, setValue]
}

export default usePersist

./memo_comp配下に作成するJSファイル

  • memo_compフォルダ配下のexport設定をindex.jsに書きます。
export {default as MemoPage} from './MemoPage'
export {default as AddForm} from './AddForm'
export {default as FindForm} from './FindForm'
export {default as DeleteForm} from './DeleteForm'
export {default as Memo} from './Memo'
export {default as Item} from "./Item"
  • 5行目は、各コンポーネントのボタン押下した場合に、10行目をrenderします。
  • 6行目は、AddForm(12行目)で追加した内容をMemo(16行目)に反映します。
  • 7行目は、FindForm(13行目)で検索した内容をMemo(16行目)に反映します。
import usePersist from '../Persist';
import { AddForm, FindForm, DeleteForm, Memo } from './';

const MemoPage = () => {
    const [mode, setMode] = usePersist('mode', 'default')
    const [memo, setMemo] = usePersist('memo', [])
    const [fmemo, setFMemo] = usePersist('findMemo', [])
    return (
        <div>
            <h5 className='my-3'>mode: {mode}</h5>
            <div className='alert alert-primary pb-0'>
                <AddForm setMode={setMode} memo={memo} setMemo={setMemo} />
                <FindForm setMode={setMode} memo={memo} setFMemo={setFMemo} />
                <DeleteForm setMode={setMode} memo={memo} setMemo={setMemo}/>
            </div>
            <Memo mode={mode} memo={memo} fmemo={fmemo} />
        </div>
    )
}

export default MemoPage
import React, {useState} from "react"

const AddForm = props => {
    const [message, setMessage] = useState('')
    const doChange = e => {
        setMessage(e.target.value)
    }
    const doAction = e => {
        props.setMode('add')
        const data = {
            message: message,
            created: new Date()
        }
        let copy_memo = props.memo.concat()
        copy_memo.unshift(data)
        props.setMemo(copy_memo)
        setMessage('')
    }
    return (
        <form>
            <div className='form-group row'>
                <input type='text' className='form-control-sm col'
                    onChange={doChange} value={message} required />
                <input type='button' onClick={doAction} value='Add' className='btn btn-primary btn-sm col-2' />
            </div>
        </form>
    )
}

export default AddForm
import React, { useState } from "react"

const FindForm = props => {
    const [message, setMessage] = useState('')
    const doChange = e => {
        setMessage(e.target.value)
    }
    const doAction = e => {
        props.setMode('find')
        if (message == '') {
            props.setFMemo(props.memo)
            return //空白の場合、全件表示
        }
        let res = props.memo.filter((item, key) => {
            return item.message.includes(message)
        })
        props.setFMemo(res)
        setMessage('')
    }
    return (
        <form>
            <div className='form-group row'>
                <input type='text' onChange={doChange}
                    value={message} className='form-control-sm col' />
                <input type='button' onClick={doAction} value='Find' className='btn btn-primary btn-sm col-2' />
            </div>
        </form>
    )
}

export default FindForm
import React, {useState} from "react"

const DeleteForm = props => {
    const [num, setNum] = useState(0)
    const doChange = e => {
        setNum(e.target.value)
    }
    const doAction = e => {
        props.setMode('delete')
        let res = props.memo.filter((item, key) => {
            return key != num //削除対象以外を抽出
        })
        props.setMemo(res)
        setNum(0)
    }
    let items = props.memo.map((value, key) => //{}付けない
        <option key={key} value={key}>
            {value.message.substring(0,10)}
        </option>
    )
    return (
        <form>
            <div className='form-group row'>
                <select onChange={doChange} className='form-control-sm col' defaultValue='-1'>
                    {items}
                </select>
                <input type='button' onClick={doAction} value='Del' className='btn btn-primary btn-sm col-2' />
            </div>
        </form>
    )
}

export default DeleteForm
  • 8と13行目は、fmemoを使っているかどうかです。switch分岐でなくてもよかったです。
import React from "react"
import {Item} from "./"

const Memo = props => {
    let data = []
    switch (props.mode) {
        case 'find':
            data = props.fmemo.map((value, key) => //{}付けない
                <Item key={value.message} value={value} index={key + 1} />
            )
            break
        default:
            data = props.memo.map((value, key) => //{}付けない
                <Item key={value.message} value={value} index={key + 1} />
            )
    }
    return (
        <table className='table mt-4'>
            <tbody>{data}</tbody>
        </table>
    )
}

export default Memo
import React from "react"

const Item = props => {
    const th = {
        width: '100px'
    }
    const td = {
        textAlign: 'right',
        width: '150px'
    }
    let d = new Date(Date.parse(props.value.created))
    let f = (d.getMonth()+1) + '/' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes()
    return (
        <tr>
            <th style={th}>No, {props.index}</th>
            <td>{props.value.message}</td>
            <td style={td}>{f}</td>
        </tr>
    )
}

export default Item

あとがき

特になし

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です