20210321のReactに関する記事は15件です。

【React】Redux Thunk での非同期処理

始めに

Redux の store に対して非同期処理で action を dispatch するときには工夫が必要になります。redux-thunk を用いて実現する方法を今回まとめてみました。
誤り等ありましたらコメントでご指摘いただけますと助かります :bow:

redux-thunk とは

Redux のミドルウェアのひとつで、これを用いることで純粋なオブジェクトだけでなく関数も action として dispatch することができるようになります。
非同期ロジックを組み込んだ関数を store が解釈できるようになり、適切なタイミングで dispatch を行ってくれます。
コンポーネント側からしたら ActionCreator の戻り値を dispatch しておけば後は store がうまくやってくれるので、コンポーネントのロジックの見通しが良くなります。

簡単な実例

export const App = () => {
  const tasks = useSelector(state => state.tasks);
  const dispatch = useDispatch();

  useEffect(() => {
    // APIからfetchしたデータをもとにstoreにdispatch
    dispatch(fetchTasks());
  }, [])

  return (
    <ul>
      {tasks.length && tasks.map(task => (
        <li key={task.id}>{task.name}</li>
      ))}
    </ul>
  );
}

例えば、上のようなコンポーネント。
Redux とのつなぎ込みは Hooks を使っており、また useEffect から dispatch を呼んでおります。

ActionCreator を下のように実装したとします。

export const fetchTasks = () => {
  fetch(api_path).then(response => {
    return response.json();
  }).then(tasks => {
    return { type: 'set_tasks', tasks: tasks };
  })
}

一見、action としてオブジェクトを return しているようにも見えますが、fetch API が同期的に返すのは Promise オブジェクトのため、dispatch が期待する結果にはなっていません。
下のエラーが発生します。

 Actions must be plain objects. Use custom middleware for async actions.

意図通りにやるには、promise が resolve されタスクキューに入ったコールバック処理が実行されるまで待ち dispatch を行う必要があります。

例えば、下のように非同期処理の結果を dispatch する関数を設けて、コンポーネントでこの関数を呼ぶやり方。

const fetchTasks = () => {
  fetch(api_path).then(response => {
    return response.json();
  }).then(tasks => {
    dispatch({ type: 'set_tasks', tasks: tasks })
  })
}

これでとりあえず動きはしますが、非同期処理と dispatch が混在していてコンポーネントの処理の見通しが悪くなってしまいます。また、dispatch 先の store に依存しているので mock でのテストなどがしずらそうです。

そこで、redux-thunk で Redux を拡張させ、非同期処理を含んだ関数をそのまま dispatch できるようにすることで処理をシンプルにしようとなります。

まずは、sotre 作成時にインポートした thunk を適用させます。

const store = createStore(reducer, applyMiddleware(thunk));

そしてActionCreator で、「dispatch を引数にとり、内部で(非同期処理後に) action をdispatch する関数」を返すようにします。

export const fetchTasks = () =>
  dispatch => {
    fetch(api_path).then(response => {
      return response.json();
    }).then(tasks => {
      dispatch({ type: 'set_tasks', tasks: tasks })
    })
  }

こうすることでロジックを Redux に寄せることができますね。
あとは冒頭記載通りにコンポーネントから dispatch(fetchTask()) をしてこの関数ごと dispatch してやれば、store 側でうまく対応をしてくれます。

拡張された store での処理

store では、まずミドルウェアが渡ってきた action がオブジェクトか関数かの判定を行います。オブジェクトであった場合は通常通り reducer に渡します。関数であった場合は dispatch を引数に関数を実行し (非同期処理などを経て)action が dispatch されるまで待ちます。そして、ここで disptach された apction がオブジェクトであったら今度は reducer に渡す、という流れです。
公式が貼っていたリンクの記事のアニメーションが分かりやすいです)
ちなみに、関数の第二引数として getState もとることができ、store の既存の state に関数内からアクセスして処理を分けることも可能です。

この流れを掴んだ上でソースを覗いてみると、この根幹の部分は意外とシンプルな実装であることが分かりますね。

scr/index.js
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

参考

https://github.com/reduxjs/redux-thunk
https://medium.com/fullstack-academy/thunks-in-redux-the-basics-85e538a3fe60
https://www.tohuandkonsome.site/entry/2019/02/05/231503
https://kde.hateblo.jp/entry/2019/02/14/220155

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

ReactのuseEffectとuseReducerを用いた非同期処理

はじめに

ReactのHooksAPIを学習したので、
useEffectとuseReducerを用いて非同期で取得したデータを表示してみる。

ソースコードと解説

import React, { useReducer, useState, useEffect } from "react";
import axios from "axios";

// stateの初期値を定義しておく
const initialState = {
  isError: false,
  data: {},
};

// Reducerを定義
// アクションは
// {type: 'FETCH_SUCCESS', payload: {name: aaa}}のように定義される
const fetchReducer = (state, action) => {
  switch (action.type) {
    // 通信成功時のアクションの場合
    case "FETCH_SUCCESS":
      return {
        isError: false,
        // stateのdataに通信で取得した値が格納される
        data: action.payload,
      };
    // 通信失敗時のアクションの場合
    case "FETCH_ERROR":
      return {
        isError: true,
        data: state,
      };
    default:
      return state;
  }
};

function App() {
  // useReducerにfetchReducerとinitialStateを登録
  // dataにReducerで扱っている状態が入り、dispatchにActionを与えることを通じてStateを更新
  const [data, dispatch] = useReducer(fetchReducer, initialState);

  // 入力中の値をuseStateを使って定義
  const [inputId, setInputId] = useState("");

  // APIで送信する値をuseStateを使って定義
  const [id, setId] = useState("");

  // useEffectでは副作用を伴う処理の関数を引数に与える
  // DOMのRenderが終了後に実行される
  // 第二引数に空配列を与えると初回Render後一度だけ実行される
  // 第二引数の配列に監視したい値を入れるとその値が変化した時だけ実行されるようになる
  useEffect(() => {
    // 初回Render時のidに何も値がない場合は実行しない
    if (id === "") return;

    // アンマウントされた時に仕様
    let unmounted = false;

    // axiosで非同期通信
    axios
      .get(`https://jsonplaceholder.typicode.com/todos/${id}`)
      .then((data) => {

        // アンマウントがされているなら状態を変更しない
        if (unmouted) return;

        // dispatchに通信成功のアクションを発行。payloadには通信で取得した値を入れる
        dispatch({ type: "FETCH_SUCCESS", payload: data });
      })
      .catch((error) => {
        console.log(error);

        // 通信失敗時のアクションを発行
        dispatch({ type: "FETCH_ERROR" });
      });

    // 入力途中の値を空にする
    setInputId("");

    // useEffectではundefinedか関数を返すことができる
    // 関数を返した場合クリーンアップ関数として登録される
    // クリーンアップ関数とはアンマウントされた時などに実行される
    // 今回は先程定義したunmoutedにtrueを代入する
    // アンマウントされた場合unmoutedにtrueが代入され、
    // 状態を変更してもRenderする画面無くなっているのでif(unmouted)return;で状態を変更しない
    return () => (unmounted = true);
    // 第二引数の配列にidを指定する。idが変更された場合useEffect関数が実行される。
  }, [id]);

  return (
    <div className="App">
      <form>
        <input
          type="text"
          value={inputId}
          // inputIdをバインディングし入力された値を監視
          onChange={(e) => setInputId(e.target.value)}
        />
        <input
          type="button"
          value="取得"
          onClick={() => {
            // クリックされたらidに入力中の値をセットする。
            // ここでidの値が変化するのでuseEffectが実行される
            setId(inputId);
          }}
        />
      </form>
      {/* useEffect内の通信で取得したデータがdispatchされReducerが状態を変化後Renderされる */}
      <div>{JSON.stringify(data.data.data)}</div>
    </div>
  );
}

export default App;

おわりに

Reduxなどを使わずに状態を手軽に管理できるのはかなりいいと思った。

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

Youtube APIを使って検索+再生をやってみた(React+Typescript)

はじめに

YoutubeAPI使ってみたいな〜と思ったので、React+Typescriptでサクッと実装してみました。
なかなか情報がなかったのでQiitaにまとめることにしました。
Reactのプロジェクトの導入についても軽く触れようかと思います。

実行環境

$ npm -v
7.5.1
$ create-react-app -V
3.3.1

作ったもの

「ヒカキン」のキーワードで検索して最も再生数が多い動画を表示します。
ちなみにこの動画は1.1億回再生されてました。すごい。(2021/3/21現在)
image.png

APIキーを取得する

公式サイトにも説明がありますが、こちらの記事を参考にAPIキーを取得しました。
Youtube Data API で取得したデータをPython(Pandas/matplotlib)で可視化する。

Reactのプロジェクトを作成する

まずはReact+Typescriptでプロジェクトを作りましょう

$ create-react-app youtube-project --template typescript

App.tsxの中身を書き換えます。

App.tsx
function App() {
  return (
    <div className="App">
      <p>Youtube APIのテストです</p>
      <Api />
    </div>
  );
}

Apiコンポーネントを作成します。まずは全文を載せておきます。解説はこの後

Api.tsx
import React, { useState, useEffect } from "react";

const YOUTUBE_SERACH_API_URI = "https://www.googleapis.com/youtube/v3/search?";
const API_KEY = "[取得したAPIキー]";

const Api = () => {
  const [videoId, setVideoId] = useState("");

  useEffect(() => {
    // クエリ文字列を定義する
    const params = {
      key: API_KEY,
      q: "ヒカキン", // 検索キーワード
      type: "video", // video,channel,playlistから選択できる
      maxResults: "1", // 結果の最大数
      order: "viewCount", // 結果の並び順を再生回数の多い順に
    };
    const queryParams = new URLSearchParams(params);

    // APIをコールする
    fetch(YOUTUBE_SERACH_API_URI + queryParams)
      .then((res) => res.json())
      .then(
        (result) => {
          console.log("API success:", result);

          if (result.items && result.items.length !== 0) {
            const firstItem = result.items[0];
            setVideoId(firstItem.id.videoId);
          }
        },
        (error) => {
          console.error(error);
        }
      );
  }, []);

  return (
    <iframe
      id="player"
      width="640"
      height="360"
      src={"https://www.youtube.com/embed/" + videoId}
      frameBorder="0"
      allowFullScreen
    />
  );
};

