buto > /dev/null

だいたい急に挑戦してゴールにたどり着かずに飽きる日々です

react Material-UI入門

Material-UIはreactで利用できるデザインライブラリです

早速インストールして使ってみます!

yarn add @material-ui/core@next @emotion/react @emotion/styled

could not find a declaration file for module 'react/jsx-runtime'

上記コマンドでMaterial-UIをインストールすると「could not find a declaration…」のエラーが全部のtsxファイルで発生してしまった

import React from 'react'; でreactを読み込んでいますがreactの型定義が見つからないためエラーとなったようです

react自体はtypescript必須ではないので型定義を別途インストールしたら解決しました

yarn add -D @types/react

App Bar

シンプルなヘッダーができました f:id:butorisa:20210920161649p:plain

components/Header.tsx

import AppBar from '@mui/material/AppBar';

function Header() {

  return (
    <AppBar
      color="secondary"
    >
      buto {`>`} /dev/null
    </AppBar>
  );
}

export default Header;

Text Field & Button

components/InputArea.tsx

import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

function InputArea() {

  return (
    <div>
      <TextField label="enter your name"/>
      <Button>enter</Button>
    </div>
  );
}

export default InputArea;

App.tsx

import Header from './components/Header';
import InputArea from './components/InputArea';

function App() {

  return (
    <div>
      <Header/>
      <InputArea/>
    </div>
  );
}

export default App;

ヘッダーと重なっちゃいました… f:id:butorisa:20210920202134p:plain

FlexBoxを使おう!

sx={{ padding:1,margin:1 }} という感じでsxプロパティにスタイル要素を記述します

App.tsx

import Box from '@mui/material/Box';
import Header from './components/Header';
import InputArea from './components/InputArea';

function App() {

  return (
    <div>
      <Box
        sx={{
          padding:1,
          margin:1
        }}
      >
        <Header/>
      </Box>
      <Box
        sx={{
          padding:1,
          margin:1
        }}
      >
        <InputArea/>
      </Box>
    </div>
  );
}

export default App;

AppBarとTextFieldを別々のBoxにしたので重ならずに表示されました! f:id:butorisa:20210920210122p:plain

Buttonの配置が上すぎるのでTextFieldとbuttonもFlexBoxで囲みます

Buttonの位置が分かりやすいように variant=contained にします

components/InputArea.tsx

import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

function InputArea() {

  return (
    <Box
      sx={{
        display:"flex"
      }}
    >
      <Box>
        <TextField label="enter your name"/>
      </Box>
      <Box
        sx={{
          padding:1
        }}
      >
        <Button
          variant="contained"
          color="secondary"
        >
          enter
        </Button>
      </Box>
    </Box>
  );
}

export default InputArea;

TextFieldとButtonが中央揃えになりました! f:id:butorisa:20210920212811p:plain

参考

【TypeScript】【React】Could not find a declaration file for module ‘react’.の対処について

Theme UI-the sx Prop

Material UI-CSS Grid

react useEffectでライフサイクル入門

今日はuseEffectフックを使って関数を実行するタイミングを操作します

useEffectフック

useEffect(() => {実行したい処理}, [state変数]);

このように書くとstate変数が変更されたタイミングで実行したい処理が実行されます

第2引数([state変数]のところ)を変えることでstate変数の変更タイミング以外でも処理を実行することができます

レンダリングごとに実行(Updating)

初期表示+再描画の度に実行したい場合は「useEffectの第2引数を省略」します

サンプルコードではWelcome.tsxで名前を入力、Enterボタンを押下するとメッセージと現在日時が表示されます

useEffectには現在日時を取得してセットする処理を書いています

components/Welcome.tsx

import {useState,useEffect} from 'react';
import {useName} from '../logic/useName';

