20190822のReactに関する記事は4件です。

React propsの初期値

Reactのpropsの初期値について

初めての投稿となります。
自分でも見返したときのメモのようになればいいなと思っています。

なんども同じものを入れていて初期値を設定できないかと調べました。

xxxx.defaultProps = {
      message: "xxxx"
    };

これでデフォルト値を設定できました。
値を入れなくてもmessage:に"xxx"が入ります。

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

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

はじめに

前にReact + Reduxはやったことがあったけど、型を付けてなかったり、非同期処理を入れてなかったりするので、今回はその辺を踏まえてやってみる。

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の型が難解過ぎて苦労した。
今回はなんとか動くように作っただけなので、また何か作って行きたいところ。
それでは、今回はこの辺で。

参考サイト

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

Class Componentライクに書けるFunction Component

動機

Reactに導入されたHooks APIは色々と便利です。
ところが、FC自体はTypeScriptのクラス機能や型付けとあまり相性が良くないように思います。1
そこで、Class Componentのときに出来た機能をちょっと移植しつつ、FCとして使えるようにするクラスを自作してみました。

コード

import * as React from 'react';

/** 関数の第1引数の型を取得します */
type FirstArg<T> = T extends (first: infer A, ...arg: any[]) => any ? A extends never ? never : A : never;
/** 第2引数で指定したプロパティ名を省略可能にします */
type Optional<Original, Names> = { [P in Extract<keyof Original, Names>]?: Original[P] } & Pick<Original, Exclude<keyof Original, Names>>;
/** defaultPropsの戻り値を省略可能にしたPropsを作ります */
type RenderProps<Original, Component extends { defaultProps: { [P in keyof Original]?: Original[P] }; }> = React.PropsWithChildren<Optional<Original, keyof Component['defaultProps']>>;

export abstract class FunctionComponent<Props = {}, State = {}> {
  private _props!: React.PropsWithChildren<Props>;
  private _state!: State;
  private _dispatchers!: { [P in keyof State]: React.Dispatch<State[P]>; };

  /** defaultPropsのチェックで使うので、publicでないといけない */
  public get props() {
    return this._props;
  }

  protected get state() {
    return this._state;
  }

  protected get dispatchers() {
    return this._dispatchers;
  }

  /**
   * 利用側で使用するJSX要素を生成し返します。
   * この部分がFunction Componentです。
   */
  public readonly toJSX = (props: RenderProps<Props, this>) => {
    this._props = props as any;
    this.initializeState();
    return this.render();
  }

  /**
   * Class ComponentやFunction Component標準のdefaultPropsと同じように実装をしてください。
   * この戻り値のプロパティ名は省略可能扱いになります。
   *
   * 戻り値をPartial<Props>にすると、全てのプロパティが省略可能になってしまい旨味がないので、指定していません。
   */
  public get defaultProps() {
    return {};
  }

  /**
   * ここで指定された値がstateの初期値になります。
   * stateを使用する場合、必ず全ての初期値を返すよう実装してください。
   */
  public get defaultState(): State {
    return {} as State;
  }

  /** このメソッドを旧来のrenderと同じように実装してください。 */
  protected abstract render(): ReturnType<React.FC>;

  /** Class ComponentのsetState相当のことをします。 */
  protected setState<T extends { [P in keyof State]?: State[P] }>(newState: T) {
    (Object.keys(newState) as any as (keyof State)[]).forEach(k => this.dispatchers[k](newState[k] as any));
  }

  private initializeState() {
    const state = this.state || this.defaultState;
    this._state = {} as any;
    this._dispatchers = {} as any;
    Object.keys(state).forEach(k => [this._state[k], this._dispatchers[k]] = React.useState(state[k]));
  }

  private __inited = this.__init();
  /** デバッグ用に生成関数にクラス名をつけたり、defaultPropsを繋げます。 */
  private __init() {
    const desc = Object.getOwnPropertyDescriptor(this.toJSX, 'name');
    if (!desc) {
      return;
    }
    desc.value = (this as any).constructor.name;
    Object.defineProperty(this.toJSX, 'name', desc);
    (this.toJSX as any).defaultProps = this.defaultProps;
  }
}