export default Api;

解説

Youtube Data APIで検索する

https://developers.google.com/youtube/v3/docs/search/list
Youtube Data APIのSearch listを使用して動画を検索します。

検索内容は全てクエリ文字列で与えるので、まずはクエリ文字列を定義します

    // クエリ文字列を定義する
    const params = {
      key: API_KEY,
      q: "ヒカキン", // 検索キーワード
      type: "video", // video,channel,playlistから選択できる
      maxResults: "1", // 結果の最大数
      order: "viewCount", // 結果の並び順を再生回数の多い順に
    };
    const queryParams = new URLSearchParams(params);

次にAPIコールを行います。
fetchを使った記載方法は公式ドキュメントに載っていた通りで実装しました。
https://ja.reactjs.org/docs/faq-ajax.html

    fetch(YOUTUBE_SERACH_API_URI + queryParams) //さっき作成したクエリ文字列
      .then((res) => res.json())
      .then(
        (result) => {
          console.log("API success:", result);

          //result.itemsの中に検索結果が格納されるので、最初の検索結果を取り出す
          //検索条件でmaxResultsを1にしているので、1件しか返ってこない
          if (result.items && result.items.length !== 0) { 
            const firstItem = result.items[0];
            // videoIdを使用して動画を取得するので保存しておく
            setVideoId(firstItem.id.videoId);
          }
        },
        (error) => {
          console.error(error);
        }
      );

これをuseEffectで囲むことで初回render後にAPIコールを実施します。

IFrame Player APIで動画を表示する

iframeタグを使用して動画を表示できます。
https://developers.google.com/youtube/iframe_api_reference?authuser=1

  return (
    <iframe
      id="player"
      width="640"
      height="360"
      src={"https://www.youtube.com/embed/" + videoId} //先ほど保存したvideoId
      frameBorder="0"
      allowFullScreen
    />
  );

最後に

Textinputを使用して任意の検索ができるようにしても面白いですね。
YoutubeAPIは他にも種類があるので色々試してみようと思います。
以上です。

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

React-BootstrapのForm.File要素の表示をカスタマイズする

概要

React-Bootstrapにはファイルのアップロードを行うForm.Fileの要素があるのですが、デフォルトのままだと下記の画像のようなシンプルなインプットのレイアウトになります。
スクリーンショット 2021-03-21 4.34.03.png
これを独自のインプットの表示にするやり方を、今回紹介します。

どのようにしたいのか

下記のTwitterのイメージにあるような、アップロードした画像をクリックするとファイルのダイアログが表示されるように、今回実装してみます。
スクリーンショットスクリーンショット 2021-03-20 16.43.04.png

対応方法

こちらのドキュメントにある通り、Form.Fileにcustomを設定します。その上で、FormFile.Input要素を配置して、これを画像の表示箇所に被るようにCSSで位置を調整します。画像が変わったときに走る関数は、FormFile.Input側に設定します。

実装サンプル

画像が未設定の時は、FontAwesomeIconの人アイコンを表示します。

sample.js
const [displayIconSrc, setDisplayIconSrc] = useState(undefined);

function onChangeIconImage(e) {
  if (e.target.files.length > 0) {
    setValue("iconImage", e.target.files);
    const imageFile = e.target.files[0];
    // FileReaderオブジェクトを使ってファイル読み込み
    var reader = new FileReader();
    // ファイル読み込みに成功したときの処理
    reader.onload = function () {
      setDisplayIconSrc(reader.result);
    };
    // ファイル読み込みを実行
    reader.readAsDataURL(imageFile);
  } else {
    setDisplayIconSrc(undefined);
  }
}

return(
  <Form.File id="iconImage" name="iconImage" type="file" custom>
    <FormFile.Input
      style={{ width: "70px", height: "70px" }}
      onChange={onChangeIconImage}
    />
    <div>
      {displayIconSrc ? (
        <img
          src={displayIconSrc}
          className="mt-2 mr-2 rounded-circle"
          style={{
            width: "70px",
            height: "70px",
            position: "relative",
            bottom: "75px",
          }}
        />
      ) : (
        <FontAwesomeIcon
          icon={faUser}
          className="fa-2x mt-2 mr-2"
          style={{
            width: "70px",
            height: "70px",
            position: "relative",
            bottom: "75px",
          }}
        />
      )}
    </div>
    <div
      style={{
        position: "relative",
        bottom: "50px",
        left: "170px",
      }}
    >
      アイコン画像を選択してください
    </div>
 </Form.File>
);

以下のような表示になります。画像をクリックするとファイル選択のダイアログが表示されます。
スクリーンショット 2021-03-21 4.44.43.png

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

【react-csv】CSVLink と CSVDownload の仕様違いで詰まった話(CSVDownloadでファイル名を指定したい)

はじめに

現在何を制作しているのかという話からすると、
とある情報をインターネット上でクローリングして取得し、その情報をブラウザ上で確認できるダッシュボードのようなWebアプリを制作しています。

ざっくりとした設計はこんな感じです。

  • Pythonでクローリングし、取得した情報をFirestoreに登録。
  • フロントエンドでFirestoreに登録された情報をフェッチし、一覧にして表示する。
  • 画面上から期間とカテゴリを選択し、データをcsvでダウンロードできるようにする。

今回はこのデータをcsvでダウンロードできるようにする部分のお話です。
react-csvというライブラリを使用したのですが、どこで詰まり、どのようにして解決したのかをまとめます。

まず react-csv とは

react-csv

React上で簡単にcsvエクスポート機能を実装できるライブラリです。
似たようなライブラリもいくつかありましたが、ダウンロード数を見る限りこれが一番メジャーなのではないかと思い選択しました。

使い方はドキュメントに記載されていますが、このようにとてもシンプルです。

sample.jsx
import { CSVLink, CSVDownload } from "react-csv";

// csvファイルの1行目にあたる部分を headers で指定できる 
headers = [
  { label: "First Name", key: "firstname" },
  { label: "Last Name", key: "lastname" },
  { label: "Email", key: "email" }
];

// csvファイルの2行目以降、要するにデータを指定できます。
// sampleでは以下のようにデータがベタ書きされていますが、
// 今回のアプリではFirestoreからフェッチしたデータをここで指定するイメージです。
data = [
  { firstname: "Ahmed", lastname: "Tomi", email: "ah@smthing.co.com" },
  { firstname: "Raed", lastname: "Labes", email: "rl@smthing.co.com" },
  { firstname: "Yezzi", lastname: "Min l3b", email: "ymin@cocococo.com" }
];

// 以下の2つからどちらか適切なほうを選択します。
<CSVLink data={data} headers={headers}>Download me</CSVLink>;
// または
<CSVDownload data={csvData} headers={headers} target="_blank" />;

CSVLink と CSVDownload の違いについて

  • CSVLinkはマウントされた<CSVLink>クリックすることでcsvがダウンロードされます。
    ですのでタグの中にクリックを促すメッセージを書くことになるでしょう。
    <CSVLink>"ここにクリックを促すメッセージを書く"</CSVLink>

  • CSVDownload<CSVDownload />がマウントされたタイミングで自動的にcsvがダウンロードされます。

どちらを使うべきか?

要件によって選択を求められますが、
今回は以下の点から<CSVDownload />が適切と判断しました。

  • ユーザーがカテゴリと期間を決定するタイミングで1クリックが発生する
  • csvとして吐き出せるのは当然フェッチが完了してから
  • となるとCSVLinkの場合はフェッチ後に再度1クリックを促すことになる

なのでフェッチできたかどうかのステートを用意し、
trueであれば<CSVDownload />をマウントするようにしました。

***.jsx
//省略

//フェッチの完了を判断するステート(完了後に true とする)
const [fetchDone, setFetchDone] = useState(false);

//省略

<button
  className={styles.fetchButton}
  onClick={() => {
    fetch(category, startDate, endDate);
  }}
>
  エクスポート
</button>
{fetchDone && <CSVDownload data={data} headers={headers} />}

カテゴリと期間を選択し、「エクスポート」というボタンをクリックするとフェッチを開始。
完了とともにCSVDownloadがマウントされ、csvファイルがダウンロードされるという流れです。

これは問題なく機能しました。

しかし、その後面倒な問題が発生しました。

CSVDownloadの問題点: csvのファイル名を指定できない

「ダウンロードするcsvのファイル名を指定したい」
という新しい要件が出たタイミングで問題に直面しました。

ちなみにreact-csvにはダウンロードするcsvのファイル名を指定するオプションが用意されています。

sample.jsx
<CSVLink filename={"my-file.csv"}>Download me</CSVLink>;

このようにfilenameで指定すればよいだけなのですが、
なぜかこのオプションはCSVLinkのみに提供されていて、CSVDownloadでは使えないのです。
(指定がない場合はハッシュ値のようなランダムなファイル名になります)

確認する限り2017年からリクエストがあげられているようですが、まだ機能として追加されていないようです。(2021年3月現在)
react-csv/issues/47

かと言って単純にCSVLinkに置き換えてユーザーに2回クリックを要求することは避けたい。
どうすべきか、、というところで一つ思いつきました。

「そうだ、フェッチ完了後にプログラム側でCSVLinkをクリックさせよう!」

CSVLinkは適当にどこか見えないところに置いておいて、
フェッチ完了後にこれをクリックさせれば、CSVDownloadとほぼ変わらない挙動を実現できるのではないか・・・

useRefを活用してcsvLinkをプログラム上からクリックする

リアルDOMを操作することになるのでuseRefを使います。
※ 命名のセンスは見逃してください..

***.jsx
const fetchDoneRef = useRef();

作成したfetchDoneRefCSVLinkに埋め込みます。