function Welcome() {

  const {disp, changeName} = useName();
  const [show, setShow] = useState(false);
  const [date, setDate] = useState('');

  // useEffectの第2引数を省略すると描画ごとに実行される
  useEffect(() => {
    const now: string = new Date().toLocaleString();
    setDate(now);
  });

  return (
    <div>
      <p>名前を入力してください</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
      <button onClick={() => setShow(true)}>Enter</button>
      {show &&
        <div>
          <p>ようこそ!! {disp}</p>
          <p>{date}</p>
        </div>
      }
    </div>
  );
}

export default Welcome;

App.tsx

import Welcome from './components/Welcome';

function App() {

  return (
    <div>
      <Welcome/>
    </div>
  );
}

export default App;

logic/useName.tsx 入力された名前のsetter/getter

import {useState,useEffect} from 'react';

export const useName = () => {

  const [name, setName] = useState<string>('risa');
  const [disp, setDisp] = useState<string>(name);

  useEffect(() => {
    setDisp(name + 'さん');
  }, [name]);

  const changeName = (input:string) => {
    setName(input);
  };

  return {disp, changeName};
}

再描画のタイミング「名前を変更してテキストボックスからフォーカスアウトした時」に現在日時も変わっています f:id:butorisa:20210919171613g:plain

初期表示のみ実行(Mounting)

初期表示のみ実行したい場合は「useEffectの第2引数を[]」にします

components/Welcome.tsx

import {useState,useEffect} from 'react';
import {useName} from '../logic/useName';

function Welcome() {

  const {disp, changeName} = useName();
  const [show, setShow] = useState(false);
  const [date, setDate] = useState('');

  // useEffectの第2引数を[](空配列)すると初期表示のみ実行される
  useEffect(() => {
    const now: string = new Date().toLocaleString();
    setDate(now);
  }, []);

  return (
    <div>
      <p>名前を入力してください</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
      <button onClick={() => setShow(true)}>Enter</button>
      {show &&
        <div>
          <p>ようこそ!! {disp}</p>
          <p>{date}</p>
        </div>
      }
    </div>
  );
}

export default Welcome;

今度は名前を変更しても現在日時は変わっていません f:id:butorisa:20210919172551g:plain

コンポーネント破棄で実行(Unmounting)

コンポーネントが非表示になったタイミングで実行したい場合は「useEffectの第1引数に実行したい処理を記述」します

import {useState,useEffect} from 'react';
import {useName} from '../logic/useName';

function Welcome() {

  const {disp, changeName} = useName();
  const [show, setShow] = useState(false);
  const [date, setDate] = useState('');

  // useEffectの第1引数return句に記述した処理はコンポーネント破棄時に実行される
  useEffect(() => {
    const now: string = new Date().toLocaleString();
    setDate(now);

    return () => console.log('コンポーネントが破棄されました');
  });

  return (
    <div>
      <p>名前を入力してください</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
      <button onClick={() => setShow(true)}>Enter</button>
      {show &&
        <div>
          <p>ようこそ!! {disp}</p>
          <p>{date}</p>
        </div>
      }
    </div>
  );
}

export default Welcome;

コードを修正後ホットリロード時にコンソールに「コンポーネントが破棄されました」と出力されました f:id:butorisa:20210919180412p:plain

第2引数を[]にするとマウント時とアンマウント時に実行されるようになるようです

コンポーネント破棄タイミングってブラウザ閉じた、別サイトへ移動って感じなのかな??

(非表示で破棄になるかと思って画像のbyeボタンを作ったが非表示では破棄されなかった)

参考

useEffectフックのしくみ

Reactの基礎 【ライフサイクル】

【ReactNative】現在の日付、時間表示をする方法

react コンポーネントでstate共有(カスタムフック)

昨日はuseStateを使って入力した名前を画面に表示するところまでできました

今日はstateをロジック処理に移動、入力コンポーネントと表示コンポーネントを分けます

コンポーネント間でstate変数を共有する方法は何種類もありそうでしたが、カスタムフックを使います

(useContextでグローバル変数的に共有する、親コンポーネントを経由して共有するがよく見かけましたが変数のスコープが広がりすぎて面倒なバグを生みそうなのでカスタムフックにしました)

