🤠

redux-thunkを用いて非同期処理を実行し、ChatGPTからのレスポンスを取得する方法

2023/03/21に公開

背景

前回は、ChatGPTのrubyライブラリを使用してrailsAPIをたたき、ChatGPTによるレスポンスを取得・表示させるアプリを作成しました。

前回作成したアプリのMVCのV(view)の部分をReactに書き換え、さらに Redux,redux-thunk を用いて非同期処理(今回の場合、railsAPIを叩いた処理)を実装しました。

今回はredux-thunkを用いてrailsAPIを叩いた非同期処理の部分を解説していきます。

react,redux,railsを用いたchatGPTによるアプリを作成することを検討している方の参考になれば幸いです。

完成動画

redux-thunkとは?

redux-thunkとは、Reduxアプリケーション内のAction Creatorに非同期処理のAPI通信や副作用を
実装するために使用されるRedux middlewareです。

通常、Action Creatorはプレーンなオブジェクトを返さなければならず、非同期処理を実装するこ
とはできません。

redux-thunkを用いることでAction Creatorで非同期処理を実装することができ、Action Creatorの
返り値(Actionのこと)をオブジェクトではなく 「関数」 として実装することができます。

では、今回なぜredux-thunkを使用したのか?
使用するメリットをかる〜くまとめました。

メリット①
非同期処理の管理: redux-thunkを使用することで、非同期処理をAction Creator内で管理することができます。
これにより、コンポーネントが非同期処理を実行する必要がなくなり、コンポーネントの責務が単純化されます。

メリット②
アクションのディスパッチ: redux-thunkを使用することで、Action Creator内で複数のアクションをディスパッチすることができます。
これにより、非同期処理が完了した場合にアクションをディスパッチし、Redux Storeを更新することができます。

メリット③
ネストされたコンポーネントへのデータの伝播: redux-thunkを使用することで、コンポーネント間でデータを伝播することができます。
Action Creatorで非同期処理を実行し、データを取得してRedux Storeに保存することで、どのコンポーネントからでもデータにアクセスすることができます。

これらの理由からredux-thunkはReduxを使用した非同期処理を実行する場合に非常に役立つと思い使用しました。
(他にもredux-thunkを使用するメリットがあるかもしれません。今回記載したメリットはあくまでご参考までに。)

解説

今回作成したアプリもトップページの入力フォームから値を入力し、ChatGPTからのレスポンス内
容を表示させるページを作成しました。
今回解説するコードは、redux-thunkを用いてrailsAPIを叩いた非同期処理の部分です。
コードは下記の通りです。

frontend/src/modules/formAction/formActions.js (入力フォームからデータを取得し、railsAPIを叩いている処理)

import axios from 'axios';
import { submitData } from '../submitData';

export const submitFormData = (formData) => {
  return (dispatch) => {
    return axios.post('http://localhost:3000/chat_response.json', { chats: formData.chats })
      .then((response) => {
        const res = response.data
        dispatch(submitData(res))
      })
      .catch((error) => {
        console.log(error)
      });
  }
};

少しわかりにくいかもしれませんが、redux-thunkは、return (dispatch) => { } という箇所で使
用されています。
この行は、submitFormData関数が関数を返すことを示しています。

上記のコードでは、Redux thunkが submitFormData 関数内で使用されています。

submitFormData は、thunkアクションクリエーターであり、非同期操作を含む関数を返すことができます。この関数は、dispatch関数を引数に取り、Reduxアプリケーションのstoreにアクションをディスパッチすることができます。具体的には、以下のようになっています。

export const submitFormData = (formData) => {
  return (dispatch) => {
    ...
  }
};

このように、 submitFormData は、関数を返す関数となっており、dispatch関数が引数として渡されています。これにより、 axios.post() が成功した場合に dispatch 関数を使用して、 submitData アクションをRedux storeに送信できます。

つまり、Redux thunkは、非同期操作を含むアクションをディスパッチするために使用され、このコードでは、 axios.post() メソッドが非同期処理を行っているため、Redux thunkを使用してアクションをディスパッチしています。

具体的には、 axios.post() は、非同期操作を行っており、サーバーからレスポンスを受け取るまでに時間がかかります。この場合、通常の同期アクションである dispatch(submitData(res)) を直接呼び出すと、レスポンスを取得するのに時間がかかり、レスポンスが返ってくる前にアクション(submitData関数のこと)が実行されてしまう可能性があります。

そこで、Redux thunkを使用することで、非同期処理が完了した後にアクション(submitData関数のこと)を実行するようにすることができます。

