5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GoogleKeep似アプリをReactで作ってみた

Last updated at Posted at 2019-10-16

はじめに

今月から自社開発系のフロントエンジニアとして働いています!
前職はSIerで、Reactは使ったことがありませんでした。そこで、入社前にReactの練習がてら、Google Keepを模したメモアプリを作ってみました!

このメモアプリの機能は、
・ メモ作成
・ 作成されたメモをjson-serverに保存
・ 作成されたメモの文章・色の編集
・ メモ削除
です。
非同期やpropsの理解もできるので、React初心者のチュートリアルとして参考にしてください。

環境構築

プロジェクト作成

以下コマンドで、reactのプロジェクトを作成します。

$ npx create-react-app my-app

$ create-react-appとは、Reactのアプリケーションの雛形を作れるコマンドラインツールです。my-appはプロジェクトの名前になるので適宜変更してください。
プロジェクトが作成できたら以下コマンドでプロジェクトを動かします。

$ cd my-app
$ npm start

以下の画面が出てくれば成功です。
image.png

さて、今回はMaterial Iconを使うのでそのセットアップとして、以下の<link>public/index.htmlの12行目に追加してください。

public/index.html

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

json-serverインストールと設定

後ほど使うjson-serverをインストールしておきます。json-serverとは、jsonを使うことでバックエンドの開発不要でAPIが使えるAPIモックアップです。

$ npm install -g json-server

そして、プロジェクト直下にdb.jsonを作成します。

db.json
{
    "tasks": [
      {
        "content": "Qiita!",
        "color": "#f28b82",
        "id": 2
      }
    ]
  }

json-serverの実行コマンドは以下です。今から起動しておいてもOKです。起動していないと、これから作るアプリは正常な動きをしません。

$ json-server --watch db.json

メモアプリのフォルダ構成

├── App.js
├── App.scss
├── components
│   ├── Card
│   │   ├── Card.js
│   │   ├── Card.scss
│   │   └── index.js
│   └── Form
│       ├── Form.js
│       ├── Form.scss
│       └── index.js
├── icon.png
├── index.js
├── index.scss
└── serviceWorker.js

  • Formフォルダ

Formは、作成するメモを入力する部分です。
image.png

  • Cardフォルダ

Cardとは、作成したメモ部分です。
image.png

  • 補足

今回はindex.jsをFormとCardフォルダ配下にそれぞれ作成しています。これは、App.jsでFormとCardをimportする際、
import Card from "./components/Card/Card.js";ではなく、
import Card from "./components/Card/Card";と書くことができるためです。

メモアプリの実装

さて、ここからは実装に入ります。

不要ファイル・不要ソースコードの削除

不要ファイルは以下なので削除します。
App.css ←あとでApp.scssを作成します

App.test.js
logo.svg
serviceWorker.js

コンパイルエラーが出てくると思いますが、エラー文に書かれてることを対処すればOKです。
例えば、下の画像のエラーだったら、エディタのプロジェクト内検索で、serviceWorkerを検索し、該当箇所を削除します。他の削除した要素についても同様に、プロジェクト内検索をし該当箇所を削除します。
image.png

 App.jsとApp.scssの作成

まず、App.jsとApp.scssを作成します。
App.jsは一番始めに読み込まれるjsなので、ここにページ表示させる土台を書いていきます。

App.jsに出てくるfetchTasks changeText addMemo deleteMemo editMemo は、これから作るForm.jsとCard.jsそれぞれで使うため、App.jsにて定義をしています。(App.jsに定義することで、Form.jsとCard.jsの両方で処理を記述する必要がありません。App.jsからFormとCardにpropsを引き渡しさえすればOKです。)また、onClickイベントでthisを用いて処理が走りますが、その際にthisが使えるように、constructor内でbind(this)を行います。

asyncを使った非同期処理については、componentDidMount()fetchTasks()を呼び、db.jsonにあるデータを取ってきます。そしてAddMemo()methodPOSTにしてdb.jsonを更新しています。

src/App.js
import React from "react";
import "./App.scss";
import Form from "./components/Form/Form";
import Card from "./components/Card/Card";
import icon from "./icon.png";
import ReactDOM from "react-dom";