失敗例:入力しても画面表示が切り替わらない

logic/useName.tsx

state変数「name」とnameを変更する「changeName」を返すロジック処理(★カスタムフック)

import React,{useState} from 'react';

export const useName = () => {

  const [name, setName] = useState<string>('risa');

  const changeName = (input:string) => {
    setName(input);
  };

  return {name, changeName};
}

components/InputArea.tsx

nameを変更するテキストボックス

import {useName} from '../logic/useName';

function InputArea() {

  const {changeName} = useName();

  return (
    <input type="text" onBlur={e => changeName(e.target.value)}/>
  );
}

export default InputArea;

components/Header.tsx

nameを表示

import {useName} from '../logic/useName';

function Header() {

  const {name} = useName();

  return (
    <header>
      <p>Hello! {name}</p>
    </header>
  );
}

export default Header;

App.tsx

import Header from './components/Header';
import InputArea from './components/InputArea';

function App() {
  return (
    <div>
      <Header/>
      <InputArea/>
    </div>
  );
}

export default App;

ビルドは成功して画面が表示されますが、フォーカスアウトしても名前が変わらない…

f:id:butorisa:20210918114538g:plain

原因1:nameが描画後にsetNameが実行されている

変更前の状態のnameを表示したままになっていたということです

ロジック処理に表示用のstate変数「disp」を追加してuseEffectでnameが変更されたらdispを変更します

logic/useName.tsx

import React,{useState,useEffect} from 'react';

export const useName = () => {

  const [name, setName] = useState<string>('risa');
  const [disp, setDisp] = useState<string>(name);

  useEffect(() => {
    setDisp(name + 'さん');
  }, [name]);

  const changeName = (input:string) => {
    setName(input);
  };

  return {disp, changeName};
}

原因2:表示コンポーネントがstateの変更を検知できていない

試しに入力コンポーネントに上記でセットされるdispを表示してみます

components/InputArea.tsx

import {useName} from '../logic/useName';

function InputArea() {

  const {disp, changeName} = useName();

  return (
    <div>
      <p>inputエリア:{disp}</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
    </div>
  );
}

export default InputArea;

Header.tsxもdispを表示するように修正しましたが、変更されたのはInputArea.tsxの表示のみです!

f:id:butorisa:20210918122928p:plain

changeNameを呼び出したInputArea.tsxコンポーネントには変更後のdispがリターンされますが、Header.tsxは初期表示でしかリターン値をもらえてないんですね

カスタムフックを使ったstateの共有方法

方法1:stateを変更するコンポーネント内で表示も行う

上記「原因2」で修正したInputArea.tsxのように同じコンポーネントでstateの表示と変更を行います

モダンJSのAtomicDesignでコンポーネント再利用する思考からは外れてしまいますが、この方法だとstateを扱う箇所がまとまっているのでコードの見通しも良いです◎

方法2:親コンポーネントでカスタムフックを使いpropsで渡す

どうしてもコンポーネントを分けておきたい場合はpropsで兄弟コンポーネントに渡します

※useName.tsxは上記「原因1」の修正状態です

App.tsx

import Header from './components/Header';
import InputArea from './components/InputArea';
import {useName} from './logic/useName';

function App() {

  const {disp,changeName} = useName();
  const nameProp = {
    disp : disp,
    changeName : changeName
  };

  return (
    <div>
      <Header {...nameProp}/>
      <InputArea  {...nameProp}/>
    </div>
  );
}

export default App;

components/Header.tsx

function Header(nameProp:any) {

  const name = nameProp.disp;

  return (
    <header>
      <p>Hello! {name}</p>
    </header>
  );
}

export default Header;

components/InputArea.tsx

function InputArea(nameProp:any) {

  return (
    <div>
      <p>inputエリア:{nameProp.disp}</p>
      <input type="text" onBlur={e => nameProp.changeName(e.target.value)}/>
    </div>
  );
}

export default InputArea;

f:id:butorisa:20210918135631p:plain

Header.tsxで表示している箇所も名前が変わりましたね!!