***.jsx
<button
  className={styles.fetchButton}
  onClick={() => {
    fetch(category, startDate, endDate);
  }}
>
  エクスポート
</button>
{fetchDone && <CSVLink
               data={data}
               headers={headers}
               filename={"hoge.csv"} //ファイル名を指定
               ref={fetchDoneRef} // refを指定
              />
}

そしてフェッチの処理が書いてある関数内で、フェッチ完了後にクリックする処理を追加します。

***.jsx
fetchDone && fetchDoneRef.current.link.click();

これで期間とカテゴリを選択し「エクスポート」ボタンをクリックすると...

hoge.png

うん、問題なく1回のクリックでダウンロードされました。動きはCSVDownloadとまったく同じです。
いい感じです!

そして実際は選択した期間やカテゴリによってファイル名を指定したいので、

***.jsx
filename={`${category}-${start}-${end}.csv`}

こんな感じでfilenameを指定すると..

finder.png

うん、やりたかったことができています!
少し力技っぽい感が否めないですが、できているので良しとします。

まとめ

CSVDownloadっぽい動きをさせたいけどファイル名も指定したい。
そんな時はuseRefを活用しCSVLinkをクリックすることで解決できますというお話でした。

csvエクスポートっていう要件自体がニッチだと思いますが、参考になれば幸いです。

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

Reactの関数コンポーネントとHooks #2

Part1の続きです。

メモ化フックとは

同じ結果を返す処理に関しては初回のみ処理を実行しておき、2回目以降は前回の処理結果を呼び出すことで毎回同じ処理を実行する事を無くす。これはプログラミングではメモ化と呼ばれる高速化のテクニックのひとつです。
これをReact Hooks上で簡単に利用できるのがuseMemouseCallbackです。
パフォーマンス向上のためにuseMemo、useCallbackは使われてるという事です。(つまりこの2つを使わなくても動作自体はします。)

パフォーマンス向上のポイントは、以下2つです。
・不要な再描画や再計算を無くす
・再レンダリングを抑える

レンダリングに時間のかかるコンポーネントや頻繁に再レンダリングが発生するコンポーネントに有効と言えます。

useMemo: 関数の結果をメモ化

//基本形、第2引数に入れた依存配列の値のいずれかが変化した場合にのみ再計算が実行される
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

実際のサンプル countが更新された時だけ再計算が実行される。そうでない場合はメモ化(保存)された値を返します。

const [count, setCount] = useState(0)
const hoge = useMemo(() => count * 2, [count])


useCallback: 関数のメモ化

親コンポーネントからコールバック関数をpropsとして受け取った子コンポーネントは再描画されてしまいます。関数の内容が同じだとしても、コンポーネントが再描画される度に再生成されるので、等価ではないからです。

それを防ぐために、useCallbackを使います。useCallbackでメモ化したコールバック関数を Propsで渡せば子コンポーネントの不要な再レンダリングをスキップできます。

//基本形、第2引数に入れた依存配列の値のいずれかが変化した場合にのみ変化します
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

使い方の注意点 1
・useCallbackでメモ化したコールバック関数を、それを生成したコンポーネント自身で利用はできない(つまりPropsで子に渡す場合のみ有効)
・親コンポーネントからuseCallbackを渡しても、子コンポーネント自体がReact.memoでメモ化されている必要がある(つまり、useCallbackとReact.memoはセットで使う)

↑の注意点を守らないと意味のないuseCallbackになってしまいます。

以下がその注意点を守ったサンプルになります。useCallback, React.memoを使ってなくても動作に変わりはありませんが、入力の度にconsole.log('InputBox is rendered')が呼ばれます。この程度の実装なら大した影響はないですが、コンポーネントが増えて大規模になってきたりするとパフォーマンスに悪影響をもたらす原因になるので、積極的に取り入れていきたいと思います。

//親コンポーネント
const App = () => {
  const [ input, setInput ] = useState("")
  const onChange = useCallback(
    (e) => {
      setInput(e.target.value)
    },
   []
  );
  return <InputBox onChange={onChange} />
}

//子コンポーネント
const InputBox = React.memo(props => {
  const { onChange } = props
  console.log('InputBox is rendered')
  return <input type="text" onChange={onChange}/>
})

使い方の注意点 2
useMemouseCallbackもuseEffectと同じく第2引数に何も入れなかった場合、新しい値がレンダーごとに毎回計算されてしまい、これらを使う意味がなくなるので、第2引数に依存する値(監視する値)もしくは空配列は必須です。


useContext
コンポーネントツリー上、親子関係にない(=ツリー上離れたところにいる)コンポーネント間で同じ値を共有する事ができる。
(propsでバケツリレーしないでも値の受け渡しができるReduxの様な使い方が可能です。)

//Provider(親)
export const Context = createContext({});
const hoge = { name: 'taro yamada' };

return (
  <Context.Provider value={hoge}>
    <ChildComponent />
  </Context.Provider>
)

//Consumer(子)
import { Context } from "...";

const value = useContext(Context);
//valueの中に{ name: 'taro yamada' }が入る

Consumer(子)側でデータを受け取るだけなら上記の様に簡単にできます。

単純にデータを渡すだけでなく、親からはコールバックを渡して、子の方からデータを送ってもらう事も可能です。

//Provider(親) addToData関数で子から値を受け取りStateに保存する
return (
  <Context.Provider sendData={hoge => addToData(hoge)}>
    <ChildComponent />
  </Context.Provider>
)

//Consumer(子) sendDataを使って親にデータを送る
import React , { useContext } from 'react';
import { Context } from "...";

const sendData = useContext(Context);
sendData(hogeData);

このuseContextを拡張すると、Redux無しでStoreを持つ事ができるのですが、それは長くなるので別の記事で紹介します。


useRef
ざっくりいうとDOMにアクセスするするためのHooksです。

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

普通に単一のDOMを参照したい場合はこの様に簡単です。useRefでref オブジェクトを生成し、それをDOMに渡せば参照する事ができます。

しかし、配列データから複数個のDOMを生成してる場合はちょっと工夫が必要です。

こちらの記事にそのやり方が書いてあるので参照してください。


まとめ

関数コンポーネントのメリットは、constructor, renderなどが不要で記述量を減らせて、慣れれば圧倒的に楽に書けるのでReactプロジェクトでこっちが主流になる理由が良くわかりました。ただまだ不完全な部分もあるので、適時追記修正していきます。

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

fetch API で POST したらエラー

Nest.jsで作ったAPIをReactからfetchしようとした時にエラーが出たので、その解決方法をメモします。

エラーの原因の結論は、データがうまく渡せていなかったからです。空で渡っていました。

エラー内容

実行したのは大体こんな感じです

App.tsx
async function addTodo() {
        const body = JSON.stringify({todo:todo,limit:limit})
        await fetch('http://localhost:3000/item',{
            method: 'POST',
            mode: 'cors',
            body: body,
        })
        .then(() =>{
            // 省略
        })
    }

POSTでfetchしたらエラーが出ました。

POST http://localhost:3000/item 400 (Bad Request)

原因

エラー内容をしっかりと見ていってわかったこと。

  • データが渡せていない(サーバー側で確認したら、空の配列になっていた)
  • サーバー側で「not null」のバリデーションを付けているため、空のデータに対してエラー

解決方法

ヘッダーにContent-Typeを指定して、Jsonであることを伝える事で解決

App.tsx
async function addTodo() {
        const body = JSON.stringify({todo:todo,limit:limit})
        await fetch('http://localhost:3000/item',{
            method: 'POST',
            mode: 'cors',
            body: body,
             // =====↓追加====
            headers:{'Content-Type': 'application/json'}
             // =====↑追加====
        })
        .then(() =>{
            // 省略
        })
    }

これで、データをサーバー側で受け取ることができました。
無事、エラーがなくなりました。

参考文献

Fetch の使用

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

~ has been blocked by CORS policy: ~ の対処法 React Nestjs

Nest.jsで作ったAPIをReactからfetchしようとした時に
「CORSポリシーによってブロックされています」とエラーが表示されたので、その解決方法をメモします。

エラー内容

Access to fetch at 'http://localhost:3000/item' from origin 'http://localhost:4000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

書いてあること

  • CORSポリシーによってブロックされています。
  • リクエストされたリソースに 'Access-Control-Allow-Origin' ヘッダーがありません。

CORSは日本語で「オリジン間リソース共有」です。
プロトコル、ポート番号、ホストが等しい場合が、同一のオリジンと言います。

セキュリティ上の理由で、ブラウザはオリジン間の通信を制限しています。
同一オリジンポリシー」に従って制限されています。

解決方法

サーバーサイドの実装

「Access-Control-Allow-Origin」を設定する必要があります。

(他のサイトでは⇩も設定が必要と書いて有りましたがエラーなく通信できました)
「Access-Control-Allow-Headers」 これについて後で調べてみます。

Nest.jsで実際に実装した例

main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // ======↓が該当箇所======
  app.enableCors({
    origin: 'http://localhost:4000',
    // allowedHeaders: 'Origin, X-Requested-With, Content-Type, Accept',  ←無しでもエラーなくなったが、他のサイトには必要と書いてあった↓
  });
  // ======↑が該当箇所======
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}

http://localhost:4000 」からのアクセスができるようになります。
自分の環境(クライアントサイドの)に合わせて、設定してください。
「origin:*」とすると全て許可です。

クライアントサイドの実装

fetchでAPIを叩く場合は、「mode cors」の設定が必要です。

Reactで実際に実装した例