class App extends React.Component {
  constructor() {
    super();
    this.fetchTasks = this.fetchTasks.bind(this);
    this.changeText = this.changeText.bind(this);
    this.addMemo = this.addMemo.bind(this);
    this.deleteMemo = this.deleteMemo.bind(this);
    this.editMemo = this.editMemo.bind(this);
    this.state = {
      showInput: false,
      black: true,
      color: null,
      tasks: []
    };
  }

  componentDidMount() {
    this.fetchTasks();
  }

  async fetchTasks() {
    await fetch("http://localhost:3000/tasks")
      .then(response => response.json())
      .then(json => {
        this.setState({ tasks: json });
      });
  }

  changeText = event => {
    const inputText = event.target.value;
    this.setState({ inputText: inputText });
  };

  changeFormColor = (color = "#ffffff") => {
    this.setState({
      color: color
    });
  };

  addMemo = () => {
    this.setState({ showInput: false });
    fetch("http://localhost:3000/tasks", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        content: this.state.inputText,
        color: this.state.color
      })
    }).then(this.fetchTasks);
  };

  deleteMemo(taskId) {
    this.setState({ showInput: false });
    fetch("http://localhost:3000/tasks/" + taskId, {
      method: "DELETE"
    }).then(this.fetchTasks);
  }

  editMemo(taskId) {
    this.setState({ showInput: true });
    let input = ReactDOM.findDOMNode(this.refs["text-cell"]);
    input && input.focus();
  }

  saveMemo = (taskId, content, color) => {
    fetch("http://localhost:3001/tasks/" + taskId, {
      method: "PUT",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ content, color })
    });
  };

  render() {
    const list = this.state.tasks.map(task => {
      const showInput = this.state.showInput;
      let input;
      if (showInput) {
        input = (
          <input
            className="input-box"
            onChange={this.changeText}
            key={task.id}
            ref="text-cell"
          />
        );
      } else {
        input = (
          <input
            className="input-box"
            onChange={this.changeText}
            key={task.id}
            value={task.content}
          />
        );
      }

      return (
        <Card
          task={task}
          color={this.state.color}
          tasks={this.state.tasks}
          key={task.id}
          changeText={this.changeText}
          deleteMemo={this.deleteMemo}
          fetchTasks={this.fetchTasks}
          saveMemo={this.saveMemo}
        />
      );
    });
    return (
      <div>
        <div className="header">
          <i className="material-icons">menu</i>
          <img src={icon} className="icon" alt="" />
          <h2>Keep</h2>
        </div>
        <Form
          changeFormColor={this.changeFormColor}
          color={this.state.color}
          tasks={this.state.tasks}
          saveMemo={this.saveMemo}
          changeText={this.changeText}
          addMemo={this.addMemo}
        />
        <div className="list-container">
          <ul>{list}</ul>
        </div>
      </div>
    );
  }
}

export default App;
src/App.scss
.icon {
  width: 40px;
  height: 40px;
  display: inline-block;
}

.header {
  display: flex;
  align-items: center;
  width: 100%;
  margin: 1rem 0;
  border-bottom: solid rgba(0, 0, 0, 0.1);
  border-width: 1px 1px 1px 6px;
}
h2 {
  display: inline-block;
  text-align: center;
  color: #5f6368;
  padding-left: 4px;
  font-size: 22px !important;
  line-height: 24px;
}
i {
  margin: 0 4px;
  padding: 12px;
}

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 40vmin;
  pointer-events: none;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.purple {
  color: #d7aefb;
}
.pink {
  color: #f28b82;
}

ul {
  list-style: none;
  width: 70%;
  padding-left: 0;
}

.list-container {
  justify-content: center;
  align-items: center;
  margin: 40px 130px 16px 130px;
}
button {
  background-color: transparent;
  border: none;
  cursor: pointer;
  outline: none;
  padding: 0;
  appearance: none;
  :focus {
    outline: none;
  }
}
.input-box {
  width: 100%;
}

 Form.jsとForm.scssの作成

Formでは、App.jsで引き渡されてきたpropsを多く使っています。

src/Components/Form/Form.js
import React from "react";
import "./Form.scss";