/** 
 * 利用側でクラスのインスタンスを都度生成しなくてもFCにしてくれるサポート関数です。
 * Propsに定義されていないキーが存在する場合や、型に互換性がない場合、ここでエラーが出ます。
 */
export const convertToFC = <
  T extends {
    toJSX: (props: any) => ReturnType<React.FC>,
    defaultProps: { [K2 in K]?: T['props'][K2] },
    props: any;
  },
  K extends (keyof T['defaultProps'] extends keyof T['props'] ? keyof T['defaultProps'] : '__notExistsKey')
>(clazz: new () => T) => {
  const func = (props: FirstArg<T['toJSX']>) => {
    const instance = React.useMemo(() => new clazz(), []);
    return <instance.toJSX {...props} />;
  };
  func.displayName = `${clazz.name}_Creator`;
  return func;
};

基本的に、Class Componentと同じようにpropsstateを設定しつつ、renderの戻り値をFCにして返しています。
そのままだとインスタンスを都度生成しないといけないため、convertToFC関数を用意しています。

使い方

試しに基底となるクラスを作成します。
今回はpropsにnametype、Stateにexamined(診察したかどうか)を持たせています。
ついでにHooks APIとしてuseMemoも使ってみました。

継承したクラスではtypeの初期値を返すdefaultTypeを実装させるようにします。

animal_component.tsx

import * as React from 'react';
import { FunctionComponent } from './function_component';

interface IProps {
  name: string;
  type: string;
}

interface IState {
  examined: boolean;
}

export abstract class AnimalComponentClass extends FunctionComponent<IProps, IState> {
  public get defaultProps() {
    return {
      type: this.defaultType(),
    };
  }

  public get defaultState() {
    return {
      examined: false,
    };
  }

  protected render() {
    const { name, type } = this.props;
    const { examined } = this.state;
    return React.useMemo(() => (
      <div onClick={this.onClick}>
        <div>{name}</div>
        <div>{type}</div>
        <div>{examined ? '' : ''}</div>
      </div>
    ), [examined]);
  }

  protected abstract defaultType(): IProps['type'];

  private onClick = () => {
    this.setState({ examined: true });
  }
}

そしてこれを継承したコンポーネントを作成します。
typeの初期値をもたせるほか、メソッドとしてcryを実装しました。

dog_component.tsx
import { AnimalComponentClass } from './animal_component';
import { convertToFC } from './function_component';

export class DogComponentClass extends AnimalComponentClass {
  public cry() {
    console.log('bow');
  }

  protected defaultType() {
    return '';
  }
}

const component = convertToFC(Class);

最後に、これを使ってみましょう。


const Hospital = () => {
  // インスタンスを生成することで、メソッドを通した操作が可能になります。
  const instance = React.useMemo(() => new DogComponentClass(), []);
  React.useEffect(() => instance.cry(), []);

  // convertToFCを通したものか、インスタンスのtoJSXを呼ぶことでJSX形式として扱うことが可能になります。
  // defaultPropsで指定しているtypeは省略可能とみなされています。
  return (
    <div>
      <DogComponent name='太郎' />
      <instance.toJSX name='次郎' type='レトリーバー' />
    </div>
  );
}

これを実行すると、以下のようになります。

image.png

どちらの方法でも正しく描画出来ていること、typeを省略した場合defaultPropsが機能していることが分かると思います。
また、クリックすることでstateの書き換えも行えます。

image.png

下の方について、だった部分がに書き換わりました。こちらも上手く機能しています。

型の警告について

必須のnameプロパティを省略した場合、以下のように警告が表示されます。

image.png

defaultPropsに、Propsに存在しないプロパティtestを指定した場合、以下のように警告が表示されます。

image.png

同様に、defaultPropstypenullを指定するなど、型違反を起こした場合、以下のように警告が表示されます。

image.png


  1. Class Componentのときは出来た、defaultPropsで返しているプロパティを判別してくれる型定義になっていないとか、stateやprops引き連れ回すときに毎回定義書かなきゃいけないので面倒等。 

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