App.tsx
await fetch('http://localhost:3000/item', {
      mode: 'cors'
    })
    .then(// 以下省略

これでエラーなく通信できるようになりました。

参考文献

なんとなく CORS がわかる...はもう終わりにする。
オリジン間リソース共有 (CORS)
同一オリジンポリシー

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

Fluxフローとは

はじめに

Reduxを勉強するにあたり、その元となるFlux思想を学習しました。
私の健忘録であり、誰かの役に立てれば嬉しいです。

Fluxとは

  • Facebook社が提唱している、クライアントサイドのWebアプリケーション開発のためのアプリケーション・アーキテクチャ(設計思想)です。
  • Reactとの併用を主に想定して作られているため、Reactとの相性が良いです。
  • 大きなメリットとしては、MVCモデルなどと比較したときに、データの見通しが良いという点です。

Fluxは次の3つの要素から成り立っています
1. Store: アプリケーションの状態データを保持するオブジェクト、状態の更新を実施する
2. Action: 状態の更新をするための指示内容を表すメッセージ
3. Dispatcher: Storeに対してActionによる更新指示を行う関数

単方向データフローは、ここにUIである「View」を加え、表示されます。↓

Action → Dispatcher → Store → View(React)

実際にアプリケーションに組み込んだ場合にはユーザーの操作に応じて新たなActionをdispatchし、状態と画面を更新していきます。↓

          ←   Action  ← 
               ↓                 ↑
Action → Dispatcher → Store → View(React)

処理の流れを一通り説明すると…

  1. ユーザーがviewを操作(onClick,onChange等)
  2. 更新するものをまとめてDispatcherへ渡す
  3. DispatcherからActionを渡されたStoreが、状態を更新
  4. Storeの状態が変化したことによりViewが書き換わる

おわりに

FluxはMVCモデルなどと比べ、単一方向にデータが進んでいくので見通しがよく、大規模開発になればなるほど効果を発揮します。
このFlux思想を元に作られたのがReduxということです。

参考

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

Gatsby+BulmaでNavbarが動かない問題

アプリのマニュアルを作成するのに流行りのSSGをやりたかったのでGatsbyに挑戦してみました。CSSは使い慣れたBulmaを選択。いつも通りBulmaのサイトにあるサンプルをコピペしたが、モバイルの場合にハンバーガーメニューが動かなかった。

コピペ+JSX向けに修正

returnの内容はJSXで記載する必要があるため、下記のように書き換えた。これだけでは動かない。

  • class要素をclassNameへ名称変更
  • リンクをaタグからLinkタグへ書き換え
  • hrやimgタグのような閉じる必要のないタグの末尾にスラッシュを追加
navigation.js
import React from "react";
import { Link } from "gatsby";

const Navigation = () => {
  return (
    <nav className="navbar" role="navigation" aria-label="main navigation">
      <div className="navbar-brand">
        <Link className="navbar-item" to="https://bulma.io">
          <img
            src="https://bulma.io/images/bulma-logo.png"
            width="112"
            height="28"
          />
        </Link>

        <a
          role="button"
          className="navbar-burger"
          aria-label="menu"
          aria-expanded="false"
          data-target="navbarBasicExample"
        >
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
        </a>
      </div>

      <div id="navbarBasicExample" className="navbar-menu">
        <div className="navbar-start">
          <Link className="navbar-item" to="#">Home</Link>

          <Link className="navbar-item" to="#">Documentation</Link>

          <div className="navbar-item has-dropdown is-hoverable">
            <Link className="navbar-link" to="#">More</Link>

            <div className="navbar-dropdown">
              <Link className="navbar-item" to="#">About</Link>
              <Link className="navbar-item" to="#">Jobs</Link>
              <Link className="navbar-item" to="#">Contact</Link>
              <hr className="navbar-divider" />
              <Link className="navbar-item" to="#">Report an issue</Link>
            </div>
          </div>
        </div>

        <div className="navbar-end">
          <div className="navbar-item">
            <div className="buttons">
              <Link className="button is-primary" to="#">
                <strong>Sign up</strong>
              </Link>
              <Link className="button is-light" to="#">Log in</Link>
            </div>
          </div>
        </div>
      </div>
    </nav>
  );
};

export default Navigation;

修正した内容

ハンバーガーメニューをタップした時にメニューが開くよう明示的にコーディングしてやる必要がある。
やったことは下記の通り。

  1. useStateでタップの状態を設定
  2. ハンバーガーメニューのaタグにタップイベントで上記のステートを変更する処理を追加。同時にclassNameでisActiveがtrueであればis-activeを追加する
  3. navbar-menuが設定されたdivタグに、モバイルの場合is-activeが付与されるように設定

2についてはNavbar burger、3についてはNavbar menuに説明がある。しっかりとドキュメントを読めば書いてあった。

navigation.js
import React from "react";
import { Link } from "gatsby";

const Navigation = () => {
  const [isActive, setisActive] = React.useState(false);

  return (
    <nav className="navbar" role="navigation" aria-label="main navigation">
      <div className="navbar-brand">
        <Link className="navbar-item" to="https://bulma.io">
          <img
            src="https://bulma.io/images/bulma-logo.png"
            width="112"
            height="28"
          />
        </Link>

        <a
          onClick={() => {
            setisActive(!isActive)
          }}
          role="button"
          className={`navbar-burger burger ${isActive ? 'is-active' : ''}`}
          aria-label="menu"
          aria-expanded="false"
          data-target="navbarBasicExample"
        >
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
        </a>
      </div>

      <div id="navbarBasicExample" className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
        <div className="navbar-start">
          <Link className="navbar-item" to="#">Home</Link>

          <Link className="navbar-item" to="#">Documentation</Link>

          <div className="navbar-item has-dropdown is-hoverable">
            <Link className="navbar-link" to="#">More</Link>

            <div className="navbar-dropdown">
              <Link className="navbar-item" to="#">About</Link>
              <Link className="navbar-item" to="#">Jobs</Link>
              <Link className="navbar-item" to="#">Contact</Link>
              <hr className="navbar-divider" />
              <Link className="navbar-item" to="#">Report an issue</Link>
            </div>
          </div>
        </div>

        <div className="navbar-end">
          <div className="navbar-item">
            <div className="buttons">
              <Link className="button is-primary" to="#">
                <strong>Sign up</strong>
              </Link>
              <Link className="button is-light" to="#">Log in</Link>
            </div>
          </div>
        </div>
      </div>
    </nav>
  );
};

export default Navigation;

参考

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

DjangoとReactでPDCAアプリを作る その4

まず私について

初めまして!

kenshoと申します。
2019年からプログラミングの勉強を本格的に始めて、web制作フリーランスを経験、その後はIT企業のインターン生として働きながら独学でwebエンジニアの勉強をしてきました。
2021年4月から新卒で上場企業のwebエンジニアとして働く予定です。

よかったらTwitterもやっておりますので気軽にフォローお願いします♪

Twitterアカウント↓
健将@WEBエンジニア×明大生

前回までのあらすじ

前回は、このPDCAアプリのログイン、登録、ホームのフロント部分を完成させた。
↓↓
DjangoとReactでPDCAアプリを作る その1

DjangoとReactでPDCAアプリを作る その2

DjangoとReactでPDCAアプリを作る その3

今回やること

git⬇️
https://github.com/kenshow-blog/workapplication

今回は、このアプリのPDCAのフロント部分をReactとTypeScriptで完成させる。

1.PDCA一覧画面
タイトルと日付を表示させている
スクリーンショット 2021-03-17 11.16.37.png

2.新規登録画面
スクリーンショット 2021-03-17 11.17.07.png

3.詳細画面
スクリーンショット 2021-03-17 11.17.26.png

4.編集画面
スクリーンショット 2021-03-17 11.17.46.png

アプリ構造

フロントエンド

.
├── README.md
├── package-lock.json
├── package.json
├── public
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── app
│   │   └── store.ts
│   ├── features
│   │   ├── auth #認証部分
│   │   │   ├── Auth.module.css
│   │   │   ├── Auth.tsx
│   │   │   └── authSlice.ts
│   │   ├── core #共通画面(ヘッダーなど)
│   │   │   ├── Core.module.css
│   │   │   └── Core.tsx
│   │   ├── home #ホーム画面
│   │   │   ├── Home.module.css
│   │   │   └── Home.tsx
│   │   ├── pdca #PDCA部分
│   │   │   ├── DeleteDialog.tsx
│   │   │   ├── Pdca.module.css
│   │   │   ├── Pdca.tsx
│   │   │   ├── PdcaDetail.tsx
│   │   │   └── pdcaSlice.ts
│   │   └── types.ts
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
└── tsconfig.json

pdcaSliceを作成する

今回コードがかなり長くなってしまいました?
その代わり全て詳細にコメントしておきましたのでご了承ください?

src/features/pdca/pdcaSlice.ts
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import axios from "axios";
import { ACTION, PDCA_STATE, CATEGORY, PDC } from "../types";

//バックからACTIONデータをGETしてる
export const fetchAsyncGetActions = createAsyncThunk(
    "get/actions", async () => {
        const res = await axios.get<ACTION[]>(
            `${process.env.REACT_APP_API_URL}/pdca/action/`,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
        return res.data;
    }
);
//バックからカテゴリーデータをGETしている
export const fetchAsyncGetCategory = createAsyncThunk(
    "get/category", async () => {
        const res = await axios.get<CATEGORY[]>(
            `${process.env.REACT_APP_API_URL}/pdca/category/`,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
        return res.data;
    }
);
//バックからpdcデータをGETしている
export const fetchAsyncGetPdc = createAsyncThunk(
    "get/pdc", async () => {
        const res = await axios.get<PDC[]>(
            `${process.env.REACT_APP_API_URL}/pdca/pdc/`,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
        return res.data;
    }
);

//バックのpdcへPOSTして、新しくpdcを作成している
export const fetchAsyncCreatePdc = createAsyncThunk(
    "post/pdc", async (pdc: PDC) => {
        const res = await axios.post<PDC>(
            `${process.env.REACT_APP_API_URL}/pdca/pdc/`,
            pdc,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
        return res.data;
    }
);

//バックのactionへPOSTして、新しくa(アクション)を作成している
export const fetchAsyncCreateAction = createAsyncThunk(
    "post/action", async (action: ACTION) => {
        const res = await axios.post<ACTION>(
            `${process.env.REACT_APP_API_URL}/pdca/action/`,
            action,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );

        return res.data;
    }
);

//バックのpdcへputメソッドで更新を命令している。
//EDIT機能の際に使用
export const fetchAsyncUpdatePdc = createAsyncThunk(
    "update/pdc", async (pdc: PDC) => {
        const res = await axios.put<PDC>(
            `${process.env.REACT_APP_API_URL}/pdca/pdc/${pdc.id}/`,
            pdc,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
        return res.data;
    }
);
//バックのactionへputメソッドで更新を命令している。
//EDIT機能の際に使用
export const fetchAsyncUpdateAction = createAsyncThunk(
    "update/action", async (action: ACTION) => {
        const res = await axios.put<ACTION>(
            `${process.env.REACT_APP_API_URL}/pdca/action/${action.id}/`,
            action,
            {
                headers: {
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
        return res.data;
    }
);

//バックのpdcへ削除を命令
//a(アクション)は「on_delete=models.CASCADE」としていて、
//外部キーとして結ばれているpdcがdeleteされると一緒にdeleteされるようにしてある
export const fetchAsyncDeletePdca = createAsyncThunk(
    "delete/pdca",
    async (id: number) => {
        const res = await axios.delete(
            `${process.env.REACT_APP_API_URL}/pdca/pdc/${id}/`,
            {
                headers: {
                    "Content-Type": "application/json",
                    Authorization: `JWT ${localStorage.localJWT}`,
                },
            }
        );
    return id;
    }
)

//それぞれの初期値を設定
export const initialState: PDCA_STATE = {
    actions: [
        {
        id: 0,
        action: "",
        pdca: 0,
        category_item: "",
        action_user: 0,
        category: 0,
        created_at: "",
        updated_at: "",
        },
    ],
    category:[
        {
        id: 0,
        item: ""
        },
    ],
    pdc: [
        {
        id: 0,
        userPdc: 0,
        title: "",
        plan: "",
        do: "",
        check: "",
        created_at: "",
        updated_at: "",
        },
    ],
    selectedPdc: {
        id: 0,
        userPdc: 0,
        title: "",
        plan: "",
        do: "",
        check: "",
        created_at: "",
        updated_at: "",
        },
    editedPdc: {
        id: 0,
        userPdc: 0,
        title: "",
        plan: "",
        do: "",
        check: "",
        created_at: "",
        updated_at: "",
    },
    editedAction: 
        {
        id: 0,
        action: "",
        pdca: 0,
        category_item: "未選択",
        action_user: 0,
        category: 5,
        created_at: "",
        updated_at: "",
        },

    editView: false,
    createView: false,

}

export const pdcaSlice = createSlice({
    name: "pdca",
    initialState,
    reducers: {
        selectPdca(state, action: PayloadAction<PDC>) {
            state.selectedPdc = action.payload
        },
        editPdc(state, action: PayloadAction<PDC>) {
            state.editedPdc = action.payload
        },
        editAction(state, action: PayloadAction<ACTION>) {
            state.editedAction = action.payload
        },
        setEditView(state) {
            state.editView = true;
        },
        resetEditView(state) {
            state.editView = false;
        },
        setCreateView(state) {
            state.createView = true;
        },
        resetCreateView(state) {
            state.createView = false;
        }
    },
    extraReducers: (builder) => {
        //HOME画面でactionsのリストを表示させる際に使用
        builder.addCase(
            fetchAsyncGetActions.fulfilled,
            (state, action: PayloadAction<ACTION[]>) => {
                return {
                    ...state,
                    actions: action.payload,
                }
            }
        );
        builder.addCase(fetchAsyncGetActions.rejected, () => {
            window.location.href = "/";
        });
        builder.addCase(
            fetchAsyncGetCategory.fulfilled,
            (state, action : PayloadAction<CATEGORY[]>) => {
                return {
                    ...state,
                    category: action.payload,
                }
            }
        );
        builder.addCase(
            fetchAsyncGetPdc.fulfilled,
            (state, action : PayloadAction<PDC[]>) => {
                return {
                    ...state,
                    pdc: action.payload,
                }
            }
        );
        builder.addCase(fetchAsyncGetPdc.rejected, () => {
            window.location.href = "/";
        });
        //更新前のpdc stateをmapで展開してその中で更新したデータのidと同じものを抽出して
        //更新後のデータに上書きさせるような処理をしている。action.payloadに更新後のデータが格納れている。
        builder.addCase(
            fetchAsyncUpdatePdc.fulfilled,
            (state, action : PayloadAction<PDC>) => {
                return {
                    ...state,
                    pdc: state.pdc.map((t) => 
                    t.id === action.payload.id ? action.payload : t)
                    ,
                    editedPdc: initialState.editedPdc,
                    editView: initialState.editView
                }

            }
        );
        builder.addCase(fetchAsyncUpdatePdc.rejected, () => {
            window.location.href = "/";
        });
        //更新前のactions stateをmapで展開してその中で更新したデータのidと同じものを抽出して、
        //更新後のデータに上書きさせるような処理をしている。action.payloadに更新後のデータが格納れている。
        builder.addCase(
            fetchAsyncUpdateAction.fulfilled,
            (state, action : PayloadAction<ACTION>) => {
                return {
                    ...state,
                    actions: state.actions.map((t) => 
                    t.id === action.payload.id ? action.payload : t)
                    ,
                    editedAction: initialState.editedAction
                }

            }
        );
        builder.addCase(fetchAsyncUpdateAction.rejected, () => {
            window.location.href = "/";
        });
     //pdcをcreateした後、現在のpdc state(配列)の先頭に新しく作成した、pdcを入れたいので、
        //配列の先頭にaction.payloadを入れている。
        //pdcを作成した後、actionも作成する。しかし、actionとpdcの紐付けがまだされていないので、
        //actionが作成される直前に、pdca情報をaction.payload.idで書き換えて、紐付けさせてる。
        builder.addCase(
            fetchAsyncCreatePdc.fulfilled,
            (state, action: PayloadAction<PDC>) => {
                return {
                    ...state,
                    pdc: [action.payload, ...state.pdc],
                    editedAction: {...state.editedAction, pdca: action.payload.id},
                    editedPdc: initialState.editedPdc
                }
            }
            )
            builder.addCase(fetchAsyncCreatePdc.rejected, () => {
                window.location.href = "/";
            });
            builder.addCase(
                fetchAsyncCreateAction.fulfilled,
                (state, action: PayloadAction<ACTION>) => {
                    return {
                        ...state,
                        actions: [action.payload, ...state.actions],
                        editedAction: initialState.editedAction,

                    }
                }
                )
                builder.addCase(fetchAsyncCreateAction.rejected, () => {
                    window.location.href = "/";
                });
                //filterを利用して、削除したデータのidをskipしている。
                //こうすることで現在のpdc stateから削除したpdcを弾くことができる
              //他のstateを初期化させておく
                builder.addCase(
                    fetchAsyncDeletePdca.fulfilled,
                    (state, action: PayloadAction<number>) => {
                        return {
                            ...state,
                            pdc: state.pdc.filter((t) => t.id !== action.payload),
                            editedAction: initialState.editedAction,
                            editedPdc: initialState.editedPdc,
                            selectedPdc: initialState.selectedPdc
                        }
                    }
                );
                builder.addCase(fetchAsyncDeletePdca.rejected, () => {
                    window.location.href = "/";
                });

    }
});

//下記の実装にて、それぞれのstateをリアルタイムで参照することができる
export const { selectPdca, setEditView, resetEditView, editPdc, editAction, setCreateView, resetCreateView } = pdcaSlice.actions;
export const selectActions = (state: RootState) => state.pdca.actions;
export const selectCategory = (state: RootState) => state.pdca.category;
export const selectPdc = (state: RootState) => state.pdca.pdc;
export const selectSelectedPdc = (state: RootState) => state.pdca.selectedPdc;
export const selectEditPdc = (state: RootState) => state.pdca.editView;
export const selectCreatePdca = (state: RootState) => state.pdca.createView;
export const selectEditedPdc = (state: RootState) => state.pdca.editedPdc;
export const selectEditedAction = (state: RootState) => state.pdca.editedAction;

export default pdcaSlice.reducer;

storeにpdcaSliceを登録する

src/app/store.py
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import pdcaReducer from '../features/pdca/pdcaSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer,
    //今回追記した部分↓↓
    pdca: pdcaReducer,
  },
});



1.PDCA一覧のコンポーネント、新規作成コンポーネントを作成する

ここも長い、、笑
コンポーネント部分だけ、引用いたしましたので、全部確認したい場合は、gitを参照してください?

src/features/pdca/Pdca.tsx
const Pdca: React.FC = () => {
    const classes = useStyles();
    const dispatch: AppDispatch = useDispatch();
    //pdcのstateをリアルタイムで確認する
    const pdc = useSelector(selectPdc);
    //新規作成画面に切り替えるか否かを確認している
    const createView = useSelector(selectCreatePdca);//true or false

    //新規作成画面で入力された値を確認する
    const editedPdc = useSelector(selectEditedPdc);
    const editedAction = useSelector(selectEditedAction);
    const category = useSelector(selectCategory);

    //入力された情報を、editPdc stateに入れる
    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        let value: string = e.target.value;
        const name = e.target.name;
        dispatch(editPdc({ ...editedPdc, [name]: value}));
    }
    //入力された情報を、editedAction stateに入れる
    const handleInputActionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        let value: string | number = e.target.value;
        const name = e.target.name;
        let test = dispatch(editAction({ ...editedAction, [name]: value}));
    }
    //選択されたカテゴリー情報を、editedAction stateに入れる
    const handleSelectCatChange = (
        e: React.ChangeEvent<{ value: unknown }>
        ) => {
            const value = e.target.value as number;
            dispatch(editAction({ ...editedAction, category: value }))
        }
    const create = async () => {
        //先にpdcを作成して、返り値をresultに格納する
        const result = await dispatch(fetchAsyncCreatePdc(editedPdc));
        if (fetchAsyncCreatePdc.fulfilled.match(result)) {
            await dispatch(fetchAsyncCreateAction({
                    id: 0,
                    action: editedAction.action,
                    pdca: result.payload.id,//返り値のidをここに入れることでpdcとactionの紐付けを行っている
                    category_item: editedAction.category_item,
                    action_user: 0,
                    category: editedAction.category,
                    created_at: "",
                    updated_at: "",
                }))
            }
     //新規作成画面の表示をfalseにして、pdca一覧画面に切り替える
        await dispatch(resetCreateView);
        window.location.reload();

    }
    //遷移した際に、実装される部分(useEffect)
    useEffect(() => {
        const fetchBootLoader = async () => {
            await dispatch(fetchAsyncGetCategory());
            await dispatch(fetchAsyncGetPdc());
        };
        fetchBootLoader();
    }, [dispatch])
    let datas = []
    //カテゴリーのselectを作成
    let catOptions = category.map((cat) => (
        <MenuItem key={cat.id} value={cat.id}>
            {cat.item}
        </MenuItem>
    ))


    return (
        <div className={styles.pdca_body}>
     {/*新規作成画面にするのか、pdca一覧画面にするのかをcreateView(true or false)を参照して切り替えてる */}
        {createView? 
        /*新規作成コンポーネントを表示*/
        <>
            <Link to="/pdca/" style={{ textDecoration: 'none' }}>
                <Button className={classes.newPdca} variant="contained" color="primary" onClick={() => dispatch(resetCreateView())}>
                    PDCA
                </Button>
            </Link> 

            <Button
            variant="contained"
            color="secondary"
            size="medium"
            onClick={create}
            className={classes.newPdca}
            >
                Create
            </Button>
        <br />

        <TextField
            variant="outlined"
            label="Title"
            type="text"
            name="title"
            InputProps={{ inputProps: { min: 0, max: 400 }}}
            InputLabelProps={{
                shrink: true,
            }}
            value={editedPdc.title}
            onChange={handleInputChange}
            className={classes.title}
            />
        <Grid container>

        <Grid item xs={12} sm={3}>
            <TextField
            label="Plan"
            type="text"
            name="plan"
            variant="outlined"
            multiline
            InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}
            InputLabelProps={{
                shrink: true,
            }}
            value={editedPdc.plan}
            onChange={handleInputChange}
            className={styles.pdca}
        />
        </Grid>
        <Grid item xs={12} sm={3}>
        <TextField
            variant="outlined"
            label="Do"
            type="text"
            name="do"
            multiline
            InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}
            InputLabelProps={{
                shrink: true,
            }}
            value={editedPdc.do}
            onChange={handleInputChange}
            />
        </Grid>
        <Grid item xs={12} sm={3} >
        <TextField
            variant="outlined"
            label="Check"
            type="text"
            name="check"
            multiline
            InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}
            InputLabelProps={{
                shrink: true,
            }}
            value={editedPdc.check}
            onChange={handleInputChange}
            />
        </Grid>
        <Grid item xs={12} sm={3}>
                <TextField
                variant="outlined"
                label="Action"
                type="text"
                name="action"
                multiline
                InputProps={{ inputProps: { min: 0, max: 400,className: classes.aPdc }}}
                InputLabelProps={{
                    shrink: true,
                }}
                value={editedAction.action}
                onChange={handleInputActionChange}
                />

                <FormControl className={classes.field}>
                <InputLabel>Category</InputLabel>
                <Select
                    name="category"
                    onChange={handleSelectCatChange}
                    value={editedAction.category}
                >
                    {catOptions}
                </Select>
            </FormControl>
        </Grid>
    </Grid>
        </>
        :
        /*pdca一覧コンポーネントを表示*/
        <>

                <Link to="/" style={{ textDecoration: 'none' }}>
                    <Button className={classes.rootHome} variant="contained" onClick={() => dispatch(resetCreateView())}>
                        HOME
                    </Button>
                </Link>
                {/*新規作成コンポーネントに切り替える際に、editPdc, editActionを初期化しておく */}
                <Button className={classes.newPdca} variant="contained" color="primary" onClick={ async () =>{
                    await dispatch(setCreateView())
                    await dispatch(editPdc(initialState.editedPdc))
                    await dispatch(editAction(initialState.editedAction))
                }}>
                    New PDCA
                </Button>

                {/*pdca一覧。クリックするとそれぞれの詳細ページに遷移するようにしてある。その際、詳細ページへ、表示させるpdcaのstateを添付している*/}
                <List component="nav" className={classes.root} aria-label="mailbox folders">
                    {pdc
                    .slice(0)
                    .reverse()
                    .map((pdc) =>(
                        <ListItem 
                            className={classes.listItem}
                            key={pdc.id}
                            >
                                <Link to={{   
                            pathname: `/pdca/detail/`,
                            state: {id: pdc.id,
                                userPdc: pdc.userPdc,
                                title: pdc.title,
                                plan: pdc.plan,
                                do: pdc.do,
                                check: pdc.check,
                                created_at: pdc.created_at,
                                updated_at: pdc.updated_at,}
                            }}
                            className={styles.ListItem}
                            >
                                <h3 style={{margin: "0", fontSize: "1em"}}>{pdc.created_at}</h3>
                                <h3 style={{margin: "0", fontSize: "1em"}}>{pdc.title}</h3>
                        </Link>
                        </ListItem>
                    ))}
                </List>
        </>
                    }


        </div>
    )
}

export default Pdca

ここで、地味につまづいたのが、TextFieldのカスタマイズ部分。
material-uiのTextFieldの入力部分のcssスタイルを変更したい場合、InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}のようにして、InputPropの中にcssをカスタマイズするコードを記述する。
コンポーネントの切り替えには、boolean型のstate(今回で言うとこの、createView)を作成しておくと何かと便利?

