20201212のReactに関する記事は13件です。

イラストが簡単に変えられるカレンダー作った

お疲れ様です。
この前某カレンダーアプリを見ていたら、簡単にスタンプが押せる機能があったんですね。
これ結構使いやすくて、概要とか入力しなくて、とりあえずイラストだけ変わればいいってことあるじゃないですか。
(イメージとしては、バイトとかのシフトが近いですかね・・・)

とりあえず使ってるアプリに不満はなかったんですが、その予定を共有したくなった時に不便が生じました。
結局アプリでローカルに持ってるだけなので、それを共有するってのがなかなか難しかったのです。。。

というわけで、今ないものは作ってしまえというのが今回のテーマです。

初めに

レギュレーション

分解して考えてみると、以下の2点がレギュレーションになるかなと

  • カレンダーでボタンを押すだけでイラストが変えられること。
  • 誰でも状態を確認できること。(→必然的にWebアプリ??)

使った技術

さくっと作りたかったので、いったんReactを使ってやってみることにしました。

  • React:フロントエンドフレームワーク。ベースです。
  • React-Calendar:今回の肝。カレンダーのライブラリーはこれ
  • Material-ui:別になくてもいいんですけど、Webアプリケーションとしての体裁を・・・

作ったもの

コードとデモ

とりあえず以下Gitで管理してます。

最初に断っときますが、動けば十分で作ってますので品質はご容赦ください・・・

https://github.com/aion0721/sharecale

デモは以下です。

https://aion0721.github.io/sharecale/

ポイント

苦労した点は以下です。

カレンダー表記

基本的には、tileContentにて、各日付の値を設定できます。dateの変数の中に日付情報が入っていますので、
これをよしなに変数に渡して何とか値を取得するようにします。

<Calendar
      tileContent={({date}) =><p><img height="100" width="100" src={`${process.env.PUBLIC_URL}/` + showTypeValue(date) + `.png`}/>{showContent(date)}</p>}
/>

ただし、ソースを読んでいただくとわかるように、毎回undefinedチェックをしているので、
これはこれで相当非効率だなと思います・・・いい方法ないですかねぇ・・・

データの持ち方

ReactはFunctionで行くようになったので、HooksであるuseStateを使ってます。
ポイントは、配列を利用していることです。
react-calendarで、日付が一致するときに判定を行いたいため、例えば、2020/12/11の定義を行いたい場合に、
20201211の値として、typeValueを持つことによって、今どのアイコンを表示するか、を保持するようにしています。

  const [ item, setItem ] = useState({20201211:{holiday:true,txt:'aiueo',typeValue:1}});

アイコン変更方法

typeValueがアイコンナンバーを示しているので、ボタンが押されたときに数字を変更しています。
Switchでやってますが、個人的には最高に頭悪いのでなんかいい方法ありますかね・・・

    switch(item[getFormatDate(e)].typeValue){
      case 1: {
        tmpTypeValue = 2;
        break
      }
      case 2:{
        tmpTypeValue = 3
        break;
      }
      case 3:{
        tmpTypeValue = 1
        break;
      }
      default: {
        tmpTypeValue = 0
        break;
      }
    }

終わりに

reactを本当にさわりだけやってない人間がやると本当に非効率な書き方しかできてなくて嫌になりますね。。。

今後の展望

バックエンドAPI

とりあえず当初の目的である”共有”ができていないので、ServerlessFrameworkとかでバックエンドのAPI作って、
そこと同期できるようにしたいなとは思います。

認証

今はURL知ってればだれでも閲覧できてしまうため、認証を入れたいです。

逆に

今回のレギュレーションを達成しているアプリとかもう既にあったりしたら是非教えてください。もれなく私が喜びます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material-UIの便利なコンポーネント

目次

1. はじめに
2. Material-UIの実装方法
3. Grid編 - コンポーネントの画面配置を決めることができる
4. Table編 - 表を扱える
5. Modal編 - クリックすると、モーダルウィンドウが開く
6. 終わりに

1. はじめに

Material-UIとはGoogleが開発したReact, Typescriptで使えるUIライブラリです。それぞれのコンポーネントは、公式が設計したprops(組み込み要素)を組み合わせることで、見栄えの良いUIを作ることができます。しかし公式のライブラリの説明は、日本語対応していなかったり、そもそも説明不足でわかりにくい部分があります。最近私はMaterial-UIを使って開発をする機会が多かったので、今回はその中で私が便利だと感じたコンポーネントとpropsについてまとめて紹介していきたいと思います。

2. Material-UIの実装方法

1. Reactの開発環境を構築する。

ここでは省略しますが、以下の記事からmac, windowsでreactの環境構築ができます。
Macの場合
Windowsの場合

2. Material-uiのパッケージをtermialでインストールする。

termial
// with npm
npm install @material-ui/core
npm install @material-ui/icons

// with yarn
yarn add @material-ui/core
yarn add @material-ui/icons

3. reactのファイルで自分の使いたいコンポーネントをimportして使う

App.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Button from '@material-ui/core/Button';

function App() {
  return (
    <Button variant="contained" color="primary">
      Hello World
    </Button>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));

3. Grid編

Gridはコンポーネントの画面配置を決めることができます。例えば、2つの要素を画面の中央に揃えたり、右端に寄せたり、等間隔で揃えることなど、、、propsを組み合わせるだけで簡単に画面配置できるため非常に強力なコンポーネントです。
以下はGrid内で使えるpropsについてまとまめました。

1. container, item

containerとitemは親子関係で親がcontainerとなるように使います。
container内の要素は一列で揃えることが可能で、これがGridの基本となるためGridコンポーネントを使う場合は必須のpropsです。

使用例
<Grid container>//container内の要素は一列で表示
  <Grid item>
  {/* 中身 */}
  </Grid>
  <Grid item>
  {/* 中身 */}
  </Grid>
</Grid>

2. justify

justifyは、containerが含まれている Gridタグ内につけます。それぞれのitemの配置を多岐にわたって調整することが可能です。下に種類 - 用途の一覧を示しています。

  • 'flex-start' - 全てのアイテムを左端に揃える
  • 'center' - 全てのアイテムを中心に揃える
  • 'flex-end' - 全てのアイテムを右端に揃える。
  • 'space-between' - itemを等間隔に配置する。
  • 'space-around' - itemを等間隔に配置する。space-betweenと違い、両端に空白が入る。
  • 'space-evenly' - itemを等間隔に配置する。space-aroundより、両端の空白の割合が大きい。
使用例
<Grid container justify="flex-end">
  <Grid item>
  {/* 中身 */}
  </Grid>
  <Grid item>
  {/* 中身 */}
  </Grid>
</Grid>
//この場合、二つのアイテムは左揃えになる。

3. spacing

containerが含まれているGridタグ内につけることで、itemの間隔を調整できます。広さは{1}~{12}で決めることができるので、画面を見ながら試してみてちょうどいい間隔となるように数を調整します。

使用例
<Grid container spacing={4}>
  <Grid item>
  {/* 中身 */}
  </Grid>
  <Grid item>
  {/* 中身 */}
  </Grid>
</Grid>
//この場合、二つのアイテムの間隔が4の幅になります。

Grid応用編

今までのpropsを組み合わせることで、以下のような構成にすることも可能です。

コード
<Grid container justify="space-between">
  <Grid item>
    <Grid container spacing={2}>
      <Grid item>
        <Button
        variant="contained"
        color="secondary"
        startIcon={<SaveIcon />}
        >保存する
        </Button>
      </Grid>
      <Grid item>
        <Button
        variant="contained"
        color="secondary"
        startIcon={<DeleteIcon />}
        >削除する
        </Button>
      </Grid>
    </Grid>
  </Grid>
  <Grid item>
    <Button
    variant="contained"
    color="secondary"
    startIcon={<SettingsIcon />}
    >設定する
    </Button>
  </Grid>
</Grid>

結果

三つのボタンの配置を一列でいい感じに整えることができています。

スクリーンショット 2020-12-12 16.47.36.png

4. Table編

Tableコンポーネントは、表を作ることができます。見た目だけでなく、表の並びを変えたりページ切り替えを可能にしたりするライブラリもあり、非常に便利です。Tableの基本構成は下図のようになっています。手書きで汚くてすみません。htmlの知識があればすぐに使えると思います。
スクリーンショット 2020-12-12 18.05.25.png
サンプルコードと補足の説明を入れています。