class Form extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            content: ""
        };
    }

    render() {
        return (
            <div className="form" style={{ backgroundColor: this.props.color }}>
                <div className="form-top">
                    <input
                        className="input-box"
                        placeholder="メモを入力..."
                        onChange={this.props.changeText}
                        style={{ backgroundColor: this.props.color }}
                    />
                </div>
                <div className="form-bottom">
                    <button onClick={() => this.props.changeFormColor()}>
                        <i className="material-icons">color_lens</i>
                    </button>
                    <button onClick={() => this.props.changeFormColor("#f28b82")}>
                        <i className="material-icons pink">color_lens</i>
                    </button>
                    <button onClick={() => this.props.changeFormColor("#d7aefb")}>
                        <i className="material-icons purple">color_lens</i>
                    </button>
                    <button
                        className="input-close"
                        type="submit"
                        onClick={this.props.addMemo}
                    >
                        作成
                    </button>
                </div>
            </div>
        );
    }
}
export default Form;

src/Components/Form/Form.scss
.form {
    justify-content: center;
    align-items: center;
    margin: 40px 130px 16px 130px;
    box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.302), 0 2px 6px 2px rgba(60, 64, 67, 0.149);
    border-radius: 12px;

    .form-top {
        margin: 1rem;
    }

    .form-bottom {
        margin: 1rem;
    }

    .input-box {
        padding: 4px 16px 12px;
        font-size: 0.875rem;
        height: 5rem;
        border: none;
        display: flex;
        width: 100%;
    }

    .input-close {
        float: right;
        border: none;
        padding: 8px 24px;
        font-size: 14px;
    }
}
input:focus {
    outline: none;
}

Card.jsとCard.scssの作成

ポイントはisEditModeです。falseを初期値としますが、☑️を押下するとtoggleEditMode()が呼ばれ、toggleEditMode()trueにします。trueにすることで、doneのボタンが表示されます。

src/Components/Card/Card.js
import React from "react";
import './Card.scss';

class Card extends React.Component {
    state = {
        isEditMode: false,
        content: this.props.task.content,
        color: this.props.task.color,
        tasks: this.props.tasks,
        task: this.props.task
    }

    toggleEditMode = () => {
        this.setState({
            isEditMode: !this.state.isEditMode
        })
    }

    handleEditDone = () => {
        this.setState({
            isEditMode: false,
        })
        this.props.saveMemo(this.props.task.id, this.state.content, this.state.color);
    }

    changeText = (e) => {
        this.setState({
            content: e.target.value
        });
    }

    changeColor = (color = "#ffffff") => {
        this.setState({
            color: color
        });
        this.props.saveMemo(this.props.task.id, this.state.content, this.state.color);
    }

    render() {
        return (
            <li className="list" key={this.props.task.id} style={{ backgroundColor: this.state.color }}>
                {this.state.isEditMode ?
                    <input
                        className="input-box"
                        onChange={this.changeText}
                        key={this.props.task.id}
                        ref="text-cell"
                        autoFocus={true}
                        value={this.state.content}
                    />
                    :
                    <p>{this.state.content}</p>
                }
                <div className="list-buttom">
                    <button onClick={this.toggleEditMode}><i className="material-icons">edit</i></button>
                    {this.state.isEditMode &&
                        <button onClick={this.handleEditDone}><i className="material-icons">done</i></button>
                    }
                    <button onClick={() => this.changeColor()} ><i className="material-icons">color_lens</i></button>
                    <button onClick={() => this.changeColor('#f28b82')}><i className="material-icons pink">color_lens</i></button>
                    <button onClick={() => this.changeColor('#d7aefb')}><i className="material-icons purple">color_lens</i></button>
                    <button onClick={() => this.props.deleteMemo(this.props.task.id)}><i className="material-icons">delete</i></button>
                </div>
            </li>
        );
    }
}
export default Card;

src/Components/Card/Card.scss
.list {
    padding: 16px 16px 1px 16px;
    box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.302), 0 2px 6px 2px rgba(60, 64, 67, 0.149);
    border-radius: 12px;
    border-width: 1px;
    margin: 16px 16px 16px 0px;
    width: 100%;
}
.list-bottom {
    display: block;
}
li {
    border-radius: 10px;
    border: solid 3px #e0e0e0;
    min-height: 5rem;
}

おわりに

このGoogleKeep模倣アプリは、3~4日間で完成できました。
作りながら色々調べたり、自分自身で手を動かしたりすることで、Reactの基礎的なことは理解できるので是非3連休などで参考にして作ってみてください!
githubのレポジトリはこちらなので何かあった時には参考にしてください。
またこうするといいよ、などのコメントも大歓迎です!

参考文献

React公式ページ

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?