ふんばりフロントエンジニアのブログ

新米フロントエンジニアの備忘録です。ふんばり温泉タオル欲しい...

InteliJ IDEAでプロジェクトツリーの一部が見えなくなってしまったときの対処法

InteliJ IDEAを普段使いしているのですが、たまにタイトルの挙動のようになってしまうことがあります。

対処法は下記です。

1. 「cmd + ;」で「Project Preferences」を開く

2. 「Add Content Root」で開きたいプロジェクトを選択
f:id:ma1129nm:20200729153901p:plain

これで再びプロジェクトツリーが復活すると思います。

なんか意外と探すのに手間取りました...

reactstrapのModalコンポーネントでなぜかモーダルが表示されなかった時

今回React案件なのですが、その中でreactstrapのModalを使う機会が多くありました。

例えばエラーモーダルとかローディングモーダルとかですね。

その中でローディングモーダルがなぜか表示される時とされない時があってなんでだろ...って思っていたのですが意外とハマったので共有します。

まず流れとしては

  1. 非同期でデータ取得
  2. 取得開始時にstoreのloadingFlg=true
  3. 取得・初期処理終了後にstoreのloadingFlg=false

というよくある流れです。

で、一回目の取得時には特に問題なくモーダルが表示されるのですが二回目以降になると表示されなくなるんですよね。

なんでだろうなあとめちゃくちゃデバッグしてたのですが、非同期でstoreのstateを変える処理は特に問題なさそう。

じゃあModalコンポーネントに問題あるのかな、と思って確認したところどうやら「fade」がこの現象の元凶みたいでした。

<Modal isOpen={isLoading} className={className}>
      <ModalBody>
        <div className="pt-1 text-center">
          <div className="sk-three-bounce">
            <div className="sk-child sk-bounce1"></div>
            <div className="sk-child sk-bounce2"></div>
            <div className="sk-child sk-bounce3"></div>
          </div>
          <p>データを読み込んでいます...</p>
        </div>
      </ModalBody>
    </Modal>

上記を下記のように変更したらこの現象は解決しました。

<Modal isOpen={isLoading} className={className} fade={false}>
      <ModalBody>
        <div className="pt-1 text-center">
          <div className="sk-three-bounce">
            <div className="sk-child sk-bounce1"></div>
            <div className="sk-child sk-bounce2"></div>
            <div className="sk-child sk-bounce3"></div>
          </div>
          <p>データを読み込んでいます...</p>
        </div>
      </ModalBody>
    </Modal>

モーダルのタイムアウトが問題なのかな?(デフォルトでは300ms)と思ったのですが、100msとかにしてもダメだったのでfade自体がアカンみたい。

結局次フェーズに持ち越した項目でしたがとりあえず解決できてよかった..

【JavaScript】reduceとmapの使い方とスプレッド構文

最近Twitterを開設したのですが、色々勉強になることが多くもっと早めに初めておけばよかったな…と後悔しております...

と、そんな訳でTwitterで見かけたある事象に対して考えてみました。

const state = [
  { id: 1, deleted: false},
  { id: 2, deleted: false},
  { id: 3, deleted: false}
]

const comp = 2;

こんな感じのディクショナリでidがある数値と一致した時にdeleted=trueにしたい、という事象です。

投稿者の方は、下記のような処理をしてました。

state.map(item => item.id === comp ? {...item, deleted: true} : item )

ただ、なんか他の方法ないかなーという感じの投稿だったので当方reduceで実装してみました。

state.reduce((acc,val) => {
  if(val.id === comp) val.deleted = true;
  return [...acc, val];
}, [])

「mapの方が簡潔やな...」って感じですが笑

ただ、reduceだと色々カスタマイズできるのがおすすめポイントです。

例えばidだけの連想配列にしたいとか。

state.reduce((acc, val) => [...acc, {id: val.id}], [])

とにかくreduce便利なんです…

どちらもいいところがあるのでどっちがいいとかはなく、ユースケースによって使い分けるのが良さそうですね。

そしてスプレッド構文が登場しますが、これはES6で登場したもの。

iterable(反復処理可能)なものはこれで展開することができちゃいます。

オブジェクトの展開はすることができません(オブジェクトがiterabaleではないため)が、投稿者の方の例のようにオブジェクトの上書きをすることはできます。

Reactのチュートリアルとかやったことある人はstateの処理とかでやったことあるかも..?

改めて調べてみるとどんどん新しい書き方出てきて楽しいですね..

今回のサンプルは下記です。


See the Pen
reduce_with_spread
by nakajima masahiro (@nakajima333ta)
on CodePen.

react-scriptsでのenvファイル切り替えと思うように行かなかったconfig設定

現在Reactの案件を進行中です。

具体的にはCoreUIというUIフレームワークを使って管理画面を作成しています。

このCoreUIはcreate-react-appで作成されているので、ビルドにはreact-scriptsを使っているのですがreact-scripts(create-react-app)でenvファイルを切り替える際には下記のようにする必要があります。