サンプルコード
<TableContainer component={Paper} style={{ marginBottom: 30 }}>
{/* componentにライブラリのPaperをつけることで立体感がでてよくなります */}
  <Table>
    <TableHead>
      <TableRow style={{ backgroundColor: "#F2F2F2" }}>
        <TableCell>ID</TableCell>
        <TableCell >氏名</TableCell>
        <TableCell >ステート</TableCell>
      </TableRow>
    </TableHead>
    <TableBody>
    {rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row) => (
              //ページ切り替えの要素を取得
      <TableRow hover key={row.id}> 
       {/* hoverを入れることでマウスポイントが表の上に乗った時に色が変わるアクションがつきます */}
        <TableCell component="th" scope="row">{row.id}</TableCell>
        <TableCell >{row.name}</TableCell>
        <TableCell >{row.state}</TableCell>
      </TableRow>
      ))}
    </TableBody>
    <TableFooter>
      <TableRow>
        <TablePagination
        labelRowsPerPage="表示件数:"
        rowsPerPageOptions={[
          { label: '10件', value: 10 },
          { label: '50件', value: 50 },
          { label: '100件', value: 100 },
          { label: '全て', value: rows.length }
          ]}
         count={rows.length}
         rowsPerPage={rowsPerPage}
         page={page}
         SelectProps={{
           native: true
           }}
         onChangePage={handleChangePage}
         onChangeRowsPerPage={handleChangeRowsPerPage}
         ActionsComponent={TablePaginationActions}
         />
      </TableRow>
    </TableFooter>
  </Table>
</TableContainer>

結果

スクリーンショット 2020-12-12 18.17.07.png

5. Modal編

Modalは、ボタンやテキストなどの指定したコンポーネントをクリックするなどの条件を満たした時に表示することができます。用途としては、inputコンポーネントを埋め込んでデータを入力できるようにしたり、注意を表示させたい時に使います。
今回は、下のような新規作成ボタンを押すと、
スクリーンショット 2020-12-12 21.05.47.png
タイトルやコメントを入力できる画面が表示されようなものを作成する手順について順を追って説明します。
スクリーンショット 2020-12-12 20.46.17.png

1.ボタン(Modalが開くきっかけ)と、クリックするとtrueになる関数を作成

react
import React, {useState} from 'react';
import AddCircleIcon from '@material-ui/icons/AddCircle';

function NoticeScreen()  {
  const [open, setOpen] = useState(false);
  return(
    <Button
      variant="contained"
      color="secondary"
      startIcon={<AddCircleIcon />}
      onClick={() => setOpen(true)}
      //クリックするとtrueになる
      style={{ marginTop: 30 }}
    >
      新規作成
    </Button>
  )
}
export default NoticeScreen

2. Modalと、Modalを閉じる関数の作成

import React, {useState} from 'react';
import AddCircleIcon from '@material-ui/icons/AddCircle';

function NoticeScreen()  {
  const [open, setOpen] = useState(false);
  return(
    <Button
      variant="contained"
      color="secondary"
      startIcon={<AddCircleIcon />}
      onClick={() => setOpen(true)}
      style={{ marginTop: 30 }}
    >
      新規作成
    </Button>
    {/* Modal */}
    <Modal open={open} onClose={() => setOpen(false)}>
    </Modal>
  )
}
export default NoticeScreen

3. Modalの中身を作成する

import { Button, Grid, Typography, Modal, Paper, TextField } from '@material-ui/core';
.
//省略
.
<Modal open={open} onClose={() => setOpen(false)}>
//open={open}は、openがtrueの場合にmodalが開くという意味
<Fade in={open}>
  <Paper style={{ width: modalWidth, position: 'absolute', top: height * 0.1, right: (width - modalWidth - 40) / 2, padding: 20 }}>
    <Typography style={{ marginLeft: 8 }}>タイトル</Typography>
    <TextField
      style={{ margin: 8, paddingBottom: 20 }}
      margin="normal"
      fullWidth
      variant="outlined"
      className="red-border"
    />
    <Typography style={{ marginLeft: 8 }}>コメント</Typography>
    <TextField
      style={{ margin: 8, paddingBottom: 20 }}
      fullWidth
      margin="normal"
      multiline
      rows={4}
      InputLabelProps={{ shrink: true }}
      variant="outlined"
    />
    <Grid container style={{ paddingTop: 30 }} justify="flex-end" direction="row">
      <Grid item>
        <Button
          variant="contained"
          color="secondary"
          onClick={addNewCommentRequest}
          startIcon={<AddCircleIcon />}
        >
          投稿
        </Button>
      </Grid>
    </Grid>
  </Paper>
</Fade>
</Modal>

完全版

import React, {useState} from 'react';
import { Button, Grid, Typography, Modal, Paper, TextField } from '@material-ui/core';
import AddCircleIcon from '@material-ui/icons/AddCircle';

function NoticeScreen()  {
  const [open, setOpen] = useState(false);
  return(
    <Button
      variant="contained"
      color="secondary"
      startIcon={<AddCircleIcon />}
      onClick={() => setOpen(true)}
      style={{ marginTop: 30 }}
      >
      新規作成
    </Button>
    {/* Modal */}
    <Modal open={open} onClose={() => setOpen(false)}>
    <Fade in={open}>
      //open={open}は、openがtrueの場合にmodalが開くという意味
      <Paper style={{ width: modalWidth, position: 'absolute', top: height * 0.1,right: (width - modalWidth - 40) / 2, padding: 20 }}>
      <Typography style={{ marginLeft: 8 }}>タイトル</Typography>
      <TextField
      style={{ margin: 8, paddingBottom: 20 }}
      margin="normal"
      fullWidth
      variant="outlined"
      className="red-border"
      />
      <Typography style={{ marginLeft: 8 }}>コメント</Typography>
      <TextField
      style={{ margin: 8, paddingBottom: 20 }}
      fullWidth
      margin="normal"
      multiline
      rows={4}
      InputLabelProps={{ shrink: true }}
      variant="outlined"
      />
      <Grid container style={{ paddingTop: 30 }} justify="flex-end" direction="row">
        <Grid item>
          <Button
          variant="contained"
          color="secondary"
          onClick={addNewCommentRequest}
          startIcon={<AddCircleIcon />}
          >
          投稿
          </Button>
        </Grid>
      </Grid>
    </Paper>
  </Fade>
  </Modal>
  )
}
export default NoticeScreen

6. 終わりに

Material-UIには、これら以外にも便利なコンポーネントはたくさんありますが、今回は私の独断と偏見で便利なMaterial-UIを選び、その使い方について書かせていただきました。今後もMaterial-UIを使っていく中で記事にしたいと感じたら、新たに付け加えていこうと考えています。
不明点や間違っている点があれば、コメントでお知らせください。

参考にさせていただいたサイト

Material-ui公式サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

zForce AIRとReactJSでプロトタイピングしたことの学び

この記事はGoodpatch Advent Calendar 2020の13日目となります。
グッドパッチでエンジニアリングマネージャーをしております、nktnfです。

ここ最近、NeonodeのzForceを使ってプロトタイピングしており
現在進行ではありますが、そちらについて触れつつ、プロトタイピングについて
少し感じたことを記載しようと思います。

話したいこと

zForce AIRとReactJSでプロトタイプというのは簡単に作れる。
が、もしタッチレスなものを作ろうとしたときは、必要とする機能があるならばLeap motionや画像認識を使い
ユーザテストをしながら作っていくのがいいということを伝えたい。

前提

パッとプロトタイピングしたかったので、zForce AIRがPlug and Playに対応しているWindows10でシステム構築します。
https://support.neonode.com/docs/display/AIRTSUsersGuide/Getting+Started+with+Sensor+Evaluation+-+Plug+and+Play+with+USB

zForce AIR

zForce AIRは現在digikeyなどで購入できる、タッチセンサーです。
主にDigi-Keyで販売されており、約8000円くらいで買うことができます。
image.png

IRレーザーベースのマルチタッチセンサで、あらゆるディスプレイ、任意の表面もしくは、赤外光の送受信であれば空中での使用が可能です。
image.png
そのため、タッチレスについてプロトタイピングが可能となります。
https://products.neonode.com/us/touch-sensor-modules/#applications より

ReactJS

特段ReactJSじゃなければならない、ということはないですが
さっとプロトタイプするならば、慣れ親しんだもので実装を進めていくのが良いと思います。
(今回は、タッチイベントで取れるだろうということでReactJSを選択)

SyntheticEvent

基本的には、zForce AIRをWindowsにつなげた場合、タッチセンサーになるため
Touch eventsのonTouchCancel onTouchEnd onTouchMove onTouchStartの中に入ってくる形で
要素を操作することができます。

タッチの場合

<button onTouchEnd={() => console.log("touch")}>タッチ</button>

スワイプみたいなある範囲を動かす場合

<div onTouchMove={() => console.log("move finger")} >
ほげほげ
</div>

なので、React Swipeableやreact-swipeable-viewsなども使うことができます。

プロトタイプはタッチなのかタッチレスなものか

ここでいうタッチは、何か物理的なものに指でタッチする。
タッチレスは、空中でタッチするということです。
タッチなものであれば、旧来の原則ルールを使って進めていくことで簡単にプロトタイプを作れると思います。

もし、タッチレスなものを作成するならば