詳細ページ(閲覧、編集)のコンポーネントを作成する

src/features/pdca/PdcaDetail.tsx
const PdcaDetail: React.FC<Props> = (props) => {
    const classes = useStyles();
    //pdca一覧ページから添付されてきた、pdc stateを受け取ってpdc_propsに格納している
    let pdc_props = props.location.state;
    const dispatch: AppDispatch = useDispatch();
    //pdc state(全部)をpdcに格納している
    const pdc =  useSelector(selectSelectedPdc);
   //action state(全部)をpdcに格納している
    const actions = useSelector(selectActions);
    const category = useSelector(selectCategory);
    const editView = useSelector(selectEditPdc);
    const editedPdc = useSelector(selectEditedPdc);
    const editedAction = useSelector(selectEditedAction);
    const [commDlg, setCommDlg] = React.useState(false);

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        let value: string = e.target.value;
        const name = e.target.name;
        dispatch(editPdc({ ...editedPdc, [name]: value}));
    }

    const handleInputActionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        let value: string | number = e.target.value;
        const name = e.target.name;
        dispatch(editAction({ ...editedAction, [name]: value}));
    }

    const handleSelectCatChange = (
        e: React.ChangeEvent<{ value: unknown }>
        ) => {
            const value = e.target.value as number;
            dispatch(editAction({ ...editedAction, category: value }))
        }


    const update = async () => {
        //editedAction等が、編集されたであろうstateであるのでそれを引数に入れてstateの更新処理をしている
        await dispatch(fetchAsyncUpdateAction(editedAction));
        await dispatch(selectPdca(editedPdc));
        await dispatch(fetchAsyncUpdatePdc(editedPdc));
    }

    useEffect(() => {
        const fetchBootLoader = async () => {
            await dispatch(selectPdca(pdc_props));
            await dispatch(fetchAsyncGetActions());
            await dispatch(fetchAsyncGetCategory());
        };
        fetchBootLoader();
    }, [dispatch]);
    //そのpdcaデータのアクション情報をpdc_props.idを参照してactionContentに格納している
    const actionContent = actions.filter((act) => {
        return act.pdca === pdc_props.id
    })
    //カテゴリーのselectを作成している
    let catOptions = category.map((cat) => (
        <MenuItem key={cat.id} value={cat.id}>
            {cat.item}
        </MenuItem>
    ))
    return (
        <div className={styles.pdca_body}>
            <Link to="/" style={{ textDecoration: 'none' }}>
                    <Button className={classes.rootHome} variant="contained" onClick={() => dispatch(resetEditView())}>
                        Home
                    </Button>
                </Link>
                <Link to="/pdca/" style={{ textDecoration: 'none' }}>
                    <Button className={classes.editPdca} variant="contained" color="primary" onClick={() => dispatch(resetEditView())}>
                        PDCA
                    </Button>
                </Link>
          {/*editView(true or false)で詳細ページの編集画面と閲覧画面の表示を切り替えている*/}
                {editView? <>
                {/*編集画面*/}
                <Button
                className={classes.editPdca} 
                        variant="contained"
                        color="secondary"
                        size="medium"
                        onClick={update}
                        >
                            Update
                </Button>
                <br />
                </> :
                <>
                <Button variant="contained" className={classes.editPdca} color="secondary" onClick={ async () =>{
                    await dispatch(editPdc(pdc));
                    /*そのpdcaデータのa(アクション)がnullで保存されていなければ、その中身を表示する処理を書いている。*/
                    if(actionContent[0].action !== 'undefined') {
                        await dispatch(editAction(actionContent[0]));
                    }
                    await dispatch(setEditView())
                }}>
                    Edit
                </Button>
          {/*削除する際に削除確認コンポーネントを表示するようにしている*/}
                <Button className={classes.editPdca} color="inherit" variant="contained" style={{marginLeft:"10px"}}
                onClick={ 
                    () => {
                     setCommDlg(true)
                        }} >
                 DEL
             </Button>
             </>
                    }
            {/*削除確認コンポーネント*/}
            <DeleteDialog
                    msg={"Are you sure you want to permanently delete this files ?"}
                    isOpen={commDlg}
                    doYes={async () => {
                        await dispatch(fetchAsyncDeletePdca(pdc.id))
                        await setCommDlg(false)
                        window.location.href = "/pdca/";

                    }}
                    doNo={() => {setCommDlg(false)}}

                />
            {editView? 
            <>
            <TextField
            variant="outlined"
            label="Title"
            type="text"
            name="title"
            InputProps={{ inputProps: { min: 0, max: 400 }}}
            InputLabelProps={{
                shrink: true,
            }}
            value={editedPdc.title}
            onChange={handleInputChange}
            className={classes.title}
            />
            <Grid container>

                <Grid item xs={3}>
                    <TextField
                    label="Plan"
                    type="text"
                    name="plan"
                    variant="outlined"
                    multiline
                    InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}
                    InputLabelProps={{
                        shrink: true,
                    }}
                    value={editedPdc.plan}
                    onChange={handleInputChange}
                    />
                </Grid>
                <Grid item xs={3}>
                <TextField
                    label="Do"
                    type="text"
                    name="do"
                    variant="outlined"
                    multiline
                    InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}
                    InputLabelProps={{
                        shrink: true,
                    }}
                    value={editedPdc.do}
                    onChange={handleInputChange}
                    />
                </Grid>
                <Grid item xs={3}>
                <TextField
                    label="Check"
                    type="text"
                    name="check"
                    variant="outlined"
                    multiline
                    InputProps={{ inputProps: { min: 0, max: 400,className: classes.pdc }}}
                    InputLabelProps={{
                        shrink: true,
                    }}
                    value={editedPdc.check}
                    onChange={handleInputChange}
                    />
                </Grid>
                <Grid item xs={3}>
                    <TextField
                    label="Action"
                    type="text"
                    name="action"
                    variant="outlined"
                    multiline
                    InputProps={{ inputProps: { min: 0, max: 400,className: classes.aPdc }}}
                    InputLabelProps={{
                        shrink: true,
                    }}
                    {/*そのpdcaデータのa(アクション)が空欄の場合、valueに空欄を入れるようにしている*/}
                    value={editedAction?.action ?
                        editedAction.action : ""}
                    onChange={handleInputActionChange}
                    />
                <FormControl className={classes.field}>
                    <InputLabel>Category</InputLabel>
                    <Select
                        name="category"
                        onChange={handleSelectCatChange}
                        value={editedAction.category}
                    >
                        {catOptions}
                    </Select>

                </FormControl>
                </Grid>
            </Grid> </>:
            <>
            <h1 className={classes.pdcaTitle}>{pdc.title}</h1>
            <Grid container>
                <Grid item xs={3}>
                    <h2>P</h2>
                    <p className={classes.pdcaSelect}>
                    {pdc.plan}
                    </p>
                </Grid>
                <Grid item xs={3}>
                <h2>D</h2>
                <p className={classes.pdcaSelect}>
                {pdc.do}
                </p>
                </Grid>
                <Grid item xs={3}>
                <h2>C</h2>
                <p className={classes.pdcaSelect}>
                {pdc.check}
                </p>
                </Grid>
                <Grid item xs={3}>
                <h2>A</h2>

                {actionContent[0]?.action ?
                <>
                <p className={classes.pdcaSelectA}>
                {actionContent[0].action}
                </p>
                <strong>Category</strong>
                <br/>
                <p className={classes.category}>
                {actionContent[0].category_item}
                </p>

                </>
                : <></>
                }
                </Grid>

            </Grid>
            </>
            }
        </div>
    )
}

