水無瀬のプログラミング日記

プログラムの勉強した時のメモ

React + ReduxでRssリーダーもどきを作ってみる

はじめに

前にReact + Reduxはやったことがあったけど、型を付けてなかったり、非同期処理を入れてなかったりするので、今回はその辺を踏まえてやってみる。
RSSリーダーとは言ったけど、
自分が望んだ形式で返ってくる前提で作っているのでRSSリーダーもどきとしてる。

TL;DR.

ソースコード

事前準備

まずはcreate-react-appを使ってプロジェクトの雛形を作るとこから。
作れたら必要なライブラリをインストールする

# プロジェクト作成
$ create-react-app react-sample -typescript
$ cd react-sample
# ライブラリインストール
# redux, react-reduxはreduxやるのに入れるいつもの
# redux-thunkはreduxで非同期処理をする際によしなにしてくれるライブラリ
$ npm install --save redux react-redux redux-thunk
# 必要に応じて型定義も
$ npm install --save-dev @types/redux @types/react-redux @types/redux-thunk

実装

Action

よくあるActionと非同期用のActionを定義しておく。
ThunkActionに型が付いてないところがあるけど、getStateextraArgumentの戻り値なので一旦付けないでおく。

import {Action, Dispatch} from 'redux';
import {RssReaderStates} from '../types/RssReaderStates';
import axios from 'axios';
import {ThunkAction} from 'redux-thunk';
    
// type定義
export enum RSS_READER_TYPES {
    ADD_LIST = 'ADD_RSS_LIST',
    FETCH = 'FETCH_RSS'
}
    
// アクション定義
interface IAddRssListAction extends Action{
    type: typeof RSS_READER_TYPES.ADD_LIST;
    payload: RssReaderStates[]
}
    
// ActionCreator群
// リストに追加用Action
export const addRssList = (rssList: RssReaderStates[]): IAddRssListAction => ({
    type: RSS_READER_TYPES.ADD_LIST,
    payload: [...rssList]
});
// Fetch用Action
export const fetchRss = (): ThunkAction<void, any, any, IAddRssListAction> => {
    return async (dispatch: Dispatch<IAddRssListAction>) => {
                // 今回取得先は`json-server`で返すJSONから取得
        const result = await axios.get<RssReaderStates[]>('http://localhost:4000/result');
        dispatch(addRssList(result.data));
    }
};
    
// Actionの型を返す
export type RssActions = (
    ReturnType<typeof addRssList>
    );

Reducer

よくあるReducerのままだと思う。
今回はAction1つしか用意していないので、意味ないけど一応switchで書いておく。

import {RssReaderStates} from '../types/RssReaderStates';
import {RSS_READER_TYPES, RssActions} from '../actions/RssReaderAction';
    
export const rssReaderReducer = (state: RssReaderStates[] | never = [], action:RssActions): RssReaderStates[] => {
    switch (action.type) {
        case RSS_READER_TYPES.ADD_LIST:
            return [...state, ...action.payload];
        default:
            return state;
    }
};
    
// =================
// 以降index.tsに記載
// =================
import {combineReducers} from 'redux';
import {rssReaderReducer} from './RssReaderReducer';
    
export default combineReducers({rssReaderReducer});

Store

今回ReduxThunkを使用したので、middleWareを追加しておく。

import ReduxThunk, {ThunkMiddleware} from 'redux-thunk';
import {applyMiddleware, createStore} from 'redux';
import rootReducer from '../reducers';
import {RssReaderStates} from '../types/RssReaderStates';
import {RssActions} from '../actions/RssReaderAction';
    
export default createStore(
    rootReducer,
    applyMiddleware(ReduxThunk as ThunkMiddleware<RssReaderStates, RssActions>)
);

Container

非同期のリクエストをする用のボタ用の結果を表示する用のリストの2つを用意する。

Button

import React from 'react';
import {fetchRss, RssActions} from '../actions/RssReaderAction';
import {connect} from 'react-redux';
import {ThunkDispatch} from 'redux-thunk';
import {RssReaderStates} from '../types/RssReaderStates';
    
type DispatchProps = {
    onClick: () => void;
};
// Action発火用ボタン
class FetchButton extends React.Component<DispatchProps> {
        // 初期表示用に読み込まれたときにイベントを発火させる
    componentDidMount(): void {
        this.props.onClick();
    }
    render() {
        return (
            <button
                onClick={() => {
                    this.props.onClick();
                }}
            >
                追加
            </button>
        );
    }
};
    
// props返却用
const mapDispatchToProps = (
    dispatch: ThunkDispatch<RssReaderStates, undefined, RssActions>
): DispatchProps => ({
    onClick: () => {
        dispatch(fetchRss());
    },
});
    
export default connect(null, mapDispatchToProps)(FetchButton);

List

受け取ったStateをPropsに変換して表示用のListに渡す。
正直、ここなくても良い気がするけど一旦噛ませない方法がわからなかったので入れておく。

import React from 'react';
import {connect} from 'react-redux';
import {RssReaderStates} from '../types/RssReaderStates';
import ListComponent from '../components/ListComponent';
import {RssReaderProps} from '../types/RssReaderProps';
    
const stateToProps = (state: RssReaderStates[]): RssReaderProps => {
    return {rssList: state};
};
export default connect(stateToProps)(ListComponent);

Component

表示用の処理群。表示する以外の役割を内容にする。
propsのrssListが何故か1段ずれて入ってくるので、対応している。(型内容は後述)
なんとかしたいけど、ぱっと解決できなかったのとサンプルだからという言い訳で放置する。
解決策ご存知の方教えて頂けると幸いです。

import React from 'react';
import {RssReaderStates} from '../types/RssReaderStates';
import {RssReaderProps} from '../types/RssReaderProps';
    
export default (props: RssReaderProps) => (
        <ul>
            {props.rssList['rssReaderReducer']
                .map((rss: RssReaderStates, index: number) =>
                    <li key={index}>
                        <a href={rss.url} target="_blank">{rss.title}</a>: {rss.description}
                    </li>)
            }
        </ul>
    );

型一覧

RssReaderProps

props用の型。
ここで指定した型と何故かずれるので↑のような解決策をしている。

import {RssReaderStates} from './RssReaderStates';
    
export type RssReaderProps = {
    rssList: RssReaderStates[]
}

RssReaderState

Stateの型。

export type RssReaderState = {
    title: string,
    description: string,
    url: string
};

実行結果

画像のような結果になればOK!
ちなみにボタンを押したところで同じ内容が追加されるだけになる。

まとめ

今回はReact + ReduxでRssリーダーを作ってみた。
Reduxで非同期処理ができないことがびっくりだったり、型付けたらReact,Reduxの型が難解過ぎて苦労した。
今回はなんとか動くように作っただけなので、また何か作って行きたいところ。
それでは、今回はこの辺で。 f:id:minase_mira:20190822222944p:plain

参考サイト