最初は世の中のタッチレスインターフェース設計ガイドラインといった先人の知恵を借りながら進めていく形が、闇雲にすすめるよりかは進めやすいと思います。
ultraleapのtouchless-interfaces( https://docs.ultraleap.com/touchless-interfaces/ )は
Leap motionといった製品向けですが、参考になります。

しかし、作るものが使いやすいものなのか、扱うテクノロジーが違うのでガイドラインをそのままというわけにもいかず、
自分も含めいろんな人に作ったものを触ってもらうのが良いと思います。

zForce AIRで気をつけるべき点

zForce AIRを空中面に対して活用する場合、ユーザはなれるのですが、もどかしく感じます

  • タッチ

    • センサーが感知しないところから、センサーが感知し押し込まれたときに反応する。
    • CSSでは、activeでマウスのhoverのような役割ができるので、そちらを活用する。
  • タッチムーブ

    • センサーがどこを感知しているか、感覚を得るまで上手く操作ができない。
    • どこで感知しているかチュートリアル・TIPSを見せてあげると良いと思います

インタラクションデザインをする上で

Gestural Interfaces:A Step Backward In Usabilityの下記のキーワードが参考になりました。

  • Visibility
  • Feedback
  • Consistency and Standards
  • Non-destructive operations
  • Discoverability
  • Scalability
  • Reliability

引用: https://www.academia.edu/2850361/Gestural_interfaces_a_step_backward_in_usability

また、最近は、下記本が参考になるなーと読み進めています。
Brave NUI World: Designing Natural User Interfaces for Touch and Gesture

最後に

アイディアをユーザーテストをしながら検証していこう

実際にそれを動かしてみないとどれだけ良いアイデアかわからないので、なんか思いついたり
フィードバックもらったら、作って触ってもらっています。

空中でそのタッチパネルの世界をそのまま持ってくるというのも、最初にやってみて
触ってもらったときに、スムーズに操作をすることができるということが確認できました。

そのため、旧来の原理を念頭におきユーザーのフラストレーションにつながらないように、
zForce AIRからLeap motionや他のテクノロジーでのインタラクションに徐々にシフトを進めていっています。

いきなり新しいハンドジェスチャーなどを考えていたら、もしかしたらスルーしてしまっていたかもしれません。

小さな学びも一歩一歩大切に、早く物づくりを進めていくのが良いと思います。

モノは完成しましたら、どこかで発表できればと思います。では、またどこかで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

StrapiでCSVからデータをインポートするプラグインを作る ~その1ファイル選択機能まで~

Strapi Advent Calenderの14日目はStrapi公式ブログのCSVからデータをインポートするプラグインを作成する記事のpart1を日本語でまとめてみました。

こちらの記事を参考に作成していきます
https://strapi.io/blog/how-to-create-an-import-content-plugin-part-1-4

Strapiのカスタムプラグインについては、5日目の記事をどうぞ!
StrapiのLocal Pluginsを利用して独自の画面を作成する

プラグインとモデルを作成する

まず、プラグインの雛形と、プラグインの中で使用するモデルを2種類作成します。

$ strapi generate:plugin import-content
$ strapi generate:model importconfig --plugin import-content
$ strapi generate:model importeditem --plugin import-content

ディレクトリ構造は以下のような形になります。

    plugins
     |-- import-content
       |-- admin
       |-- config
       |-- controllers
       |-- models
         |-- Importconfig.js
         |-- Importconfig.settings.json
         |-- Importeditem.js
         |-- Importeditem.settings.json
       |-- services

モデルに項目を追加する

plugins/import-content/models/Importconfig.settings.json に以下をコピペします。

plugins/import-content/models/Importconfig.settings.json
{
  "connection": "default",
  "collectionName": "",
  "info": {
    "name": "importconfig",
    "description": ""
  },
  "options": { "timestamps": true, "increments": true, "comment": "" },
  "attributes": {
    "date": { "type": "date" },
    "source": {
      "type": "string"
    },
    "options": { "type": "json" },
    "contentType": {
      "type": "string"
    },
    "fieldMapping": { "type": "json" },
    "ongoing": {
      "type": "boolean"
    },
    "importeditems": {
      "collection": "importeditem",
      "via": "importconfig",
      "plugin": "import-content"
    }
  }
}

次に、plugins/import-content/models/Importeditem.settings.jsonに以下をコピペします。

plugins/import-content/models/Importeditem.settings.json
{
  "connection": "default",
  "collectionName": "",
  "info": {
    "name": "importeditem",
    "description": ""
  },
  "options": { "increments": true, "timestamps": true, "comment": "" },
  "attributes": {
    "ContentType": {
      "type": "string"
    },
    "ContentId": { "type": "integer" },
    "importconfig": {
      "model": "importconfig",
      "via": "importeditems",
      "plugin": "import-content"
    },
    "importedFiles": {
      "type": "json"
    }
  }
}

ここまでの手順を踏んで起動すると、管理画面のサイドメニューのコレクションタイプにImportconfigsImporteditemsが、プラグインにimport-contentが表示されていると思います。ここからプラグインの中身を作成していきます。
スクリーンショット 2020-12-12 21.33.45.png

必要なパッケージをインストールする

今回のプラグインで必要になるパッケージは以下の通りです。
- content-type-parser
- csv-parse
- get-urls
- moment rss-parser
- request
- simple-statistics
- striptags
- lodash

yarn add content-type-parser csv-parse get-urls moment rss-parser request simple-statistics striptags lodash

コンポーネントを作成する

1. ファイル選択フォームを作成する

plugins/import-content/admin/srcの中にcomponentsディレクトリを作成します。

plugins/import-content/admin/src/componentsUploadFileFormディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。

plugins/import-content/admin/src/components/UploadFileForm/index.js
import React, { Component } from "react";
import PropTypes from "prop-types";
class UploadFileForm extends Component {
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    }
  };
  onChangeImportFile = file => {
    file &&
      this.setState({
        file,
        type: file.type,
        options: { ...this.state.options, filename: file.name }
      });
  };
  render() {
    return <input 
    onChange={({target:{files}}) => files && this.onChangeImportFile(files[0])} name="file_input" accept=".csv" type="file" />;
  }
}
export default UploadFileForm;

次に、plugins/import-content/admin/src/containers/HomePage/index.jsに、先ほど作成したUploadFileFormコンポーネントを読み込ませて表示させてみます。

plugins/import-content/admin/src/containers/HomePage/index.js
import React, { memo } from "react";
import pluginId from "../../pluginId";
import UploadFileForm from "../../components/UploadFileForm";

const HomePage = () => {
  return <UploadFileForm />;
};
export default memo(HomePage);

ここまでで、CSVファイルを選択するフォームができました。見栄えをよくするために、コンポーネントをいくつか追加します。
スクリーンショット 2020-12-12 21.55.25.png

次に、plugins/import-content/admin/src/componentsPディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。

plugins/import-content/admin/src/components/P/index.js
import styled from "styled-components";
const P = styled.p`
  margin-top: 10px;
  text-align: center;
  font-size: 13px;
  color: #9ea7b8;
  u {
    color: #1c5de7;
  }
`;
export default P;

plugins/import-content/admin/src/componentsRowディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。

plugins/import-content/admin/src/components/Row/index.js
import styled from "styled-components";
const Row = styled.div`
  padding-top: 18px;
`;
export default Row;

plugins/import-content/admin/src/componentsLabelディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。

plugins/import-content/admin/src/components/Label/index.js
import styled, { css, keyframes } from "styled-components";
const Label = styled.label`
  position: relative;
  height: 146px;
  width: 100%;
  padding-top: 28px;
  border: 2px dashed #e3e9f3;
  border-radius: 2px;
  text-align: center;
  > input {
    display: none;
  }
  .icon {
    width: 82px;
    path {
      fill: ${({ showLoader }) => (showLoader ? "#729BEF" : "#ccd0da")};
      transition: fill 0.3s ease;
    }
  }
  .isDragging {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
  }
  .underline {
    color: #1c5de7;
    text-decoration: underline;
    cursor: pointer;
  }
  &:hover {
    cursor: pointer;
  }
  ${({ isDragging }) => {
    if (isDragging) {
      return css`
        background-color: rgba(28, 93, 231, 0.01) !important;
        border: 2px dashed rgba(28, 93, 231, 0.1) !important;
      `;
    }
  }}
  ${({ showLoader }) => {
    if (showLoader) {
      return css`
        animation: ${smoothBlink("transparent", "rgba(28,93,231,0.05)")} 2s
          linear infinite;
      `;
    }
  }}
`;
const smoothBlink = (firstColor, secondColor) => keyframes`
0% {
fill: ${firstColor}; background-color: ${firstColor}; } 26% {
fill: ${secondColor}; background-color: ${secondColor}; } 76% {
fill: ${firstColor}; background-color: ${firstColor}; } `;
export default Label;

作成した3つのコンポーネントを、UploadFileFormにインポートして、ファイルをドラッグ&ドロップで選択できるエリアも作成していきます。

plugins/import-content/admin/src/components/UploadFileForm/index.js
import React, { Component } from "react";
import PropTypes from "prop-types";
import P from "../P"; // 追加
import Row from "../Row"; // 追加
import Label from "../Label"; // 追加
import { Button } from "@buffetjs/core"; // 追加
class UploadFileForm extends Component {
  state = {
    file: null,
    type: null,
    options: {
      filename: null
    },
    isDragging: false // 追加
  };
  onChangeImportFile = file => {
    file &&
      this.setState({
        file,
        type: file.type,
        options: { ...this.state.options, filename: file.name }
      });
  };