export default PdcaDetail

削除確認コンポーネントを作成する

この削除確認画面は、とても汎用的に使うことができるので、ここにて、シェアしておく

参考記事↓
React + Material-UIで確認ダイアログを作成してみた。

src/features/pdca/DeleteDialog.tsx
import React, {useEffect} from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';

export const DeleteDialog: React.FunctionComponent<
    { msg: any, isOpen: any, doYes: any, doNo: any}
    > = ({msg, isOpen, doYes, doNo}) => {

  const [open, setOpen] = React.useState(false)
  useEffect(() => {
    setOpen(isOpen)
  }, [isOpen])

  return (
      <div>
        <Dialog
            open={open}
            keepMounted
            onClose={() => doNo()}
            aria-labelledby="common-dialog-title"
            aria-describedby="common-dialog-description"
        >
          <DialogContent>
            {msg}
          </DialogContent>
          <DialogActions>
            <Button onClick={() => doNo()} color="primary">
              No
            </Button>
            <Button onClick={() => doYes()} color="primary">
              Yes
            </Button>
          </DialogActions>
        </Dialog>
      </div>
  )
}

これで、削除ボタンを押した際、「本当に削除しますか?? yes or no」
を表示して、yesならば、deleteメソッドを呼び出す、noなら削除確認コンポーネントを閉じることができる