練習コードなのでpropsで渡す値も少なくきれいに見えますが、実際のシステムだと扱うコンポーネントと変数が多いのでコードの可読性が下がるかもしれませんね…

私は「方法1:stateを変更するコンポーネント内で表示も行う」が良いかなと思います!

参考

説明をしながらreact+typescriptでカスタムフックを作っていく

【React】「useStateの値を更新しても反映されない!」の解決方法

react useState入門

昨日に引き続きreactHooksの練習をします まずはよく使うuseStateから!!

useStateフック

state変数とsetState関数の定義をするフック(API)です

「useState<型>(初期値)」で使います

const [name, setName] = useState<string>('risa');

<練習>state変数に初期値セットして画面に表示

npx create-react-app プロジェクト名 --template typescript で作成したプロジェクトを修正しています

プロジェクト名/src配下にcomponentsフォルダを作成し、Header.tsxを作成しました

nameをstate変数にして宣言しています {変数名}で画面に表示されます

import React, {useState} from 'react';

function Header() {

  const [name, setName] = useState<string>('risa');

  return (
    <header className="App-header">
      Hello! {name}
    </header>
  );
}

export default Header;

App.tsxを修正して上記のHeader.tsxを読み込むようにします

import React from 'react';
import Header from './components/Header';

function App() {
  return (
    <div className="App">
      <Header/>
    </div>
  );
}

export default App;

yarn start で開発サーバを起動すると表示されました!!

f:id:butorisa:20210917115113p:plain

<練習>入力した名前を画面に表示する

今度はrisaからテキストボックスに入力した名前をフォーカスアウトしたタイミングで表示を切り替えるようにします

components/Header.tsx

import React, {useState} from 'react';

function Header() {

  const [name, setName] = useState<string>('risa');

  function changeName(name:any) {
    setName(name.target.value);
  }

  return (
    <header className="App-header">
      <p>Hello! {name}</p>
      <input type="text" onBlur={e => changeName(e)}/>
    </header>
  );
}

export default Header;

changeName関数を追加してテキストボックス入力値でstate変数を更新しています

フォーカスアウトのイベントはonBlurです

入力した名前で表示を切り替えることができました!

f:id:butorisa:20210917122316g:plain

参考

React入門(TypeScript版)

React再入門<<急がば回れ>>

モダンJSについてほぼ忘れたので今日はReactのそもそもを学び直します

プロジェクト作成

せっかくなのでTypeScriptバージョンの記事にならいました

React × TypeScript 入門

私の環境ではcreate-react-appのバージョンが4.0.3だったので--templateが必要でした!

npx create-react-app {プロジェクト名} --template typescript

この記事にたどり着く前に非TypeScriptのReactプロジェクト作成の記事を読みながらTypeScriptに変えようとしたのですが失敗しました。。。

関数コンポーネント

React:関数コンポーネントとクラスコンポーネントの違い

関数コンポーネントは「コンポーネントの書き方」のこと

以前はクラスコンポーネントというのが主流だったようです(他の言語のクラスと同じですね)

現在は関数コンポーネントが推奨されていて、以下が理由(他にもありますが分かりやすかったものはこちら)

  • おまじないコードがなくなりコードがスッキリする
  • ライフサイクルメソッドからの解放
    • jQuery時代マウント時、描画時…などで同じような処理たくさん書いて読みづらくなったことある
  • stateをコンポーネント間で共有したい時にクラスでは「引数にクラスを取り、新しいクラスを返す」クラスを作成する必要があったが、関数ではuseEffectフックを使ってコンポーネント内にstateが更新された時に実行したい処理を書くことができる

Hooks

関数コンポーネントで利用できるReactが提供しているライブラリまたはAPIのようなもの

  • useState
    • state変数、set関数を定義する
  • useEffect
    • 第2引数のstate変数が更新された時に第1引数のコールバック関数を実行する
  • useRef
    • useRefで定義した変数をテキストボックスref属性にする+イベント関数でstate変数にref変数をセットすることで入力値が表示されるようになる(双方向バインディングのような動き)