  handleDragEnter = () => this.setState({ isDragging: true }); // 追加
  handleDragLeave = () => this.setState({ isDragging: false }); // 追加
  handleDrop = e => { // 追加
    e.preventDefault();
    this.setState({ isDragging: false });
    const file = e.dataTransfer.files[0];
    this.onChangeImportFile(file);
  };
  render() {
    return ( // 書き換え
      <div className={"col-12"}>
        <Row className={"row"}>
          <Label
            showLoader={this.props.loadingAnalysis}
            isDragging={this.state.isDragging}
            onDrop={this.handleDrop}
            onDragEnter={this.handleDragEnter}
            onDragOver={e => {
              e.preventDefault();
              e.stopPropagation();
            }}
          >
            <svg
              className="icon"
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 104.40317 83.13328"
            >
              <g>
                <rect
                  x="5.02914"
                  y="8.63138"
                  width="77.33334"
                  height="62.29167"
                  rx="4"
                  ry="4"
                  transform="translate(-7.45722 9.32921) rotate(-12)"
                  fill="#fafafb"
                />
                <rect
                  x="5.52914"
                  y="9.13138"
                  width="76.33334"
                  height="61.29167"
                  rx="4"
                  ry="4"
                  transform="translate(-7.45722 9.32921) rotate(-12)"
                  fill="none"
                  stroke="#979797"
                />
                <path
                  d="M74.25543,36.05041l3.94166,18.54405L20.81242,66.79194l-1.68928-7.94745,10.2265-16.01791,7.92872,5.2368,16.3624-25.62865ZM71.974,6.07811,6.76414,19.93889a1.27175,1.27175,0,0,0- .83343.58815,1.31145,1.31145,0,0,0-.18922,1.01364L16.44028,71.87453a1.31145,1.31145,0,0,0,.58515.849,1.27176,1.27176,0,0,0,1.0006.19831L83.23586,59.06111a1.27177,1.27177,0,0,0,.83343- .58815,1.31146,1.31146,0,0,0,.18922-1.01364L73.55972,7.12547a1.31146,1.31146,0,0,0-.58514-.849A1.27177,1.27177,0,0,0,71.974,6.07811Zm6.80253- .0615L89.4753,56.35046A6.5712,6.5712,0,0,1,88.554,61.435a6.37055,6.37055,0,0,1-4.19192,2.92439L19.15221,78.22019a6.37056,6.37056,0,0,1-5.019-.96655,6.57121,6.57121,0,0,1-2.90975- 4.27024L.5247,22.64955A6.57121,6.57121,0,0,1,1.446,17.565a6.37056,6.37056,0,0,1,4.19192-2.92439L70.84779.77981a6.37055,6.37055,0,0,1,5.019.96655A6.5712,6.5712,0,0,1,78.77651,6.01661Z"
                  transform="translate(-0.14193 -0.62489)"
                  fill="#333740"
                />
                <rect
                  x="26.56627"
                  y="4.48824"
                  width="62.29167"
                  height="77.33333"
                  rx="4"
                  ry="4"
                  transform="translate(0.94874 87.10632) rotate(-75)"
                  fill="#fafafb"
                />
                <rect
                  x="27.06627"
                  y="4.98824"
                  width="61.29167"
                  height="76.33333"
                  rx="4"
                  ry="4"
                  transform="translate(0.94874 87.10632) rotate(-75)"
                  fill="none"
                  stroke="#979797"
                />
                <path
                  d="M49.62583,26.96884A7.89786,7.89786,0,0,1,45.88245,31.924a7.96,7.96,0,0,1-10.94716-2.93328,7.89786,7.89786,0,0,1-.76427-6.163,7.89787,7.89787,0,0,1,3.74338- 4.95519,7.96,7.96,0,0,1,10.94716,2.93328A7.89787,7.89787,0,0,1,49.62583,26.96884Zm37.007,26.73924L81.72608,72.02042,25.05843,56.83637l2.1029- 7.84815L43.54519,39.3589l4.68708,8.26558L74.44644,32.21756ZM98.20721,25.96681,33.81216,8.71221a1.27175,1.27175,0,0,0-1.00961.14568,1.31145,1.31145,0,0,0-
10
.62878.81726L18.85537,59.38007a1.31145,1.31145,0,0,0,.13591,1.02215,1.27176,1.27176,0,0,0,.80151.631l64.39506,17.2546a1.27177,1.27177,0,0,0,1.0096-.14567,1.31146,1.31146,0,0,0,.62877-.81726l13.3184- 49.70493a1.31146,1.31146,0,0,0-.13591-1.02215A1.27177,1.27177,0,0,0,98.20721,25.96681Zm6.089,3.03348L90.97784,78.70523a6.5712,6.5712,0,0,1-3.12925,4.1121,6.37055,6.37055,0,0,1- 5.06267.70256L18.39086,66.26529a6.37056,6.37056,0,0,1-4.03313-3.13977,6.57121,6.57121,0,0,1-.654-5.12581L27.02217,8.29477a6.57121,6.57121,0,0,1,3.12925-4.11211,6.37056,6.37056,0,0,1,5.06267- .70255l64.39506,17.2546a6.37055,6.37055,0,0,1,4.03312,3.13977A6.5712,6.5712,0,0,1,104.29623,29.0003Z"
                  transform="translate(-0.14193 -0.62489)"
                  fill="#333740"
                />
              </g>
            </svg>
            <P>
              <span>
                Drag & drop your file into this area or
                <span className={"underline"}>browse</span> for a file to upload
              </span>
            </P>
            <div onDragLeave={this.handleDragLeave} className="isDragging" />
            <input
              name="file_input"
              accept=".csv"
              onChange={({ target: { files } }) =>
                files && this.onChangeImportFile(files[0])
              }
              type="file"
            />
          </Label>
        </Row>
      </div>
    );
  }}
export default UploadFileForm;

ここまでで、ファイルをドラッグ&ドロップで選択できるエリアができました。
次は選択したCSVファイルを解析する部分を作成します。
スクリーンショット 2020-12-12 22.53.13.png

2. インポートソースとインポート先の選択部分を作成する

HomePageindex.jsを書き換えます。

javascript
import React, { memo, Component } from "react";
import {request} from "strapi-helper-plugin"; // 追加
import PropTypes from "prop-types"; // 追加
import pluginId from "../../pluginId";
import UploadFileForm from "../../components/UploadFileForm";

class HomePage extends Component {
  state = { // 追加
    analyzing: false,
    analysis: null
  };

  onRequestAnalysis = async analysisConfig => { // 追加
    this.analysisConfig = analysisConfig;
    this.setState({ analyzing: true }, async () => {
      try {
        const response = await request("/import-content/preAnalyzeImportFile", {
          method: "POST",
          body: analysisConfig
        });

        this.setState({ analysis: response, analyzing: false }, () => {
          strapi.notification.success(`Analyzed Successfully`);
        });
      } catch (e) {
        this.setState({ analyzing: false }, () => {
          strapi.notification.error(`Analyze Failed, try again`);
          strapi.notification.error(`${e}`);
        });
      }
    });
  };

  render() {
    return ( // 書き換え
       <UploadFileForm
         onRequestAnalysis={this.onRequestAnalysis}
         loadingAnalysis={this.state.analyzing}
       />
     );
  }
}
export default memo(HomePage);

次に、UploadFileFormにファイルを解析する処理を加えます。

plugins/import-content/admin/src/components/UploadFileForm/index.js
readFileContent = file => {
    const reader = new FileReader();
    return new Promise((resolve, reject) => {
      reader.onload = event => resolve(event.target.result);
      reader.onerror = reject;
      reader.readAsText(file);
    });
  };

  clickAnalyzeUploadFile = async () => {
    const { file, options } = this.state;
    const data = file && (await this.readFileContent(file));
    data &&
      this.props.onRequestAnalysis({
        source: "upload",
        type: file.type,
        options,
        data
      });
  };

同じくUploadFileFormrender()内の、一番最後のRowタグの後に、解析用のボタンを設置ます。またexportの前に処理を加えます。

plugins/import-content/admin/src/components/UploadFileForm/index.js
~~省略~~
            <input
              name="file_input"
              accept=".csv"
              onChange={({ target: { files } }) =>
                files && this.onChangeImportFile(files[0])
              }
              type="file"
            />
          </Label>
        </Row>
        <Row className={"row"}> // 追加
          <Button
            label={"Analyze"}
            color={this.state.file ? "secondary" : "cancel"}
            disabled={!this.state.file}
            onClick={this.clickAnalyzeUploadFile}
          />
        </Row>
      </div>
    );
  }}
UploadFileForm.propTypes = { // 追加
  onRequestAnalysis: PropTypes.func.isRequired,
  loadingAnalysis: PropTypes.bool.isRequired
};
export default UploadFileForm;

componentsディレクトリに、Blockディレクトリを追加し、その中にindex.jscomponents.jsを作成し、それぞれのファイルに以下を記述します