Reactのアーキテクトを考える道のり - その1 (最小の構成)

この記事を書くに至ったきっかけ
新システムを構築するにあたり、候補の一つであるReactにまずは慣れてみようと思い、自身でアプリケーションを構築しました。
かなりの試行錯誤の連続の結果、ある程度の形が出来てきたので、これまでの経緯を纏めておきたいと思います。

現時点では、下記の構成を着地点としています。

各ライブラリを使うに至った経緯などは、後述(または別記事に記載)します。

Name Note ドキュメントURL
React これがなければ始まらない。 https://reactjs.org/
Typescript 所謂、AltJS。解説は巷に溢れているが、静的な型付をすることが特徴。 https://www.typescriptlang.org/
material-ui 簡単にそれなりの画面を作る為のコンポーネント群を提供してくれる。 https://material-ui.com/
react-redux 非常にわかりやすい記事があるので添付
たぶんこれが一番分かりやすいと思います React + Redux のフロー図解
https://redux.js.org/
https://react-redux.js.org/
typescript-fsa(-reducers) TypescriptでReduxを使うときのボイラーテンプレートです。これにより、記述を簡略化出来ます。 https://github.com/aikoven/typescript-fsa
https://github.com/dphilipson/typescript-fsa-reducers
redux-saga React Redux構成でAPI通信を行う為のMiddlewareです。 https://redux-saga.js.org/
react-router SPAで画面遷移をさせる為のデファクトスタンダードライブラリ。これを使うことで、ブラウザの「戻る」ボタンなども使える様になる。 https://reacttraining.com/react-router/
Storybook 作成したコンポーネントを単体で見られる様にしてくれるツール。
(このツール自体はReactには限らない。vue.jsなどでも利用可能。)
https://storybook.js.org/

コンポーネントのディレクトリ構成にはAtomic Designを採用。

上記の構成に至った経緯

まずは、Reactを動かしてみるところから。

チュートリアルをするにも、手元で動く環境がなければ始まりません。
何はともあれ、create-react-appをインストールしましょう。
簡単にReactを始めることが出来ます。

#自分自身のローカルにcreate-react-appをインストール
npm install -g create-react-app

#プロジェクトを`Typescript`で作成。
create-react-app sampleProject --typescript

本当に何てことことなかったです。

ここからmaterial-uiも取り入れつつ、チュートリアルをこなし、ちょっとしたコンポーネントを作ってみたりしました。

ここで得た感想

React

pros

  • Webページの要素を小さく部品化して、再利用可能とするコンポーネント指向が、プログラマーとして非常に馴染む
  • 冗長になりがちな記述をまとめることで、出来るだけDryな状態を保ちながら、統一したWebページを作れる
  • Typescript JSX記法も特に違和感なく書けそう

cons

  • コンポーネントが増えてきたときの管理が大変そう。(似ているコンポーネント作っちゃいそう。)
  • stateとpropsが便利だが、viewの中にビジネスロジックを入れ込んで複雑にしてしまいそう

Typescript

pros

  • Visual Studio Codeとの相性がたまらなく良い!インテリセンスが効くのでストレスなくプログラムできる
  • 型チェックが効いているから、子コンポーネントへのプロパティ渡し漏れや誤った型の変数を渡さずにすむ。(つまらないミスをしなくなる)
  • Nullundefinedの可能性がある場合もコンパイルエラーになるので、Guardするなで、事前に対処できる。

cons

  • 多くのサンプルがjsで書かれているので、そのままでは動かないことが多い。(サンプルの絶対数も少ない)
  • 導入するライブラリによっては、typescriptの型定義ファイルがない。その場合、自作しなければいけない。(メジャーなものは、だいたいある)

Material UI

pros

  • Sampleも豊富に提供されているので、特に迷うことなく実装できる。
  • それなりのページがサクサクできる。

cons

  • 割とありがちなデザインに落ち着きがち

1ページだけの簡単なページを作ってみると、この様な知見を得ることができました。
ただ、機能を追加するに従って、statepropertyに管理に辛みが。。。

次回は、この辛みを緩和してくれる、reduxについて書いていきたいと思います。

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