Hooksをちゃんと使えるようになると変数の状態管理がきれいにできそう!

Java DynamoDB接続

久しぶりにDynamoDB使ってみる

半年前くらいにちょこっと仕事で使ったDynamoDB。費用が安いNoSQLデータベースってことしか分かってないけど

昨日作ったRESTAPIからDynamoDBにデータ登録してみます

DynamoDBテーブル作成

AWSマネジメントコンソールでAPIからデータを登録するテーブルを作成する

ちょっとした日記(嬉しかったこと、悲しかったこと)を記録するアプリを想定しています

記録内容を保持するjournalテーブルを作成しました

試しに「アプリ開発をスタートしたよ」という嬉しかった出来事(eval=10)のデータを入れています(AWSマネジメントコンソールで登録) f:id:butorisa:20201227220256p:plain

APIからDB接続するためのアクセスキーを作成

JavaAPI)から先ほど作成したDynamoDBテーブルにアクセスするためのアクセスキーを作成する

AWSマネジメントコンソールにてIAM-ユーザーでアクセスキーの作成ボタンを押下すればキーが作成される

AWS CLIの設定ファイルにアクセスキーを記述

AWS CLIのインストール手順はこちら

brewでAWS CLIのインストール

インストールすると~/.aws/configという設定ファイルが作成されるのでこのファイルに先ほどのアクセスキーを記述する

[default]
aws_access_key_id=アクセスキー
aws_secret_access_key=シークレットアクセスキー
region=us-east-1

JavaからDynamoDBにアクセスする

build.gradle
dependencies {
    /* dynamoDB */
    implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.926')
    implementation 'com.amazonaws:aws-java-sdk-dynamodb'
}
application.yml

credential.profile:defaultは先ほど編集した~/.aws/configファイルのdefault部分を指します

amazon:
  dynamodb:
    endpoint: https://dynamodb.ap-northeast-1.amazonaws.com
  credential:
    profile: default
DBConfigクラス

こちらの記事のクラスをコピペしたが、@EnableDynamoDBRepositoriesが見つからずエラーになるので一旦消す

Spring Data DynamoDBでSpringからDynamoDBにアクセスする

Controllerクラス
package com.calico.controller;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.PutItemResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class JournalController {

    private static AmazonDynamoDB amazonDynamoDB = AmazonDynamoDBClientBuilder.standard().build();

    /**
     * 記録の登録
     *
     * @param request 記録(画面入力値)
     * @return response 登録結果
     */
    @RequestMapping(value = "/journals", method = RequestMethod.POST)
    public PutItemResult postJournal(@RequestBody journalRequest request) {
        Map<String, AttributeValue> registerData = new HashMap<>();
        registerData.put("user_id", new AttributeValue().withS(request.user()));
        registerData.put("category", new AttributeValue().withS(request.category().name()));
        registerData.put("eval", new AttributeValue().withS(String.valueOf(request.eval())));
        registerData.put("memo", new AttributeValue().withS(request.memo()));

        long currentTime = System.currentTimeMillis();
        registerData.put("register_date", new AttributeValue().withS(String.valueOf(currentTime)));
        registerData.put("update_date", new AttributeValue().withS(String.valueOf(currentTime)));

        // データ登録処理
        PutItemResult registerResult = amazonDynamoDB.putItem(new PutItemRequest("journal", registerData));
        return registerResult;
    }

    /**
     * 記録登録APIリクエストRecords
     *
     * @param user     ユーザーID
     * @param category 記録カテゴリー
     * @param eval     記録内容のポジティブ度(0-10 ポジティブになるほど値が大きくなる)
     * @param memo     記録内容
     */
    public record journalRequest(String user, Categories category, int eval, String memo) {
    }

    /**
     * 記録登録APIレスポンスRecords
     */
    public record journalResponse(String user, Categories category, int eval, String memo) {
        public String getUser() {
            return user;
        }

        public Categories getCategory() {
            return category;
        }

        public int getEval() {
            return eval;
        }

        public String getMemo() {
            return memo;
        }
    }

    /**
     * 記録カテゴリー
     */
    public enum Categories {
        Home,
        Job,
        Community,
        Other
    }
}