{
// 省略
"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "start:develop": "env-cmd -f .env.development yarn start",
    "start:staging": "env-cmd -f .env.staging yarn start",
    "start:production": "env-cmd -f .env.production yarn start",
    "build:develop": "env-cmd -f .env.development yarn build",
    "build:staging": "env-cmd -f .env.staging yarn build",
    "build:production": "env-cmd -f .env.production yarn build"
  }
}

CRA.md(create-react-appのmdファイル)を見てみると、「envファイルの切り替えは「env-cmd」パッケージ使うといいぞ」的なことが書いてあったのでそうしてます。
「dotenv -e .env.development」とかでもいけるみたいですが。

で、envファイルの読み込みはうまくいったのですが「awsのconfig系を全部書くのだるいな...」と思った私は一つのjsにまとめてNODE_ENVで切り替えればいいかなと思い下記のようなコードを書きました。

const config = {
  "aws_project_region": "ap-northeast-1",
  "userPoolId": "",
  "userPoolWebClientId": "",
  "identityPoolId": "",
  "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
};

const awsmobile = () => {
  switch(process.env.NODE_ENV) {
    case 'development':
      config.userPoolId = 'ap-northeast-xxxxxxxxxx';
      config.userPoolWebClientId = 'xxxxxxxxxx';
      config.identityPoolId = 'ap-northeast-1:xxxxxxxxxx';
      break;
    case 'staging':
      config.userPoolId = 'ap-northeast-xxxxxxxxxx';
      config.userPoolWebClientId = 'xxxxxxxxxx';
      config.identityPoolId = 'ap-northeast-1:xxxxxxxxxx';
      break;
  }

  return config;
}

export default awsmobile;

こんな感じでやろうとしたら見事にハマりまして...

というのも、react-scriptsはstartかbuildでNODE_ENVが固定されて、上書きできないそうなんです。

github.com

なので、build(本番)なら「production」start(ローカル)なら「development」となってしまい、NODE_ENVでの切り替えは無理だとわかりました。

ということで大人しく.envに書きました笑

ただ、固定されることは意外と便利でローカルと本番で画像パス変えたいときとかは下記のように書くことができます。

const url = process.env.NODE_ENV === 'production'
          ? `/information/assets/${id}/${fileName}`
          : `${process.env.REACT_APP_S3_OBJECT_ROOT}/information/assets/${id}/${fileName}`

つまり大枠の環境は「start/build」で決めて、細部の環境(development/staging/production)は.envにまとめるといった形がいいのかな、と。


変なことしようとするもんじゃないですねほんと..

CognitoのライブラリはもうやめてAmplify使おうっていう話

最近Cognitoでユーザー認証して、各ユーザーグループに応じて叩けるAppSyncのクエリやらミューテーションやら変えたいな、という局面が出てきました。

ちょっとCognitoでユーザー認証する流れが初めてだったので色々調べてたんですが、結構最近の記事でもCogitoのライブラリ(SDKと言った方がいい?)「amazon-cognito-js」を使って実装してるものが多かった印象です。

AmplifyはCogitoやらAppSyncやら色々ひとまとめにしてくれているAWSサービスなのでそっち使った方がかなり楽できました。

ということでログイン処理を実装するまでの一例。

require('amazon-cognito-js');
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
const AWS = require('aws-sdk');

const POOL_DATA = {
  UserPoolId: 'ap-northeast-1_xxxxxxx',
  ClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
};

const login = () => {
    const inputData = userData;
    const authenticationData = {
      Username: inputData.username,
      Password: inputData.password
    };

    const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
    const userDatas = {
      Username: inputData.username,
      Pool: new AmazonCognitoIdentity.CognitoUserPool(POOL_DATA),
    };

    const CognitoUser = new AmazonCognitoIdentity.CognitoUser(userDatas);

    CognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: (result) => {
        const accessToken = result.getAccessToken().getJwtToken();
        console.log(result);
        console.log(accessToken);
      },
      onFailure: (err) => {
        console.log(err);
      },
    });

  }

cogitoのライブラリを使う場合は上記のような感じです。めっちゃ長いですよね。

import Amplify, { Auth} from 'aws-amplify';
import awsconfig from './aws-exports';

Amplify.configure(awsconfig);

const login = async() => {
  const res = await Auth.signIn('test', 'password');
}

amplify使うとこんな感じ。めちゃくちゃ短い。

また、AppSyncを使う際にも便利です。

import Amplify, {API, graphqlOperation} from 'aws-amplify';
import awsmobile from './aws-exports';

Amplify.configure(awsmobile);

const executeQuery = async (query, input = null) {
    return await API.graphql(graphqlOperation(query, input)).catch( (err) => err);
  }

queryに、そのままクエリを入れると動きます。めっちゃ簡単ですよね。

とにかく新しいサービスは一旦使ってみるのがいいですよね、というお話でした。

GraphQL+AppSyncで「currentObservable.query.getCurrentResult is not a function」と出た時の対処法