admin/src/components/Block/index.js
import React, { memo } from "react";
import PropTypes from "prop-types";
import { Wrapper, Sub } from "./components";
const Block = ({ children, description, style, title }) => (
  <div className="col-md-12">
    <Wrapper style={style}>
      <Sub>
        {!!title && <p>{title} </p>} {!!description && <p>{description} </p>}
      </Sub>
      {children}
    </Wrapper>
  </div>
);
Block.defaultProps = {
  children: null,
  description: null,
  style: {},
  title: null
};
Block.propTypes = {
  children: PropTypes.any,
  description: PropTypes.string,
  style: PropTypes.object,
  title: PropTypes.string
};
export default memo(Block);
admin/src/components/Block/index.js
import React, { memo } from "react";
import PropTypes from "prop-types";
import { Wrapper, Sub } from "./components";
const Block = ({ children, description, style, title }) => (
  <div className="col-md-12">
    <Wrapper style={style}>
      <Sub>
        {!!title && <p>{title} </p>} {!!description && <p>{description} </p>}
      </Sub>
      {children}
    </Wrapper>
  </div>
);
Block.defaultProps = {
  children: null,
  description: null,
  style: {},
  title: null
};
Block.propTypes = {
  children: PropTypes.any,
  description: PropTypes.string,
  style: PropTypes.object,
  title: PropTypes.string
};
export default memo(Block);

HomePageにインポートと関数を追加し、render()の中を書き換えます

import React, { memo, Component } from "react";
import {request} from "strapi-helper-plugin";
import PropTypes from "prop-types";
import pluginId from "../../pluginId";
import UploadFileForm from "../../components/UploadFileForm";
import { // 追加
  HeaderNav,
  LoadingIndicator,
  PluginHeader
} from "strapi-helper-plugin"; // 追加
import Row from "../../components/Row"; // 追加
import Block from "../../components/Block"; // 追加
import { Select, Label } from "@buffetjs/core"; // 追加
import { get, has, isEmpty, pickBy, set } from "lodash"; // 追加

const getUrl = to => // 追加
  to ? `/plugins/${pluginId}/${to}` : `/plugins/${pluginId}`;

class HomePage extends Component {
  importSources = [ // 追加
    { label: "External URL ", value: "url" },
    { label: "Upload file", value: "upload" },
    { label: "Raw text", value: "raw" }
  ];


  state = {
    loading: true, // 追加
    modelOptions: [], // 追加
    models: [], // 追加
    importSource: "upload",
    analyzing: false,
    analysis: null,
    selectedContentType: "" // 追加
  };

  selectImportDest = selectedContentType => { // 追加
    this.setState({ selectedContentType });
  };

  componentDidMount() { // 追加
    this.getModels().then(res => {
    const { models, modelOptions } = res;
    this.setState({
      models,
      modelOptions,
      selectedContentType: modelOptions ? modelOptions[0].value : ""
    });
  });
}

getModels = async () => { // 追加
  this.setState({ loading: true });
  try {
  const response = await request("/content-type-builder/content-types", {
      method: "GET"
    });

    // Remove non-user content types from models
    const models = get(response, ["data"], []).filter(
      obj => !has(obj, "plugin")
    );
    const modelOptions = models.map(model => {
      return {
        label: get(model, ["schema", "name"], ""), // (name is used for display_name)
        value: model.uid // (uid is used for table creations)
      };
    });

    this.setState({ loading: false });

    return { models, modelOptions };
  } catch (e) {
    this.setState({ loading: false }, () => {
      strapi.notification.error(`${e}`);
    });
  }
  return [];
};

  onRequestAnalysis = async analysisConfig => {
    this.analysisConfig = analysisConfig;
    this.setState({ analyzing: true }, async () => {
      try {
        const response = await request("/import-content/preAnalyzeImportFile", {
          method: "POST",
          body: analysisConfig
        });

        this.setState({ analysis: response, analyzing: false }, () => {
          strapi.notification.success(`Analyzed Successfully`);
        });
      } catch (e) {
        this.setState({ analyzing: false }, () => {
          strapi.notification.error(`Analyze Failed, try again`);
          strapi.notification.error(`${e}`);
        });
      }
    });
  };

  selectImportSource = importSource => { // 追加
    this.setState({ importSource });
 };

  render() {
    return ( // 書き換え
      <div className={"container-fluid"} style={{ padding: "18px 30px" }}>
        <PluginHeader
          title={"Import Content"}
          description={"Import CSV and RSS-Feed into your Content Types"}
        />
        <HeaderNav
          links={[
            {
              name: "Import Data",
              to: getUrl("")
            },
            {
              name: "Import History",
              to: getUrl("history")
            }
          ]}
          style={{ marginTop: "4.4rem" }}
        />
        <div className="row">
          <Block
            title="General"
            description="Configure the Import Source & Destination"
            style={{ marginBottom: 12 }}
          >
            <Row className={"row"}>
              <div className={"col-4"}>
                <Label htmlFor="importSource">Import Source</Label>
                <Select
                  name="importSource"
                  options={this.importSources}
                  value={this.state.importSource}
                  onChange={({ target: { value } }) =>
                    this.selectImportSource(value)
                  }
                />
              </div>
              <div className={"col-4"}>
                  <Label htmlFor="importDest">Import Destination</Label>
                  <Select
                    value={this.state.selectedContentType}
                    name="importDest"
                    options={this.state.modelOptions}
                    onChange={({ target: { value } }) =>
                      this.selectImportDest(value)
                    }
                  />
              </div>
            </Row>
            <UploadFileForm
              onRequestAnalysis={this.onRequestAnalysis}
              loadingAnalysis={this.state.analyzing}
            />
          </Block>
        </div>
      </div>
     );
  }
}
export default memo(HomePage);

ここまでで、インポートソースとインポート先の選択部分が完成したと思います。
実際にインポートしていく部分は、また後日紹介したいと思います。

スクリーンショット 2020-12-12 23.38.08.png

参考元: How to create your own plugin on strapi

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hookを使ってチャットボットを作成した

概要

React初心者です。
少し前にReactのHookを使って簡単なチャットボットを作成しました。
若干ウザめなチャットボットですが、恥を忍んで記事にさせていただきます。

完成したチャットボット

某元テニスプレイヤーの方が熱い言葉で返してくれるチャットボットです。
ezgif.com-gif-maker (1).gif

開発環境

React Hookの学習がメインだったので、UIやサーバ側は既存のサービスに頼りました。

  • React 16.13.1
  • Material UI 
  • Firebase(Firestore、 Functions、Hosting)

ファイル構造

src
├── assets
│   ├── image              
│   └── styles        
├── components
│   ├── Answer.jsx
│   ├── AnswersList.jsx
│   ├── Chat.jsx
│   ├── Chats.jsx
│   └── index.js       //components内のjsxファイルをまとめたもの
├── App.jsx     
├── dataset.json       //firestoreへアップするデータ
├── firebase
├── index.js
└── serviceWorker.js

componentsの中身について

以下の4つに分類しています。
スクリーンショット 2020-12-05 22.56.17.png

index.jsにはcomponentsで作成したjsxファイルをまとめてexportしておきます。

components/index.js
export {default as AnswersList} from './AnswersList';
export {default as Answer} from './Answer';
export {default as Chats} from './Chats';
export {default as Chat} from './Chat';

これにより、一行で複数のcomponentをimportして使えます。

App.jsx
import { AnswersList, Chats } from './components/index';

データベースについて

Firebaseの「Firestore」を使いました。

まず、保存したいデータをjson形式で作成します。