そしてハマる。

ビルドは通るのですが、SpringBootアプリケーションを実行するとDBドライバ設定がされてないエラーが出ます

application.ymlにspring.datasourceを設定すれば良さそうだけど、お手本が見つからない…明日調べます

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
    If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

Java Recordsの使いドコロを探る

Java15にスキルをアップデート

未だに仕事ではJava8を使い続けているのでそれにかまけてJavaキャッチアップできてなかった…

これからも仕事、趣味共にバックエンドはJavaを使っていくことになりそうなので今日久しぶりに最新Javaを調べてみた!

(資格もJavaGold11にアップデートしなきゃ)

新しいデータの扱い方 Records

JDK 14でPreview ReleaseされるRecordsが素敵なのでJShellで試そう

前から存在は知っていたのですが、試したことなかったのでRecordsを使ってみます!

getter/setterの記述不要でコンストラクタでフィールドに値をセットしてくれる仕組みなのかー

RESTAPIで使ってみた

私はJavaAPI開発で使うことが多いのでSpringBootのRESTAPIでRecordsを試してみました

(家事や仕事で気づいたこと、嬉しかったことなどを記録するアプリを想定)

  • 開発環境
    • MacbookAir
    • OracleJDK15
    • SpringBoot2.4.1

JDK15でもRecordsはプレビューリリースなのでbuild.gradleに以下の設定を記述しないとrecordsがコンパイルエラーになります

tasks.withType(JavaCompile) {
    options.compilerArgs += ['--enable-preview']
}

またIDEの実行環境設定でJVM引数が必要です f:id:butorisa:20201226235935p:plain

お試しなのでコントローラーにRecords書いちゃってます(本来はモデルクラスに書くべき?)

package com.calico.controller;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class JournalController {

    /**
     * 記録の登録
     *
     * @param request 記録(画面入力値)
     * @return response 登録結果
     */
    @RequestMapping(value = "/journals", method = RequestMethod.POST)
    public journalResponse postJournal(@RequestBody journalRequest request) {
        journalResponse response = new journalResponse(1, request.category(), request.eval(), request.memo());
        // TODO データ登録処理
        return response;
    }

    /**
     * 記録登録APIリクエストRecords
     * @param category 記録カテゴリー
     * @param eval 記録内容のポジティブ度(0-10 ポジティブになるほど値が大きくなる)
     * @param memo 記録内容
     */
    public record journalRequest(Categories category, int eval, String memo) {
    }

    /**
     * 記録登録APIレスポンスRecords
     */
    public record journalResponse(int id, Categories category, int eval, String memo) {
        public int getId() {
            return id;
        }

        public Categories getCategory() {
            return category;
        }

        public int getEval() {
            return eval;
        }

        public String getMemo() {
            return memo;
        }
    }

    /**
     * 記録カテゴリー
     */
    public enum Categories {
        Home,
        Job,
        Community,
        Other
    }
}

APIのリクエスト、レスポンスにRecordsを使っています

ちなみに実行結果はこちらです まだDB接続していないのでリクエストをそのままレスポンスするだけです f:id:butorisa:20201226235801p:plain

ここで注意なのですが、レスポンスのRecordsはgetterが必要です

getterを書かないと以下のエラーが発生します

org.springframework.http.converter.HttpMessageNotWritableException: No converter found for return value of type

これは@RestControllerをつけているAPIメソッドではSpringBootがレスポンスをjsonに変換する時にgetCategory()メソッドが必要なのに定義されていないからっぽい

Recordsが暗黙で用意するメソッドはgetCategory()ではなく、プロパティ名ままのcategory()だからSpringBootはgetterと認識してくれないみたい。。。

SpringBootがRecords対応してくれたらすごく便利かも!!