GraphQL使い始めてみたのですが、結構つまづいたのでメモ。

GraphQLクライアントとして「Apollo」を使ってみたのですが、素直に使うとどうやらappsyncとはあまり相性がよくなさそう。

解決方法

aws-appsync」を使わず、一からApollo Clientを作成する

詳細

基本的な使い方は下記のような感じでApollo Clientを作成して使用する方法だと思います。

import awsmobile from "./aws-exports"; //appsyncの設定ファイル
import AWSAppSyncClient from "aws-appsync";

const client = new AWSAppSyncClient({
    url: awsmobile.aws_appsync_graphqlEndpoint,
    region: awsmobile.aws_project_region,
    auth: {
        type: awsmobile.aws_appsync_authenticationType,
        apiKey: awsmobile.aws_appsync_apiKey,
    }
});

「おお、まとまっててええやんけ!」と思ったのですが、useQueryを使うと「currentObservable.query.getCurrentResult is not a function」のエラーが...

早速検索したところ下記の記事がヒットしました。

github.com

なるほど、依存関係を明示的に解決する方法があるのかと下記をpackage.jsonに追加しました。

"resolutions": {
    "apollo-client": "2.6.4"
  },

しかし、これを追加したところuseQueryは動くけどuseMutationは使えないというバグに出くわしました。

改めて調べてみたところ、これらのバグは「aws-appsync」が古い「apollo client」を参照しているため起こるバグのようです。

stackoverflow.com

上記に沿ってApollo Clientを一から作成する方法を取ってみました。

import awsmobile from "./aws-exports"; //appsyncの設定ファイル
import { ApolloLink } from 'apollo-link';
import ApolloClient from 'apollo-client';
import { createAuthLink } from 'aws-appsync-auth-link';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from "apollo-cache-inmemory";

const url = awsmobile.aws_appsync_graphqlEndpoint;
const region = awsmobile.aws_appsync_region;
const auth = {
  type: awsmobile.aws_appsync_authenticationType,
  apiKey: awsmobile.aws_appsync_apiKey
};
const link = ApolloLink.from([
    createAuthLink({ url, region, auth }),
    createHttpLink({ uri: url })
]);
const client = new ApolloClient({
    link,
    cache: new InMemoryCache(),
});


Apollo Clientの初期化は長くなりましたが、AppSyncとApolloを使うにはこうするしかなさそう...

早くアップデートしてもらえるといいなあ

ローカル画像から直接フォームのファイル(type=file)に画像情報を渡す方法

今回も小ネタです。
あんま必要とする人いないかもしれないですが、タイトルに際した状況に出くわしたのでメモ。

概要

画像プレビュー機能を実装する際には、「input(type=file)」のイベントをハンドリングして画像情報を引き出してプレビュー用の要素に画像を下記のように設定するような流れが多いですよね。

const target = document.getElementById('input_file');
const preview = document.getElementById('preview');

target.addEventListener('change', (e)=>{
        preview.classList.add('drop');

        const FR = new FileReader();
        FR.onload = (e) => {
            preview.querySelector('img').src = e.target.result;
        };
        FR.readAsDataURL(e.target.files[0]);
    });


FileReaderを使って画像のsrcに設定するわけですが、「 e.target.result」には「readAsDataURL」で生成されたbase64形式の文字列が返ってきます。

さて、こんな感じでプレビュー機能を作っていたわけですが、プロフィール画像の変更が不要な時にファイルのプレビュー出すのはいけるけどそのまま送信したら空になるしアカンなと気づいたわけです。

サーバーサイドで処理させてもいいんですが、ちょっとファイルの扱いがまだ勉強不足だったこともあり、フロントで対処しようとなった次第です。

実装コード

const src = thumb.src;
const pos = src.lastIndexOf('.');
if (pos === -1) return '';
const ext = src.slice(pos + 1); // 拡張子取得

fetch(src)
    .then(res => res.blob())
    .then( (blob) => {
        const file = new File([blob], `image.${ext}`);
        let list = new DataTransfer();
        list.items.add(file);

        target.files = list.files;
})

まあやり方があってるのかはわかりませんが..

上の四行目までは拡張子取得してるだけなので気にしなくていいです。

流れとしては、
1. 画像のURLを叩いてBlobに変換
2. blobから新しくFileオブジェクトを作成
3. DataTransferを作成し、そこに作成したFileオブジェクトを追加
4. ファイルinputのfilesにDataTransferを入れる

といった感じです。

まず、画像のsrcを取得し画像の保管場所を取得してそこをfetchで叩きます。

そうするとレスポンスをblobで取得できる「res.blob」を使ってblobに変換します。

blobはバイナリデータが格納されているImmutableなオブジェクトです。生データが入ってるということですね。

そしてFileオブジェクトはblobを引数に新しいファイルを作成することができます。

ファイルinputのfilesはDataTransferオブジェクトだったので、新しくDataTransferを作って渡している、というわけです。

やっぱサーバーサイドで処理すりゃよかったかな..と思いましたが、勉強になりました。