dataset.json
{
  "init": {
    "answers": [
        {"content": "仕事で悩んでいます", "nextId": "work"},
        {"content": "恋愛に悩んでいます", "nextId": "love"},
        {"content": "人生に悩んでいます", "nextId": "life"},
        {"content": "ただ元気がありません", "nextId": "health"}
    ],
    "question": "おい、そこの君!いつもの笑顔がないぞ!"
},
"work": {
  "answers": [
      {"content": "上司に叱られました", "nextId": "work_1"},
      {"content": "ミスをしました", "nextId": "work_2"},
      {"content": "イライラすることがありました", "nextId": "work_3"},
      {"content": "疲れました", "nextId": "work_4"},
      {"content": "辞めたいです", "nextId": "work_5"}
  ],
  "question": "仕事で何があったんだ?"
},
//以下省略

「answers」 :ユーザー側の回答
「content」  :ユーザーの回答選択肢
「nextId」  :回答選択後の遷移先キー
「question」 :某Mさんからの自動回答

最初は「init」の内容が表示され、その後は選択された内容に応じて次の内容(nextId)が呼び出されるようになっています。

使用したHook

useStateuseEffectuseCallbackを使いました。
それぞれの役割についてはこちらをご参考ください。

useState

チャット内容、回答選択肢が変更されると、それらを状態として保持し管理します。

const [answers, setAnswers] = useState([]),
      [chats, setChats] = useState([]);

useEffect

最新のチャット内容がチャット下部に常に表示されるようにしたいので、
追加される(scrollAreaの値が変化する)と自動スクロールするようにしています。

  useEffect(() => {
    const scrollArea = document.getElementById('chats-area')
    if(scrollArea) {
      scrollArea.scrollTop = scrollArea.scrollHeight
    }
  })

useCallback

初期の回答選択肢を保持し、ユーザーが回答を選択すると、チャット内容を変更&次の回答選択肢を準備する動きをしています。

また、setTimeoutを使い、ボットっぽく敢えてタイムラグが発生するような動きにしています。

  const selectAnswer = useCallback((selectedAnswer, nextQuestionId) => {
    addChats({
      text: selectedAnswer,
      type: 'answer'
    })
    setTimeout(() => displayNextQuestion(nextQuestionId, dataset[nextQuestionId]), 1000);
  },[answers])

最後に

今回はチャットボットを作ってみましたが、Hookの簡単な動きは理解できたかなと思います。
ページ構造が簡易的すぎて少し物足りなさもありますが…
他にも個人開発向けでHookをふんだんに使えそうなものがあればぜひ教えていただけると嬉しいです。挑戦してみたいと思います。

チャットボットはこちらのYoutubeのサイトを参考にさせていただきました。
解説が非常にわかりやすいので、特に初心者の方におすすめです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

昔懐かしのアクセスカウンターを作った話

この記事はCivictech1年目 Advent Calendar 2020 12日目の記事です。

11日目は @t-mat さんのシビックテック1年目の取組と課題でした。
私も今年入ってから初めてシビックテックというものを知ったので、この後t振り返ろうかなと思っています。

TL;DR

「BADオープンデータ供養寺」建立の宮大工をしていた時に作ったアクセスカウンターが同できてるかをゆるゆる書くっていう記事です。

BADオープンデータ供養寺のソースコードは https://github.com/codeforjapan/bad-opendata-temple です!
いまだに面白いissueが残ってたりしますので皆さんもぜひ...!

BADオープンデータ供養寺自体は https://bad-data.rip にありますので興味のある方はぜひ。(.ripなんてドメイン初めて聞いた...)

はじめに

BADオープンデータ供養寺とは

同じく12日目のCivictech テック好きのカレンダーに書いてらっしゃる @kaizumaki さんの BADオープンデータ供養寺、匠たちの技から引用をさせていただきまして...

オープンデータ供養寺とは

住職(※1)の「ぶっ飛んだデザインにした方が良いのでは」というひとことに、腕に覚えありの宮大工(※2)達が結集し、懐かしのインターネッツネタをモダンコーディングで再現しようと力を合わせたのでした。

※1…言わずと知れた我らがリーダー、下山紗代子氏のこと。オープンデータの普及に東へ西へと駆け回っている。すっかり「住職」と呼ばれるのが板についている。
※2…私たちの間ではエンジニアのことを「宮大工」、デザイナーのことを「宮造りデザイナー」としている。そして彼らの名前は「〜〜匠」と呼ばれる。

書いてある通りではあるんですが、

  • カーソルをローソクにする
  • お経を流す
  • お鈴を鳴らす
  • 餅を投げる

だったり、

  • MARQUEEタグじゃないマーキー
  • CGIじゃないアクセスカウンター

など、ローテク?昔懐かし?のようなハイテクノロジー()の数々があります

私はこの CGIじゃないアクセスカウンターを作ったのでこれについてこのあとかこうかと思います(ほかのことについては引用元をぜひ...)

CGIじゃないアクセスカウンターってどゆこと????

タイトルにも書いてる通り、今回のアクセスカウンターにCGIを使っていません!
正真正銘Reactのコンポーネントです。
(このタイトルで読みに来る方は大体CGIが何かわかってると思うのでそこは書きません)

(なぜcgiじゃないのかというとそれをホストするばしょがないからですね)

アクセスカウンターなのでアクセス数を記録しないといけない。
しかもそれは共通させなきゃいけないのでどこかにDBを持つ必要がある。
じゃあどうしてるのか....
実はGoogle Spreadsheet + Google App Scriptでできています。

React側

accessCounter.tsx
import React, {
  ReactElement,
  useEffect,
  useState,
} from 'react';
import styled from 'styled-components';

const AccessCounterContent = styled.div`
  font-size: 30px;
`;

const NumberText = styled.span`
  color: #ebff00;
  background-color: #000000;
  padding: 2px 4px;
  margin: 0 2px;
  box-sizing: border-box;
`;

const AccessCounter: React.FC = (): ReactElement => {
  const [accessStr, setAccessStr] = useState('LOADING');
  useEffect(() => {
    fetch(
      'https://script.google.com/macros/s/えーぴーあいの/exec',
    )
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        setAccessStr(
          String(json.your_access).padStart(4, '0'),
        );
        return json;
      })
      .catch((_) => {
        setAccessStr('ERROR');
      });
  }, []);

  return (
    <AccessCounterContent>
      あなたは
      {[...accessStr].map((str, index) => {
        return (
          <NumberText key={str + String(index)}>
            {str}
          </NumberText>
        );
      })}
      人目の参拝者です
    </AccessCounterContent>
  );
};

export default AccessCounter;

分けて解説をすると..

const AccessCounterContent = styled.div`
  font-size: 30px;
`;

const NumberText = styled.span`
  color: #ebff00;
  background-color: #000000;
  padding: 2px 4px;
  margin: 0 2px;
  box-sizing: border-box;
`;

ここでデザインをかいていて...

const AccessCounter: React.FC = (): ReactElement => {
  const [accessStr, setAccessStr] = useState('LOADING');
  useEffect(() => {
    fetch(
      'https://script.google.com/macros/s/えーぴーあいのid/exec',
    )
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        setAccessStr(
          String(json.your_access).padStart(4, '0'),
        );
        return json;
      })
      .catch((_) => {
        setAccessStr('ERROR');
      });
  }, []);

React FCを使っているので、useStateでStateを作成(初期値にLOADINGと文字列を入れておきます)
useEffectでcomponentDidMountedでこの後作るAPIにアクセスしてアクセス数を読み取ります。
ちなみに

String(json.your_access).padStart(4, '0'),

これは23とかで帰ってきた数字を0023とかにするものです

  return (
    <AccessCounterContent>
      あなたは
      {[...accessStr].map((str, index) => {
        return (
          <NumberText key={str + String(index)}>
            {str}
          </NumberText>
        );
      })}
      人目の参拝者です
    </AccessCounterContent>
  );