これで、今回のアプリのフロント部分が全て完成した??

ここまでの感想

今回は、オリジナルアプリ2号機である.前回作成したTwitterアプリ( DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました(TwitterAPI)その1 )に比べて、外部apiを使用しなかった分サーバーサイドの実装は簡単だったが、フロント部分の実装にかなり世話を焼かされた。
しかし、前回のは逆に、フロント部分の実装がそれほど難しくなかったので、この二つのアプリでバランスよくスキルを磨けたのではないかと思う。

今後の展望

今後下記の機能をこのアプリに組み込めたらと思う。

1.一つのpdcaにつき、アクションを複数作成できるようにする
2.Google カレンダーAPIを使ってgoogleカレンダーとの紐付けをできるようにする。
3.他の生産性向上させてくれるフレームワークを組み込んで、機能を拡張させる

1が、できる前提で今回のアプリを開発していたのだが、終わってから早速拡張しようとしたところ、実装がとても難しく止まってしまっている状態である。
なんとか実装できるようにしていきたい。

最後に

ここまで読んでくださりありがとうございました!?‍♂️?‍♂️

また、Twitterでも日々の積み上げや、プログラミング学習についてのツイートをしておりますので、よかったらフォローと応援の程よろしくお願いします!?‍♂️

Twitterアカウント↓
健将@WEBエンジニア×明大生

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

改めてクロージャ(Clojure)とは何か【JavaScript】

JSの基本をやり直すためにJavaScriptにとってのクロージャについて解説。

よくある解説として、
「クロージャとは関数閉包のことです。(終わり)」
のようなWiki情報だけで理解できる人は前提知識がいろいろありそうな人でないと無理な気がします。
そこで自分なりにピンとくるように噛み砕いて見ます(間違えていたり語弊があったら指摘をいただきたいです)

もう少し優しくクロージャって何?

JavaScriptにおけるクロージャとは2つの要素が組み合わさったものとして考えます。

①関数内にスコープされたプロパティ(キーと値)のまとまったデータをもつこと
②関数の中に関数があって返り値にもなっていること

①+②がクロージャです。スコープとはご存知だと思いますがここでは宣言された関数の範囲のことを指します。
そして①をよりJavaScript以外でも抽象化させた専門用語として"環境"と呼ぶそうです(レキシカル環境とも呼ぶ)。

さて話は戻して、ここでクロージャ(関数clojure)のソースコードを載せました。

sample.js
const clojure = () => {
  let member = 0;

  const shareHouse = () => {
    return (member += 1);
  };

  return shareHouse;
};

上のソースコードでいうところの
「①関数内にスコープされたプロパティ(キーと値)のまとまったデータをもつこと」は、
宣言した変数memberのことです。(プロパティに置き換えると { member: 0 } )

「②関数の中に関数があって返り値にもなっていること」は、
関数shareHouseにあたります。

クロージャの使い方

先のコードだけ見てもどのように使うのかがわかりません。以下の手順で実行すると見えてきます。

①返り値が return shareHouse となっているのでそのまま 関数clojure をそのまま実行して 返り値(関数shareHouse) を取り出す。
②shareHouseをそのまま2回実行する

するとどうでしょうか。
shareHouseから見て外側に宣言されたmemberの値が更新されています。

sample.js
const clojure = () => {
  let member = 0;

  const shareHouse = () => {
    return (member += 1);
  };

  return shareHouse;
};

const invite = clojure();
console.log(invite()); // 返り値 1
console.log(invite()); // 返り値 2

当然ながら 変数member は 関数clojure の中でスコープされているのでよりグローバルな変数から代入されても値は保持されています。

sample.js
//省略

const invite = clojure();
console.log(invite()); // 返り値 1
console.log(invite()); // 返り値 2

let member = 10;
console.log(invite()); // 返り値 3

クロージャの使い道

「これの何が嬉しいの?」とここで疑問に思います。

①関数内にスコープされたプロパティ(キーと値)のまとまったデータをもつこと
②関数の中に関数があって返り値にもなっていること
と先に述べましたが、実際のコードにおいて②の関数とは計算だったり、APIを引っ張ってきたり、繰り返し処理をいれたりと機能が実装されています。

つまり、関数の処理結果を関数の範囲内に閉じ込めておきながら値を引っ張っていきたいケースで使うことができます。

省略したクロージャの書き方

しかし同じような手法はクラスにすれば同じことができます。(もしくはオブジェクト指向型の書き方で)
加えて、まだ直感的に理解しやすいのはクラスです。

しかしクロージャであればクラスよりもスッキリして書くことができます。

sample.js
const clojure = (member = 0) => () => member += 1;

const invite = clojure();
console.log(invite()); // 返り値 1
console.log(invite()); // 返り値 2

clojureの引数に member = 0 をおくことで変数の宣言ができます。
そして 関数clojure の返り値である () => member += 1; を 関数clojure外 から実行すれば先の例と同じように値を保持したまま値を変更していくことができました。

もしくは、

sample.js
const clojure = (member = 0) => (invited = 1) => member += invited;
const invite = clojure();
console.log(invite());
console.log(invite());

関数clojure の返り値にも引数を設定した形である (invited = 1) => member += 1; のような形で値を保持したまま値を変更していくこともできます。

ただし、返り値は一つしか取れないため複数の機能をもったものを実装したいのであればクラスの方が良いでしょう。

なんだかパッとしないなという人は下のコードを見れば理解しやすいはずです。

sample.js
const clojure = (member = 0) => {

  // const shareHouse = (invited = 1) => {
  //   return (member += invited);
  // };

  return (invited = 1) => {
    return (member += invited);
  };
};