具体的には、 axios.post()then() メソッドの中で、以下のように dispatch(submitData(res)) を呼び出しています。

export const submitFormData = (formData) => {
  return (dispatch) => {
    ...
  }
};

このように、 submitFormData は、関数を返す関数となっており、dispatch関数が引数として渡されています。これにより、 axios.post() が成功した場合に dispatch 関数を使用して、 submitData アクションをRedux storeに送信できます。

つまり、Redux thunkは、非同期操作を含むアクションをディスパッチするために使用され、このコードでは、 axios.post() メソッドが非同期処理を行っているため、Redux thunkを使用してアクションをディスパッチしています。

また、上記のコードをリファクタリングしたものが下記になります。

frontend/src/modules/formAction/formActions.js

import axios from 'axios';
import { submitData } from '../submitData';

export const submitFormData = (formData) => async (dispatch) => {
  try {
    const response = await axios.post('http://localhost:3000/chat_response.json', { chats: formData.chats });
    dispatch(submitData(response.data));
  } catch (error) {
    console.error(error);
  }
};

以上が redux-thunkを用いてrailsAPIを叩いた非同期処理の部分の解説部分となります。

まとめ

今回は、redux-thunkを用いてrailsAPIを叩いた非同期処理の部分を解説しました。

以前からredux-thunkという名前は知っていたのですが、今回学習し直し改めてredux-thunkの重要性を痛感しました。

今後もrailsAPIを叩き非同期処理を実装することが多いと思うので今回のアプリの作成をきっかけにより深くredux-thunkについて理解していきたいです。

参考

https://github.com/reduxjs/redux-thunk

https://redux.js.org/usage/writing-logic-thunks

http://www.code-magagine.com/?p=13043

 

関連コード

訳あってソースコード全てを載せていませんが、今回関連しそうなソースコードを載せておきます。よかったら参考に。

 
frontend/src/containers/components/ChatsIndex.jsx (トップページ)

import React from "react";
import { useDispatch} from 'react-redux'
import { reduxForm, Field, change } from 'redux-form'
import { submitFormData } from "../../modules/formAction/formActions";
import { useNavigate } from 'react-router-dom';

const ChatsIndex = ({ handleSubmit }) => {

  const dispatch = useDispatch()
  const navigate = useNavigate();

  // APIを実行
  const onSubmit = async(formData) => {
    await dispatch(submitFormData(formData))
    .then((res) => {
      navigate('/results')
      dispatch(change('chatform', 'chats', ''))
    })
    .catch((error) => {
      navigate('/')
      dispatch(change('chatform', 'chats', ''))
    });
  };

  return (
    <div className="top">
      <form onSubmit={handleSubmit(onSubmit)}>
        <h3>ChatGPTに聞きたいことを入力してください。</h3>
        <Field name="chats" component="input" className="field-style" />
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

export default reduxForm({
  form: 'chatform',
})(ChatsIndex)

 
 
frontend/src/containers/components/Results.jsx(ChatGPTからのレスポンス内容を表示するためのページ)

import React from 'react'
import { useSelector } from 'react-redux'
import { Paper,Typography } from '@mui/material';
import ChatsIndex from './ChatsIndex';

const Results = () => {

  const chat_data = useSelector(state => state.chat_data)

  return (
    <div className='top'>
      <Paper>
        <Typography variant="h5">ChatGPTから返信が返ってきました。</Typography>
        <br></br>
        <div>{chat_data.data}</div>
        <br></br>
        <ChatsIndex />
      </Paper>
    </div>
  )
}

export default Results

 
 
frontend/src/modules/submitData/index.js

// initialState
const initialState = {
  data: []
}

// reducer
export const chat_data = (state = initialState, action) => {
  switch(action.type) {
    case 'SUBMIT_DATA':
      return { ...state, data: action.payload }
    default:
      return state
  }
}

// Action Creator
export const submitData = (value) => {
  return {
    type:  'SUBMIT_DATA',
    payload: value
  }
}

 
 
app/controllers/chats_controller.rb(railsAPIを叩いた時に実行されるアクション)

class ChatsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def chat_response
    @query = params[:chats]
    # 入力したテキストに対して返答する
    response = @client.chat(
      parameters: {
          model: "gpt-3.5-turbo",
          messages: [{ role: "user", content: @query }],
      }
    )
    @chats = response.dig("choices", 0, "message", "content")
    render json: @chats
  end
end

 
 
config/routes.rb(railsAPIを叩いた時に実行されるアクションのルーティング)

Rails.application.routes.draw do
  post 'chat_response', to: 'chats#chat_response'
end

Discussion