これは、表示を
image.png
こうしたかったので、
[...accessStr].map((str, index) => {で一文字ずつに分割してmapで回して、

          <NumberText key={str + String(index)}>
            {str}
          </NumberText>

で一文字ずつ分割したコンポーネントとしています。

これが、Reactコンポーネントの動きです。

App Scriptのスクリプトくん

function doGet(e) {
  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var accessNumber = Number(sheet.getRange("A1").getValues()) + 1;
  sheet.getRange("A1").setValue(accessNumber);
  var result = {
    "your_access": accessNumber
  }
  var out = ContentService.createTextOutput();

    //Mime TypeをJSONに設定
  out.setMimeType(ContentService.MimeType.JSON);

    //JSONテキストをセットする
  out.setContent(JSON.stringify(result));
  return out;
}

簡単な内容ですのでほぼ解説しませんが....
スプシはつないでいるので

  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var accessNumber = Number(sheet.getRange("A1").getValues()) + 1;
  sheet.getRange("A1").setValue(accessNumber);

でA1に格納している数値を取得して

  var result = {
    "your_access": accessNumber
  }
  var out = ContentService.createTextOutput();

    //Mime TypeをJSONに設定
  out.setMimeType(ContentService.MimeType.JSON);

    //JSONテキストをセットする
  out.setContent(JSON.stringify(result));
  return out;

でgetへの返り値としてます

あとはこれをウェブアプリケーションとして導入をすればアクセスカウンターのAPIの出来上がりです!

おわりに

結構前からいんたーねっとにいる学生ではあるのでアクセスカウンターのcgiは見たことはあるし、使ったこともあるんですが実際に作ったのは初めてでした...
そしてそれ以上にcgiじゃないアクセスカウンターを作ることになるとは.....

こんな面白いものがいろいろそろったBADオープンデータ供養寺をぜひご覧ください!(https://bad-data.rip)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単にDockerでreact環境を速攻で作る

こんにちは、IT技術者の田中です。
簡単にDockerでreact環境を速攻で作る

背景

Dockerでreact環境を速攻で作る最新(2020年12月12日)情報がなく困ったので、記事を書きました!
(色々惜しい記事はありましたが、ちょこちょこミスがあり困りました)

前提としてはmacbook proで構築しました!

フォルダ構成

下記のフォルダ構成で作ります。

react
├ Dockerfile
├ docker-compose.yml
└ react-sample/

Dockerfile

Dockerfileの内容

FROM node:15.3.0-alpine
WORKDIR /usr/src/app

docker-compose.yml

docker-compose.ymlの内容

version: '3'
services:
  node:
    build: 
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/usr/src/app
    command: sh -c "cd react-sample && yarn start"
    ports:
      - "3000:3000"

dockerビルドを実行する

docker-compose.ymlのフォルダに移動します。

> docker-compose build

上記が完了するまで待ちます。

docker-composeでreactをプロジェクト作成

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

> docker-compose run --rm node sh -c "npm install -g create-react-app && create-react-app react-sample"

dockerのコンテナを起動する

下記のコマンドでdockerコンテナを起動する。

docker-compose up

reactの起動を確認する

ブラウザーで下記のURLにアクセスする
http://localhost:3000

下記の画面が表示されたら無事成功
スクリーンショット 2020-12-12 午後5.16.28.png

終わり!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとPHP 初めての連携

はじめに

今回はReactで簡単なフォームを作って、PHPの方で受け取るプログラムを書いてみました。Laravelではやったことがあるのですが、フレームワーク無しのPHPでやったことがなくて、フレームワーク無しのPHPをだいぶ忘れてしまったのでやってみました。

テキストを入力して送信したら、そのテキストが表示されて、何も入力しなければエラーが表示される感じのシンプルなフォームです。
image.pngimage.png

以下を参考にさせていただきました。
参考:Create a Contact Form With PHP and React in 3 Min

React側

package.json
{
  "name": "simple-form",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@material-ui/core": "^4.11.2",
    "@types/axios": "^0.14.0",
    "@types/node": "^14.14.12",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "axios": "^0.21.0",
    "react": "^16.11.0",
    "react-dom": "^16.11.0",
    "react-hook-form": "^6.13.0",
    "react-scripts": "^4.0.1",
    "styled-components": "^5.2.1",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

↑使用したpackageです。react-hook-formとmaterial-uiを使用していますが、これらの説明は割愛します。


↓react-hook-formの記事も書いているので良ければ是非!
react-hook-formの使い方を解説!V6.12.0追加

index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

index.tsxでApp.tsxからインポートしています。見慣れた形だと思います。

import React, { useState } from "react";
import axios from "axios";
import { Button, CircularProgress, styled, TextField } from "@material-ui/core";
import { useForm } from "react-hook-form";

type FormData = {
  text: string;
};

export const App: React.FC = () => {
  const [message, setMessage] = useState("");
  const [error, setError] = useState(false);

  const {
    register,
    handleSubmit,
    formState: { isSubmitting },
  } = useForm<FormData>();

  const onSubmit = async (data: FormData) => {
    try {
      const res = await axios.post("http://localhost:8080/index.php", data);
      setError(res.data.error);
      setMessage(res.data.message);
    } catch {
      setError(true);
      setMessage("通信に失敗しました。");
    }
  };

  return (
    <>
      <Form onSubmit={handleSubmit(onSubmit)}>
        <TextField
          defaultValue=""
          margin="normal"
          variant="outlined"
          name="text"
          error={error}
          inputRef={register}
          helperText={message}
        />
        <Button
          type="submit"
          variant="contained"
          color="primary"
          disabled={isSubmitting}>
          {isSubmitting ? <CircularProgress size={24} /> : "送信"}
        </Button>
      </Form>
    </>
  );
};

const Form = styled("form")({
  display: "flex",
  flexDirection: "column",
  width: 300,
  margin: "0 auto",
});

axiosでhttp://localhost:8080/index.phpにフォームの値を送信し、レスポンスとして、errorとmessageが入った連想配列を受け取ります。PHPの方のコードを見ればイメージがしやすいと思います。

PHP側

PHPはDockerでnginxを使い、http://localhost:8080で立ち上げました。MAMPとかを使っても簡単にできると思います。

index.php
<?php
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Headers: Content-Type');
$rest_json = file_get_contents("php://input"); // JSONでPOSTされたデータを取り出す
$_POST = json_decode($rest_json, true); // JSON文字列をデコード

if(empty($_POST['text'])) {
    echo json_encode(
        [
           "error" => true,
           "message" => "Error: 入力してください。",
        ]
    ); 
} else {
    echo json_encode(
        [
           "error" => false,
           "message" => 'Success: 入力されたテキスト→'.$_POST['text'],
        ]
    ); 
}
  • Access-Control-Allow-Originで異なるオリジンからのアクセスを可能に
  • Access-Control-Allow-Headersで使用可能なHTTPヘッダーを設定
  • file_get_contentsでJSONでPOSTされたデータを取り出す
  • POSTされたデータがなければ、errorがtrueでエラーメッセージを返し、POSTされたデータがあれば、errorがfalseでPOSTされたデータを返す感じです

終わりに

ここまで読んでいただきありがとうございます!細かい説明があまりできていませんが、初めてフロントエンドとサーバーサイドで連携する時のサンプルとしてみていただければと思います。次はデータベースを使って簡単なCRUDをできるようにしたいなと思います。文章力あまりないのでわかりにくいかもしれませんが、少しずつ続けながら上げていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsbyでフォントをセルフホスティングする方法

はじめに

GatsbyでWebフォントをセルフホスティングする方法を紹介します。
セルフホスティングをすると、Google Fontsなどのサービスからフォントを取得する場合と比べて、パフォーマンスが向上するというメリットがあります。

フォントのセルフホスティングとは

セルフホスティングとは、自前のサーバに置いたフォントファイルをwebページのフォントとして使用することです。
Google Fontsなどのサービスがもつサーバからフォントを取得する方法とは異なります。

セルフホスティングするメリット

セルフホスティングをするメリットは、Webページのパフォーマンスが向上することです。
サービスがもつサーバからWEBフォントを取得する場合は、そのサーバにHTTPリクエストをする必要があり、日本語などの文字の種類が多いフォントの場合は、大きなボトルネックになってしまいます。
しかしながら、セルフホスティングをすると、上記のフォント取得による遅延を軽減することができます。

使用するパッケージ

Fontsourceというモノレポから好きなフォントを選び、インストールします。
自前でフォントファイルを、デュレクトリに配置するという方法もありますが、設定が面倒臭いのでライブラリに頼ることとします。

実装方法

Google Fontsにも存在する「Noto Sans JP」をインストールすることを例に実装方法を説明します。

パッケージのインストール

まずパッケージをインストールします。

yarn add fontsource-open-sans

パッケージマネージャーにnpmを使用している場合は下記の通りです。

npm install fontsource-open-sans

Layout.jsにimportする

Layout.jsに下記のようにimportします。

import 'fontsource-noto-sans-jp';

上記では、デフォルトのフォントウェイトに、全てのスタイル(Boldなど)が含まれる状態でimportしてしまうため、下記のように使用するフォントウェイトとスタイルを指定すると、ペイロードサイズを削減できます。

import "fontsource-noto-sans-jp/700.css"
import "fontsource-noto-sans-jp/900-normal.css"

あとは、CSSでfont-familyを指定してあげれば、使用ができるようになります。

まとめ

Fontsourceというライブラリを使用して、GatsbyでWebフォントをセルフホスティングする方法を紹介しました。
参考になれば幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CreateReactAppにWebVitals計測ライブラリが入ったので試してみた

この記事はReact #2 Advent Calendar 2020の12日目の記事です

概要

  • 最近はNext.jsばかり使ってますが久しぶりにCreateReactAppを使ってみたらreportWebVitals.tsという見慣れないファイルが生成されていたのを見たのが今日の記事のきっかけです
  • Reactアプリの雛形生成ツールであるCreateReactAppはv4でWebVitalsの計測ライブラリがデフォルトで組み込まれるようになりました
  • せっかくなのでドキュメントに沿って少し試してみたいと思います

ファイルの確認

  • まずは生成されたコードを見てみましょう
  • create-react-appコマンドで雛形を生成します
npx create-react-app --template typescript web-vitals-sample
  • 生成されたファイルの一覧はこんな感じです
.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock
  • reportWebVitals.tsが今回注目するファイルですね
  • 中身はこんな感じです
src/reportWebVitals.ts
import { ReportHandler } from 'web-vitals';

const reportWebVitals = (onPerfEntry?: ReportHandler) => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(onPerfEntry);
      getFID(onPerfEntry);
      getFCP(onPerfEntry);
      getLCP(onPerfEntry);
      getTTFB(onPerfEntry);
    });
  }
};

export default reportWebVitals;
  • web-vitalsというライブラリを使っていろいろ設定してくれています
  • web-vitalsはGoogleChromeが提供するWebVitals計測ライブラリですね
  • このライブラリの追加とセットアップを事前にやってくれているということのようです

使い方

  • 使い方はCreateReactAppのドキュメントに載っています
  • reportWebVitals.tsでexportされたreportWebVitalsを呼び出して引数に関数を渡して使います
    • 渡した関数が内部で呼び出され各種メトリクスが渡されます
  • サンプルにあるようにconsole.logを引数として渡してみます
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root'),
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(console.log);
  • ページを表示するとコンソールにLCPなどのWebVitalsの指標が表示されました!

スクリーンショット 2020-12-11 18.14.37.png

function sendToAnalytics(metric: Metrics) {
  const body = JSON.stringify(metric);
  const url = 'https://example.com/analytics';

  // Use `navigator.sendBeacon()` if available, falling back to `fetch()`
  if (navigator.sendBeacon) {
    navigator.sendBeacon(url, body);
  } else {
    fetch(url, { body, method: 'POST', keepalive: true });
  }
}

reportWebVitals(sendToAnalytics);
function sendToAnalytics({ id, name, value }) {
  ga('send', 'event', {
    eventCategory: 'Web Vitals',
    eventAction: name,
    eventValue: Math.round(name === 'CLS' ? value * 1000 : value), // values must be integers
    eventLabel: id, // id unique to current page load
    nonInteraction: true, // avoids affecting bounce rate
  });
}