// const clojure = (member = 0) => (invited = 1) => member += invited;
const invite = clojure();
console.log(invite());
console.log(invite());

改めてクロージャとは

ここまでくれば堅苦しい言い方、「クロージャとは関数閉包のことです。(終わり)」と言われても少しは言わんとすることがわかってきたのではないでしょうか。

更にここで、MDN Web Docsのクロージャの定義を引用します。
「クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。」

つまり、データ(プロパティのまとまり)の値の変化を関数の中の関数がコントロールしているといったところでしょうか。
ちなみにレキシカル環境とは何か、についてですが、レキシカル環境とは、静的スコープにおける関数の環境のことで、静的スコープとはJavaScriptのスコープの決まり方です。対照的に動的スコープがあります。
この点を掘り下げた内容に関してはJavaScript の原理:クロージャの真実が詳しいです。

なぜJavaScriptの関数を使ったクロージャは値を保持できるのか

sample.js
const clojure = () => {
  let member = 0;

  const shareHouse = () => {
    return (member += 1);
  };

  return shareHouse;
};

ここでの 変数member は 関数shareHouse の引数でもなければローカル変数でもありません(つまり自由変数)。しかし 関数shareHouse の中で値を参照できています。
これはJavaScriptのメモリ管理の仕組み上、クロージャにおける自由変数はメモリリークされずに残っているためです。

Reactの関数コンポーネントにおいてもクロージャの仕組みが用いられることがあります。

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

Reactとaxiosを用いて非同期でサインアウトする

実装方針

  • devise-auth-tokenで使用する、uid, client, access-tokenの3つが全て存在する場合のみサインアウトする
  • csrf-tokenのチェックを事前に行う
  • client, tokenをdestroyアクション内で削除
  • csrf-token,sessionをafter_actionで削除

フロントエンド

リクエストの送信

getCsrfToken() {
    if (!(axios.defaults.headers.common['X-CSRF-Token'])) {
      return (
        document.getElementsByName('csrf-token')[0].getAttribute('content')
      )
    } else {
      return (
        axios.defaults.headers.common['X-CSRF-Token']
      )
    }
  };


setAxiosDefaults() {
    axios.defaults.headers.common['X-CSRF-Token'] = this.getCsrfToken();
  };

userAuthentification() {
    if (axios.defaults.headers.common['uid'] && axios.defaults.headers.common['client'] && axios.defaults.headers.common['access-token']) {
      axios.defaults.headers.common['uid']
      axios.defaults.headers.common['client']
      axios.defaults.headers.common['access-token']
    } else {
      return null
    }
  }


if (this.props.content == 'SignOut') {
      this.setAxiosDefaults();
      this.userAuthentification()
      axios
      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})
  this.setAxiosDefaults();
  this.userAuthentification()

この2行でそれぞれcsrf-tokenとuser情報をrequest.headersにセットする。後者は新規登録時に発行される情報で、devise-auth-tokenを用いると発行されるもの。

def update_auth_header 
    @token = @user.create_token
    return unless @user && @token.client
    @token.client = nil unless @used_auth_by_token
    if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request
      auth_header = @user.build_auth_header(@token.token, @token.client)
      response.headers.merge!(auth_header)  
    else
      unless @user.reload.valid?
        @user = @user.class.find(@user.to_param) 
        unless @user.valid?
          raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}"
        end
      end
      refresh_headers
    end
  end

これを新規登録後に読み込むことで request.headerにclient、tokenをmerge。uidはデフォルトでresponse.headersに含まれている。

      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})

ここは普段どおりのaxiosのリクエスト。パラメータにはuserの識別子となるuidを渡す。

サーバーサイド

def destroy  
  @user = User.find_for_database_authentication(uid: request.headers['uid'])
  @token = request.headers['access-token']
  @client = request.headers['client']
  if @user && @client && @token
    @token.clear 
    @client = nil
    render_destroy_success
  else
    render_destroy_error
  end
end

リクエストヘッダーからuid(email)を取り出してインスタンスをDBから参照し、token、clientもそれぞれリクエストヘッダーから定義する。
user,token,client全てが存在しないとログアウトできないようにしている。
次にtokenとclientを削除することでクライアント側でuserが存在しない状態にし、ログアウト処理完了。

destroyアクション後にafter_actionで以下を読み込む

after_action :set_csrf_token_header
after_action :reset_session, only: [:destroy]

これでcsrf-tokenとsessionを削除

挙動

recommended-books-2-2.gif

感想

結局deviseのコントローラー0から改造することになってしまった。
まだ処理内容わかっていない点も多いのでこの後コメントアウトをつける作業に移る。

参考

devise-auth-token公式
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb

次やること

  • 結合テスト
  • コメントアウトつける
  • react-routerの導入
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Reactとaxiosを用いて非同期でdevise-auth-tokenを利用したのサインアウトを行う

実装方針

  • devise-auth-tokenで使用する、uid, client, access-tokenの3つが全て存在する場合のみサインアウトする
  • csrf-tokenのチェックを事前に行う
  • client, tokenをdestroyアクション内で削除
  • csrf-token,sessionをafter_actionで削除

フロントエンド

リクエストの送信

getCsrfToken() {
    if (!(axios.defaults.headers.common['X-CSRF-Token'])) {
      return (
        document.getElementsByName('csrf-token')[0].getAttribute('content')
      )
    } else {
      return (
        axios.defaults.headers.common['X-CSRF-Token']
      )
    }
  };


setAxiosDefaults() {
    axios.defaults.headers.common['X-CSRF-Token'] = this.getCsrfToken();
  };

userAuthentification() {
    if (axios.defaults.headers.common['uid'] && axios.defaults.headers.common['client'] && axios.defaults.headers.common['access-token']) {
      axios.defaults.headers.common['uid']
      axios.defaults.headers.common['client']
      axios.defaults.headers.common['access-token']
    } else {
      return null
    }
  }


if (this.props.content == 'SignOut') {
      this.setAxiosDefaults();
      this.userAuthentification()
      axios
      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})
  this.setAxiosDefaults();
  this.userAuthentification()

この2行でそれぞれcsrf-tokenとuser情報をrequest.headersにセットする。後者は新規登録時に発行される情報で、devise-auth-tokenを用いると発行されるもの。

def update_auth_header 
    @token = @user.create_token
    return unless @user && @token.client
    @token.client = nil unless @used_auth_by_token
    if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request
      auth_header = @user.build_auth_header(@token.token, @token.client)
      response.headers.merge!(auth_header)  
    else
      unless @user.reload.valid?
        @user = @user.class.find(@user.to_param) 
        unless @user.valid?
          raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}"
        end
      end
      refresh_headers
    end
  end

これを新規登録後に読み込むことで request.headerにclient、tokenをmerge。uidはデフォルトでresponse.headersに含まれている。

      .delete('/api/v1/users/sign_out', {uid: axios.defaults.headers.common['uid']})

ここは普段どおりのaxiosのリクエスト。パラメータにはuserの識別子となるuidを渡す。

サーバーサイド

def destroy  
  @user = User.find_for_database_authentication(uid: request.headers['uid'])
  @token = request.headers['access-token']
  @client = request.headers['client']
  if @user && @client && @token
    @token.clear 
    @client = nil
    render_destroy_success
  else
    render_destroy_error
  end
end

リクエストヘッダーからuid(email)を取り出してインスタンスをDBから参照し、token、clientもそれぞれリクエストヘッダーから定義する。
user,token,client全てが存在しないとログアウトできないようにしている。
次にtokenとclientを削除することでクライアント側でuserが存在しない状態にし、ログアウト処理完了。

destroyアクション後にafter_actionで以下を読み込む

after_action :set_csrf_token_header
after_action :reset_session, only: [:destroy]

これでcsrf-tokenとsessionを削除

挙動

recommended-books-2-2.gif

感想

結局deviseのコントローラー0から改造することになってしまった。
まだ処理内容わかっていない点も多いのでこの後コメントアウトをつける作業に移る。

参考

devise-auth-token公式
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb

次やること

  • 結合テスト
  • コメントアウトつける
  • react-routerの導入
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

このエラーの解決方法 Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>

Reactを書いていたら以下のようなWarningエラーが
Consoleに出たので備忘録として残しておく

今回出現したエラー文

Consoleに、以下のようなエラー文が出力された

Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>

エラー文をDeepLで翻訳すると・・・

<div>は<p>の子孫として表示できません

ん?どういうこと?

エラーの原因

先ほどのエラー文は
「HTMLの入れ子のルール」に関するエラー

今回は、pタグの中にdivタグを
入れ子にしているからWarningエラーが起きています

pタグの中にdivタグを
入れ子にすることはできないからです

解決方法

Reactの場合は関数コンポーネントの
returnでJSXを返している箇所のコードを見て

以下のような
HTMLの構成になっていないか確認する

const Sample = () => {
  return (
   <p>
     <div>pタグにdivは入れられない</div>
   </p>
 )
}

もしこのような構成になっているコードを確認したら
以下のようにHTMLの構成を変更してあげればOKです!

const Sample = () => {
  return (
   <div>
     <p>divタグにpタグは入れられる</p>
   </div>
 )
}

これで解決できるはずです!

ところで、なぜpタグにdivタグは入れられないのか?

この質問に対する回答としては

HTML5から「ブロック要素」と「インライン要素」
という概念が廃止されて

入れ子のルールが「コンテンツモデル」
という概念で決まるようになったからです

コンテンツモデルとは「その要素にはどのカテゴリーのコンテンツを入れていいか」を決めているルールのことで、タグの入れ子のルールは全てこの「コンテンツモデル」で決まっています

だからpタグの中にdivタグを入れると「あなたの書いたコードはコンテンツモデル的におかしいよ。なので警告出しとくから直してね」というWarningエラーが出たんですね。Reactは優しい!

この話題に関する詳しい説明は
この記事が非常にわかりやすいです。

暇な時に軽く目を通しておくと
HTMLをより深く理解できるのでおすすめです!

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