なになれ

IT系のことを記録していきます

Tour of HeroesをReact+Reduxで再実装した

最近React+Reduxを勉強したアウトプットとして簡単なアプリを作りました。
hi1280.hatenablog.com

作ったもの

Tour of HeroesはAngular公式のチュートリアルです。
複数画面があって、WebAPIを呼び出すサンプルになっています。
今回はこのTour of Heroesを題材にして、React+Reduxの理解を深めるためにTour of Heroesを再実装しました。

実装したプログラムはこちらです。

github.com

Angularと比較しつつ、React+Reduxによる実装内容を説明します。

Viewの作成

リスト形式でデータを表示する

javascriptの複数のオブジェクトを繰り返すメソッドを使って、HTMLを生成します。
dashboard.jsでは、lodashのmapメソッドを使用して繰り返し処理をしています。

dashboard.js

// 一部抜粋
return _.map(this.props.heroes, hero => {
  return (
    <StyledLink to={`/detail/${hero.id}`} key={hero.id}>
      <Hero>
        <h4>{hero.name}</h4>
      </Hero>
    </StyledLink>
  );
}).slice(0, 4);

Angularの場合はngForというテンプレート構文があります。
これによりHTML内で繰り返し処理が可能です。

dashboard.component.html

<!-- 一部抜粋 -->
<a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</a>

フォームを作成する

redux-formというフォームをReduxに則って扱うためのモジュールを使っています。
Reduxでプログラム自体は複雑になりますが、データが単方向に流れる設計になることで、フォームの値がシンプルに制御されています。

hero-detail.js

// 一部抜粋
class HeroDetail extends Component {
  componentDidMount(){
    const { id } = this.props.match.params;
    this.props.fetchHero(id);
    const { hero } = this.props;
    this.props.initialize({
      name: hero.name
    });
  }

  onBack() {
    this.props.history.goBack();
  }

  onSave(id, values) {
    this.props.updateHero(id, values);
    this.props.history.goBack();
  }

  render() {
    const { hero, handleSubmit } = this.props;

    if (!hero) {
      return <div>Loading...</div>;
    }

    return (
      <form onSubmit={handleSubmit(this.onSave.bind(this, hero.id))}>
        <h2>{hero.name} Details</h2>
        <div><span>id: </span>{hero.id}</div>
        <div>
          <Label>name:
            <Input name="name" component="input" type="text"/>
          </Label>
        </div>
        <Button onClick={this.onBack.bind(this)} type="button">go back</Button>
        <Button type="submit">save</Button>
      </form>
    );
  }
}

function mapStateToProps({ heroes }, ownProps) {
  return {
    hero: heroes[ownProps.match.params.id]
  };
}

export default reduxForm({
  form : 'HeroDetailForm'
})(
  connect(mapStateToProps, { fetchHero, updateHero })(HeroDetail)
)

Angularではフォームを作るのにリアクティブフォームとテンプレート駆動フォームの2つの方法があります。
Angularのチュートリアルではテンプレート駆動フォームが使われていて、双方向データバインディングでフィールドの値とフォームの値とを同期しています。

hero-detail.component.html

<!-- 一部抜粋 -->
<input [(ngModel)]="hero.name" placeholder="name"/>

hero-detail.component.ts

// 一部抜粋
export class HeroDetailComponent implements OnInit {
  @Input() hero: Hero;
}

WebAPI呼び出し

ReduxのActionと呼ばれるオブジェクトでWebAPIを呼び出します。
HTTP通信の公式なやり方はないので、npmのモジュールやWeb標準APIを使います。
今回はaxiosを使っています。

actions/index.js

// 一部抜粋
export function fetchHeroes() {
  const request = axios.get(`${ROOT_URL}?key=${key}`);
  return {
    type: FETCH_HEROES,
    payload: request
  }
}

ActionからReducerに処理が流れる時に、redux-promiseを使って、PromiseのオブジェクトをJavaScriptのオブジェクトに変換しています。
ReducerではPromiseの値であることを意識せずに処理します。

reducer-heroes.js

// 一部抜粋
export default function(state = {}, action) {
  switch(action.type) {
    case DELETE_HERO:
      return _.omit(state, action.payload);
    case FETCH_HEROES:
      return _.mapKeys(action.payload.data, 'id');
    case CREATE_HERO:
    case FETCH_HERO:
    case UPDATE_HERO:
      return {...state, [action.payload.data.id]: action.payload.data};
    default:
      return state;
  }
}

Angularでは、ReduxのようにAPIの呼び出しからStoreへの値の格納までの決められた設計はありません。
サービスクラスを作り、APIを呼び出すことが推奨されているくらいです。
公式のチュートリアルではサービスクラスを用意して、HttpClientでAPIを呼ぶ実装になっています。

hero.service.ts

// 一部抜粋
getHeroes (): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      tap(heroes => this.log('fetched heroes')),
      catchError(this.handleError('getHeroes', []))
    );
}

API呼び出しによる値はReduxのStoreのように集中管理するわけではなく、各コンポーネントのフィールドに格納しています。
複雑にAPI呼び出しと画面操作が行われると値の管理が難しくなる危険がありそうです。

dashboard.component.ts

// 一部抜粋
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }
}

CSS指定

コンポーネント内に限定してCSSを指定するためのやり方はありません。
今回はstyled-componentsを使い、CSS in JSでCSSの指定を行いました。

index.js

// 一部抜粋
const Main = styled.div`
  h3 {
    text-align: center; margin-bottom: 0;
  }
  h1 {
    font-size: 1.2em;
    color: #999;
    margin-bottom: 0;
  }
`

ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}>
    <BrowserRouter>
      <Main>
        <h1>Tour of Heroes</h1>
        <nav>
          <Link to="/">Dashboard</Link>
          <Link to="/heroes">Heroes</Link>
        </nav>
        <Switch>
          <Route path="/heroes" component={Heroes}/>
          <Route path="/detail/:id" component={HeroDetail}/>
          <Route path="/" component={Dashboard}/>
        </Switch>
      </Main>
    </BrowserRouter>    
  </Provider>
  , document.querySelector('.container'));

Angularでは、コンポーネントクラスに指定したCSSコンポーネントクラスのスコープのみに適用されます。
他のコンポーネントに影響しません。
これは設定によって、グローバルなスコープにすることも可能です。

まとめ

React+ReduxによるコードとAngularのコードはかなり違います。
今までAngularをメインに利用してきたので、React+Reduxのコードにまだ違和感があります。
ただReduxを使うことで実装に迷いがなくなるというメリットがあり、かつ、Reduxを便利にするためのモジュールも充実しているために慣れてくると使い勝手が良いのではという印象です。