reportWebVitals(sendToAnalytics);

試してみた

  • 送信されたデータをDBに保存するAPIを作って適当にグラフ表示してみました
    • どういう風に集計するのが有用かはもうちょっとちゃんと考えないといけないですね・・

スクリーンショット 2020-12-12 3.40.55.pngスクリーンショット 2020-12-12 3.41.10.pngスクリーンショット 2020-12-12 3.41.18.png

まとめ

  • CreateReactAppに新しく入ったWebVitalの計測機能はweb-vitalsライブラリのセットアップを事前にしてくれているというものでした
  • 取得できるメトリクスを収集するのは簡単なのでそれをどう活用していくか考えていけばよさそうですね
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】一定時間で消える表示を作る

前回作ったサクセスメッセージがいつまでも表示されるのは如何なものかと思い、今回は表示後に何秒か経ったら消えるようにしました。

前回
【React】Railsでsaveされた時に、Reactでサクセスメッセージを表示

今回の完成形

思った以上に簡単、と言うか思いっきりJavaScriptの書き方だったので勉強不足を感じました。

setTimeout()を使ってstateを変更する

前回、state の値が true か false かでメッセージを表示させる、という風に作成しました。

初期値がfalseのstateをtrueでメッセージを表示させたので、逆にtrueをfalseに戻せばOKということです。

まずは初期値。

  state = {
    books: [],
    successCreate: false
  }
  createBook = (title, body) => {
    axios
      .post('http://localhost:3001/books', { title: title, body: body})
      .then((response) => {
        const newData = update(this.state.books, { $push: [response.data]})
        this.setState({books: newData, successCreate: true})
      })
  }

rails側のcreateが呼ばれ、データがsaveされるとfalseがtrueに変わる、というものです。

このtrueを何秒後かにfalseにsetStateするようにします。

  createBook = (title, body) => {
    axios
      .post('http://localhost:3001/books', { title: title, body: body})
      .then((response) => {
        const newData = update(this.state.books, { $push: [response.data]})
        this.setState({books: newData, successCreate: true})
        // ここから追記
        setTimeout(function(){
          this.setState({successCreate: false})
        }.bind(this),3000)
      })
  }

これは3秒後にstateを変更するようにしています。

stateがfalseになるので表示されていたメッセージは消えるようになります。

おそらくあまりに初歩的な内容なので調べても全然出てこなかった!!
もっと精進してさくっと実装できる力を付けたいものです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Railsでsaveされた時に、Reactでサクセスメッセージを表示

個人のブログで、ReactとRailsでDBとやりとりする記事を作りました

フロントReact、バックRailsでデータベースに保存する

某プログラミングスクール(◯MM)で課題で出されたものをReactで作ってみて、そこにサクセスメッセージを出そう、と言うのが今回の目的です。

メモ書き程度で書いていきます。

完成後の動作

フォーム内容の保存に成功するとサクセスメッセージが呼び出されます。
流れとしては、

  • stateを作る
  • createが呼ばれsaveされたらstateを変更する
  • stateをコンポーネントに渡してメッセージを表示させる

こんな感じです。

stateを作る

  state = {
    books: [],
    successCreate: false
  }

successCreateと言うstateを作りました。
ここは初期値はfalseとして、createが呼ばれたらtrueとします。

createが呼ばれsaveされたらstateを変更する

  createBook = (title, body) => {
    axios
      .post('http://localhost:3001/books', { title: title, body: body})
      .then((response) => {
        const newData = update(this.state.books, { $push: [response.data]})
        this.setState({books: newData, successCreate: true})
      })
  }

Rails側でbooksコントローラーのcreateを呼び出し、保存されたデータはbooksに追加し、successCreateはtureに変更します。
ちなみにbooksにはBookモデルのデータが全て入っています。

stateをコンポーネントに渡してメッセージを表示させる

このsuccessCreateの値をコンポーネントに渡します。

<BookCreateForm
  createBook={this.createBook}
  successCreate={this.state.successCreate}
/>

受け取った先で、stateがtrueの時にメッセージを表示するよう記述します。

{this.props.successCreate && (
  <p style={{color: '#16de69', fontSize: 6}}>Book was successfully created.</p>
)}

これでBookがcreateされ、stateが変更した時にメッセージが出るようになりました。

react側とrails側でバリデーションをかけてより安全性を高めれば、思いがけないメッセージの表示はなくなるかと。

忘備録的なものなのでとりあえず動いた時点で記事にしております、あしからずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを書く時に気をつけたいこと

はじめに

  • Reactを使っての開発経験が約1年となったので私がコーディングする上で気をつけていること、これから気をつけていきたいことをこの記事にまとめたいと思います。

コンポーネントのスタイリングでインラインスタイルは避ける

  • style属性でコンポーネントのスタイリングを行うと小さなコンポーネントはまだ大丈夫なのですが大きなコンポーネントになってくる程、可読性が下がってくる気がしています。
    • cssに切り出す、もしくはstyled-componentを使うとかが良さそうな気がしています(まだ試したこと無い)。
    • プロジェクトではmaterial-uiを使っているので、試しにmakeStylesを使ってスタイリングを行ってみましたが感想は「うーん...」でした。
  • Reactの公式でもstyle属性を用いた方法は推奨されていないようです。

不要なdivタグは避ける

  • Reactでは複数タグをreturnすることはできないので、致し方なく<div>を使うことが多いと思います。
// pタグbuttonタグの2つは返せないのでdivタグで囲わざるを得ない
const Component = () => {
  return(
    <div> 
      <p>へい</p>
      <button type='button'>ボタン</button>
    </div>
  );
};
  • こういった時はフラグメントを使います。
    • フラグメントを使うことで不要なタグがrenderされません。
// <> はFlagmentの省略記法
const Component = () => {
  return(
    <> 
      <p>へい</p>
      <button type='button'>ボタン</button>
    </>
  );
};

export default を使わない

  • 少し前までは積極的にexport defaultを使用していたのですがどうやらあまりよろしくないらしいのでこれからはexportを使っていきたいと思います。
    • export defaultexportが混在しているとimport文で{}が必要だったり要らなかったりといった細かな違いもあるのでexportで統一していきたい所存。
// OK
export const Component = () => {
  return(
    .
    .
    .
  );
};

// NG
export default Component;

.tsx(.jsx) と .ts(.js)を使い分ける

  • Reactを使用しているファイルには.tsxを、使用していないファイルには.tsを付けるようにしています。
    • こうすることでコンポーネントに関するファイルかどうかを拡張子で見分けがつきやすいです。
  • 拡張子の機能の違いはstack overflowに詳しく書いてありました。

コンポーネントのプロパティに直接ごちゃごちゃ書かない

  • 例えば、以下のようなコンポーネントがあったとします。
interface SubmitButtonProps {
  label: string;
  onSubmit: () => void;
};
 const SubmitButton: React.FC<SubmitButtonProps> = (props: SubmitButtonProps) => {
  return (
    <button type='submit' name='submit-button'>{props.label}</button>
  );
};

const SimpleForm: React.FC<SimpleFormProps> = (props: SimpleFormProps) => {
  return ( 
    <div className='simple-form'>
      <input type='text' name='login-id-field'/>
      <input type='password' name='password-field'/>
      <SubmitButton 
        label={送信} 
        onSubmit={() => {
        .
        . // 複数行
        .
        }}
      />
    </div>
  );
};
  • onSubmitプロパティに直接書くのではなくhandlerメソッドを定義してそれを呼ぶようにする
    • こうするとプロパティ内がごちゃごちゃせずスッキリする。
const SimpleForm: React.FC<SimpleFormProps> = (props: SimpleFormProps) => {
  return ( 
    <div className='simple-form'>
      <input type='text' name='login-id-field'/>
      <input type='password' name='password-field'/>
      <SubmitButton 
        label={送信} 
        onSubmit={handleSubmit}
      />
    </div>
  );
};

const handleSubmit = () => {
 // ここにonSubmitで行いたいことを書く
};

import文の順序

  • import文が下のようにまとまりがなくごちゃごちゃしていると「ぬぉおおおお…」となるタイプです。
import React from 'react';
import { hogeGetAction } from '../action/HogeAction';
import { ChildComponentA } from '../ChildComponentA';
import { Button } from 'material-ui/@core'
import { connect } from 'react-redux';
import { ChildComponentB } from '../ChildComponentB';
import { fooGetAction } from '../action/FooAction';

const SampleComponent = () => {...};
  • 私はサードパーティライブラリ → UIフレームワーク → 自作コンポーネント → その他の順で書くようにしています。
import React from 'react'
import { connect } from 'react-redux';
import { Button } from 'material-ui/@core';
import { ChildComponentA } from '../ChildComponentA';
import { ChildComponentB } from '../ChildComponentB';
import { hogeGetAction } from '../action/HogeAction';
import { fooGetAction } from '../action/FooAction';

const SampleComponent = () => {...};
  • スッキリ。自己満。:open_hands:

その他

  • React Hooks APIをふんだんに使って何か作りたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む