20200213のReactに関する記事は9件です。

ReactのuseEffectの簡単サンプルまとめ

はじめに

React のHooksのUseEffectのメモです。
ご指摘、感想、お待ちしています。。。

目次

  1. まずはクラスで普通に書いてみる
  2. 関数コンポーネントで書き換え
  3. コンポーネントが削除される時は?

1. まずはクラスで普通に書いてみる

まずはクラスでチュートリアルにあるようなカウンターを書いてみます。

コンポーネントがマウントされた際に
componentDidMount()でカウントをコンソールログに出力しています。
componentDidUpdate()で更新が行われる度に同じくログを出力しています。

これを関数コンポーネントで書き換えてみましょう。

ClassCounter
import React, { Component } from 'react'

class ClassCounter extends Component {
    constructor(props) {
        super(props)

        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        console.log(`Class Counter Mount !! ${this.state.count}`)
    }
    componentDidUpdate() {
        console.log(`Class Counter Update !! ${this.state.count}`)
    }

    render() {
        const { count } = this.state
        return (
            <div>
                <button onClick={() => this.setState({ count: count + 1 })}>
                    Click {count} times
                </button>
            </div>
        )
    }
}


export default ClassCounter

2. 関数コンポーネントで書き換え

ポイントはuseEffectの第2引数です。
空のリストの場合は、初回マウント時のみ
リストの中にstateを指定すると、そのstateが更新された時のみ
それぞれログが出力されるようになります。
また、リストには複数のstateを指定できます。
その場合は、どちらかが更新されるとログが出力されます。

FunctionCounter
import React, { useState, useEffect } from 'react'

export default function FunctionCounter() {

    const [count, setCount] = useState(0)
    const [text, setText] = useState('')
    const [isShow, setShow] = useState(true)

    // レンダリングされた時にのみ実行
    useEffect(() => {
        console.log(`Function Component Mount`)
    }, [])

    // countが更新された時に実行
    useEffect(() => {
        console.log(`count state is updated ${count}`)
    }, [count])

    // textが更新された時に実行
    useEffect(() => {
        console.log(`text state is updated ${text}`)
    }, [text])

    // count または textが更新された時に実行
    useEffect(() => {
        console.log(`state text and count are updated count=${count} text=${text}`)
    }, [text, count])

    return (
        <div>
            <input type='text' value={text} onChange={e => setText(e.target.value)}></input>
            <button onClick={() => setCount(count + 1)}>
                Click {count} times
            </button>
        </div>
    )
}

3. コンポーネントが削除される時は?

ボタン操作でClassChildComponentを表示を切り替えてみます。

ClassComponent
import React, { Component } from 'react'
import ClassChildComponent from './ClassChildComponent'

class ClassComponent extends Component {
    constructor(props) {
        super(props)

        this.state = {
            isShow: false
        }
    }

    render() {
        const { isShow } = this.state
        return (
            <div>
                <button onClick={() => this.state.isShow ?
                    this.setState({ isShow: false }) : this.setState({ isShow: true })}>
                    isShow is {isShow}
                </button>
                {isShow && <ClassChildComponent />}
            </div>
        )
    }
}


export default ClassComponent

ClassChildComponentの方でコンポーネントが削除される際の
componentWillUnmount()を定義しています。
これをuseEffectで書き換えます。

ClassChildComponent
import React, { Component } from 'react'

export default class ClassChildComponent extends Component {

    componentWillUnmount(){
        console.log('Child Class Component will Unmount !!')
    }

    render() {
        return (
            <div>
                <h3>Child Component</h3>
            </div>
        )
    }
}

まずは、同じように表示を切り替えるボタンのみのコンポーネントを準備します。

FunctionComponent
import React, { useState} from 'react'
import FunctionChildComponent from './FunctionChildComponent'

export default function FunctionComponent() {

    const [isShow, setShow] = useState(false)

    return (
        <div>
            <button onClick={() => isShow? setShow(false) : setShow(true)}>
                isShow is {isShow}
            </button>
            {isShow && <FunctionChildComponent />}
        </div>
    )
}

呼び出すコンポーネントはこんな感じ
useEffectの中でreturn()を定義してあげると、componentWillUnmountと同じ動きになります。

FunctionChildComponent
import React, {useEffect} from 'react'

export default function FunctionChildComponent() {

    useEffect(() => {
        return () => {
            console.log('Child Function Component will Unmount !!')
        };
    }, [])

    return (
        <div>
            <h3>Child Component</h3>
        </div>
    )
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでどうテストを書くべきか(プロダクト初期のユニットテスト)

お疲れ様です tenIOIOten です

みなさんフロントでテストを書いてますか?

まさか手動でテストをしてたりしないですよね?

確かにフロントは「json 色付け係」と揶揄されるように json をいい感じにスタイリングすればいいので、他への影響が少なく、テストをしなくても壊れにくいです

しかし、本当にフロントは壊れにくいですか?

今回は React でのテストの書き方についてまとめていきたいと思います

テストを書く時間のない時期でも書くべき、ユニットテストとコンポーネントのテストについてまとめていきます

ユニットテスト以上のテストはプロダクト初期では壊れやすいので、書くかどうかは任せます

書く場所

大まかに以下の所になります

  • 条件分岐を持つ reducer
  • 条件分岐を持つ saga(適宜利用している middleware に置き換えてください)
  • action creator を持つコンポーネント
  • regsuit

reducer

redux-toolkitを使ってます、boilerplate 削減の革命児なので触ってない人はぜひ触ってから記事に戻ってきてください

reducer では条件分岐を持つ場合のみテストを書くことにします

reducer は純粋関数なので条件分岐を持たないなら、テストは書く優先度は低いです

import { PayloadAction } from "@reduxjs/toolkit";
import { createAction, createSlice } from "@reduxjs/toolkit";
import { addDays, addMonths, addWeeks, startOfWeek } from "date-fns";
import { ModeSelectEnum } from "./types";

const name = "sample";

type State = {
  mode: ModeSelectEnum;
  startTimestamp: number;
};

const initialState: State = {
  mode: ModeSelectEnum.weekly,
  startTimestamp: startOfWeek(new Date(), { weekStartsOn: 1 }).getTime()
};

const slice = createSlice({
  name,
  initialState,
  reducers: {
    selectMode: (state, action: PayloadAction<ModeSelectEnum>) => {
      state.mode = action.payload;
    },
    nextDate: (state: State) => {
      switch (state.mode) {
        case ModeSelectEnum.daily:
          state.startTimestamp = addDays(new Date(state.startTimestamp), 1)
            .getTime()
            .getTime();
          break;
        case ModeSelectEnum.weekly:
          state.startTimestamp = addWeeks(
            new Date(state.startTimestamp),
            1
          ).getTime();
          break;
        case ModeSelectEnum.monthly:
          state.startTimestamp = addMonths(
            new Date(state.startTimestamp),
            1
          ).getTime();
          break;
        default:
          break;
      }
    }
  }
});

export const actions = { ...slice.actions, index, failedIndex };

export default slice.reducer;

上記のような、modeが選択されそれによってnextDateの処理が変わるreducerがあるとします

次のように書く条件を通るように state を作りなおしつつ、テストをします

import reducer, { actions } from "./slice";
import { ModeSelectEnum } from "./types";

describe("authReducer", () => {
  // selectModeは条件分岐がないので省略

  describe("nextDate", () => {
    it("should go next day", () => {
      let mode = ModeSelectEnum.daily;
      let startTimestamp = new Date("2020/01/15").getTime();
      let state = { mode, startTimestamp };

      expect(reducer(state, actions.successSignIn(payload))).toEqual({
        ...state,
        startTimestamp: new Date("2020/01/16").getTime()
      });
    });

    it("should go next week", () => {
      let mode = ModeSelectEnum.weekly;
      let startTimestamp = 0;
      let state = { mode, startTimestamp };

      expect(reducer(state, actions.successSignIn(payload))).toEqual({
        ...state,
        startTimestamp: new Date("2020/01/22").getTime()
      });
    });
    it("should go next week", () => {
      let mode = ModeSelectEnum.monthly;
      let startTimestamp = 0;
      let state = { mode, startTimestamp };

      expect(reducer(state, actions.successSignIn(payload))).toEqual({
        ...state,
        startTimestamp: new Date("2020/02/15").getTime()
      });
    });
  });
});

saga

saga も条件分岐を持つもののみテストします

export const getSample = function*(dep: Dependencies) {
  const sampleApi = getSampleApi(dep);
  const state = yield select();
  const anyRes = yield call(sampleApi.index, {
    start_timestamp: selectors.startTimestamp(state)
  });
  const res: ThenArg<ReturnType<typeof sampleApi.index>> = anyRes; // ← yieldは型をanyにしかできないので型を付け直す
  switch (res.status) {
    case Status.success:
      yield put(actions.successIndex(res.data.data));
      break;
    default:
      yield put(actions.failedIndex());
      break;
  }
};

簡単な API を呼ぶだけの saga です

redux-saga-test-plan を使って、条件分岐が入るところで save をしています

もし条件が増える場合はセーブポイントを増やしてください

import { axiosResponse } from "./../../test/axiosResponse";
import { SampleApi } from "./../../lib/api/SampleApi";
import { dependencies } from "../../components/contexts/DIContext/dependencies";
import { put, call } from "redux-saga/effects";
import { getSample } from "./sagas";
import { actions } from "./slice";
import { cloneableGenerator } from "@redux-saga/testing-utils";
import { testSaga } from "redux-saga-test-plan";

describe("sampleSagas", () => {
  describe("getSample", () => {
    const sampleApi = new SampleApi(); // ← インスタンスを作っておく

    const dep = mergeDependencies({ api: { sample: sampleApi } }); // ←依存を作る

    const gen = testSaga(signIn, dependencies, actions.signIn(payload));
    const savePoint = "call api";

    it("should call api", () => {
      gen
        .next()
        .next(mockState)
        .call(sampleApi.index, {
          week: mockStartTimestamp,
          workId: mockWorkId
        })
        .save(savePoint);
    });
    it("should success", () => {
      const apiData = [{ x: 1, y: 1 }];
      gen
        .restore(savePoint)
        .save(savePoint)
        .next({ status: 200, data: { data: apiData } })
        .put(actions.successIndex(apiData)) // ←成功アクションがdispatchされるかの確認
        .next()
        .isDone();
    });

    it("should failed with other error", () => {
      gen
        .restore(savePoint)
        .save(savePoint)
        .next({ status: 500, data: null })
        .put(actions.failedIndex()) // ← 失敗アクションがdispatchされるかの確認
        .run();
    });
  });
});

コンポーネント

コンポーネントでは action 発行のテストしかしなくも問題ないです、描画の変更は regsuit を使って確認します

hook や個々のコールバックなどのテストをしたい場合は、@testing-libary/react や@testing-libary/react-hooks を使ったサンプルをググってみてください

無駄なテストが実装できないので、いいコードしかないです

ちなみに RTL には Angular や Vue への派生ライブラリもあります

const Sample: React.FC<SampleProps> = () => {
  const startTimestamp = useSelector(sampleSelectors.startTimestamp);
  const { nextDate } = useBoundAction();
  useEffect(() => {
    nextDate();
  }, [getName]);
  return (
    <div>
      <button
        onClick={() => {
          nextDate();
        }}
      >
        Next Date
      </button>
      <div>
        {startTimestamp}
      </div>
    </div>
  );
};

const useBoundAction = () => {
  const dispatch = useDispatch();
  return useMemo(() => {
    return bindActionCreators(
      {
        nextDate: actions.nextDate
      },
      dispatch
    );
  }, [dispatch]);
};

このコンポーネントでは以下のように action が発行されたかをテストします

import React from 'react';
import { render } from '@testing-library/react';
import { Provider as ReduxProvider } from 'react-redux';
import Sample from './Sample';
import {createMockStore} from '../../../tests/createMockStore';
import { actions } from '../../../modules/sample/slice';
import { ModeSelectEnum } from '../../../modules/sample/types';
import { rootReducer } from '../../../modules/reducers';

const mockStore = createMockStore();

let mockState = {mock: { mode: ModeSelectEnum.daily, startTimestamp: new Date('2020/01/01').getTime()  } }; // ← selectorがundefを参照して落ちるので、storeのstateのモックはしっかりやる必要がある

let store = mockStore(mockState);

beforeEach(()=>{
  store = mockStore(let store = mockStore(mockState));
})

describe('sample page', () => {
  it('should next date on mount', () => {
    const wrapper = render(
      <ReduxProvider store={store}>
        <Sample />
      </ReduxProvider>);

    const resultActions = store.getActions();
    expect(resultActions[0]).toEqual(actions.getName());
  });
  it('should next date on click', () => {
    const { getByText } = render(
      <ReduxProvider store={store}>
        <Sample />
      </ReduxProvider>);

    const button = getByText('Next Date')
    button.click()

    const resultActions = store.getActions();
    expect(resultActions[1]).toEqual(actions.nextDate());
  });
});

regsuit

  1. ライブラリを入れる
   yarn add -D reg-suit zisui
  1. reg-suitの初期化
   yarn reg-suit init
  1. scripts の追加

package.json

   {
     "scripts": {
       "pretest-visual": "zisui --serverCmd \"npm run storybook\" http://localhost:9009 -o actual_images -V '1440x900'",
       "test-visual": "reg-suit run"
     }
   }

終わりです

まとめ

プロダクト初期はとりあえずこれでいいと思います

保守フェイズに入ったら、e2e やインテグレーションテスト、条件分岐なしの reducer や saga のユニットテスト、connect してないコンポーネントのテストを書いてもいいと思います

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

Vue.jsの環境構築を完了したら、翌日React.jsでオフショアとの共同開発が決まった時の話

概要

前回の続きです。

前回までのあらすじ

  1. プロジェクトで使用する試作品が Vue.js で開発することが決まった。
  2. Vue.js で協力会社の方が開発を行い試作品が完成した。
  3. 前任者から受け継いだ試作品の Vue.js のプロジェクトを実行したら 20 個のエラーが残っていた。
  4. 全部自分一人で修正した。
  5. ついでに Eslint すら入ってない環境だったので、既存の環境に Eslint と Pritter などを導入する。( 全く使用していない Jquey のライブラリも js というフォルダ内に何故か存在していたので抹消した )
  6. ついでに Nuxt と TypeScript の環境に構築し直して、ルーティングを行った。
  7. 本格的に開発が始まったので、オフショアと共同開発が決定した。
  8. オフショアは Vue.js の使用経験がなく、React の使用経験しかなかったので、オフショアに合わせて React で一から作り直すことが決まった。
  9. 筆者の React 経験は独学で 1 か月程。サンプルにページを追加して Redux を弄った程度、だが、筆者がオフショアに指示を出すことが決まった。

本編

今回はオフショアとの共同開発を行う上で、どのような問題が発生したか、どのようなメリットがあったか、その時どのような方法で対処したか、またどのように対処すればより良い結果が残せたか、
という反省を込めた記事になっております。あまり技術的な要素はない記事なのでご了承下さい。

そもそもの問題

  1. そもそも筆者がオフショアの方と仕事をするのが初めてであり、更に人に指示などを行って共同開発した経験が少なかった。
    • それまでは設計者から指示を貰って仕事をしたり、年の近い若手同士で協力して業務を行うことが多かった。
    • そのため、初対面の外国の方との共同開発が大変だった。
      • オフショアの方は 2 人いたが、一人は日本語を話せたがもう一人は日本語を話せなかった。( 英語で質問がきて、自分は英語で対応できなかった )
  2. 筆者の React.js の開発経験が不足していた。

苦労した点と対処方法

  1. お互い日本語で話しても発音などの問題でコミュニケーションが上手くいかなかった。

    • Slack などを使って文章による対話を行うことで対応。( オフショアも識字は得意だったのでお互い文通の方がコミュニケーションが上手くいった )
    • 日本語の敬語や丁寧語が伝わらなかったので、単語を強調し、友達のような話し方で話した。( 相手も年が近かったからできた方法でした。でもこれのお陰でその人と仲良くなれました。)
    • 同じ技術の用語でも違う言い方をすることがあったので、伝わらない場合、技術用語の言い方を変えた。( リポジトリ → Repository  など、日本人がカタカナ用語で言っている言葉が通じないことがあった )
    • 同じく、なるべく技術の用語は英語で書くように注意した。( GitHub、VSCode、Repository、k8s など )
    • 無意識に.NET Framework で使う用語を使っていたので言い方を変えた。( Label → ReadOnlyText など )
  2. 文通に慣れてくるともう一人の方が、英語で質問をしてきた。

    • Slack で英語を送るように頼み、自分はその文章を Google 翻訳で日本語に翻訳して、質問を理解し、回答を行った。
  3. React について理解不足なので、どんなライブラリや環境構築を行えばいいかわからなかった。

    • 以前に React で開発する可能性があるから React について調査して欲しいと言われたので React で開発を行った場合、 Vue.js で使用していたライブラリと比較して何を使えばいいか調査してスプレッドシートにまとめておいた。
      • そのスプレッドシートを元に Vue.js で行っていたこの機能は React では何が対応する、などをオフショアの方とその場で話して合意を取り、環境構築で使用するライブラリを逐一スプレッドシートに追記するように合意を取った。( Vuex → Redux、TypeScript → Flow、vue-router → react-router-dom など )
  4. オフショアの開発方法と、試作品での開発方法で意見が対立した。

    • WebPack を使うか否か、静的型付けを行うために TypeScript にするのか Flow を使用するのか。
      • それぞれのツールの良し悪しを調査し、打合せを重ねてよりプロジェクトに適したツールを使用した。
  5. 画面のイメージが Excel のお絵かきでオフショアに納品された。更にその画面のイメージでは色合いや大きさなどが実際に作る予定の試作品とマッチしておらず、開発するのに必要な情報が不足していた。

    • 別の部署で Adobe XD を使用していると聞き覚えがあったので、その部署の方に使い方や利点などを教えて貰い、プロジェクト内に浸透させた。

よかった点

  1. オフショアのフロントエンドに関する技術力が高かったこと。

    • 自分がオフショアの方に React について質問した時に、懇切丁寧に応えてくれた。
    • 自分も Vue.js をやっていたのでルーティングなど共通の概念は理解できたので、実装方法に関する話しなどは順調に進んだ。
  2. オフショアが OSS に精通していたので GitHub などでのやり取りは社員よりも上手くいき ( 社内で GitHub 導入中であったため )、また GitHub Featerure Fllow など効率的な運用方法はオフショアから教えてもらい、プロジェクト内に浸透できた。

  3. 一部のライブラリは Vue.js でも React でも使用可能だったので、ライブラリの選定にかかる時間を短縮できた。( Jest、StoryBook、JsDoc、husky )

  4. オフショアも使用経験のなかった JestJsDochuskyStoryBook などに関しては自分がプロジェクト内で有用性をアピールし、導入することでチーム開発効率化がなされた。

  5. オフショアの影響もあり、弊社のレガシーな技術でのプロジェクト開発が一部モダンな物に置き換わり、社内に浸透した。( SVN → GitHub 、VSCode、Excel の画面イメージ → Adobe XD )

  6. 日頃から資料やメモなどは全て Markdown やスプレッドシートで作成しており、Gsuite の Google ドライブで管理していたため、オフショアとの情報のやり取りや管理方法で戸惑うことが少なかった。

反省点

  1. 社内に自分以外の React の有識者がいなかったこと。(オフショア以外のメンバーからもフロントエンド、クラウド、GitHub などの質問が全て自分に集中して対処しきれなくなった。)
  2. プロジェクト内でフロントエンド以外にもクラウドや GitHub の管理も担当していたので、フロントエンドに集中できなかったこと。
  3. React を独学した経験を活かして、最初から Vue.js ではなく React.js を推奨して開発を行うことをしなかったこと。( 開発初期では、内製を目指していたので、多人数で開発する場合、JavaScript の経験者が少ないのであれば学習コストの低い Vue.js が最適だと判断してしまった )

まとめ

日頃から学習した内容を資料として残しておいてよかった。効率的な方法で情報をまとめておき、臨機応変に対応すれば不意の事態にもある程度対応できることがわかった。

参考記事

StoryBook が何故必要か?(Vue.js 編)

husky で git commit 時に npm test を走らせる

この頃流行りの Jest を導入して軽快に JS をテストしよう

JSDoc 使い方メモ

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

ReactでApollo-clientを用いてGraphQLでデータ取得するClassコンポーネント

背景

ReactにてGraphQLでデータ取得したい、かつ、クラスベースコンポーネントをTypeScriptで実装したいが、Appolo-Client公式のサンプルコードがどうしても動かなかったのでそのメモ。あと、細かいところがどうしてもわからなかったのでどなたかコメントくれたら嬉しいなという期待を込めて。

この記事で書かないこと

GraphQL、React、Typescript、サーバサイドの実装について

サンプルコード

まずはサンプルコードからお見せします。サーバサイドは、termという検索キーワード的な文字列を受け取ってid, title, descriptionのリストを返すイメージです。

Works.tsx
import React from "react";
import { graphql, ChildProps } from "react-apollo";
import gql from "graphql-tag";

const query = gql`
  query GetWorks($term: String) {
    works(term: $term) {
      id
      title
      description
    }
  }
`;

interface Work {
  id: string;
  title: string;
  description: string;
}

interface Response {
  works: Work[];
}
type Variables = {
  term: string | undefined;
};

interface InputProps extends Variables {}

type Props = ChildProps<InputProps, Response, Variables>;

const ChildWorks = graphql<InputProps, ResWorks, Variables, Props>(query, {
  options: ({ term }) => ({
    variables: { term }
  }),
  props: ({ data, ownProps }) => ({ data, ...ownProps })
});

class _Works extends React.Component<Props, {}> {
  renderWorks(): JSX.Element[] {
    if (this.props.data) {
      const { loading, works, error } = this.props.data;
      if (loading) return [<div key={0}>Loading...</div>];
      if (error) return [<div key={0}>ERROR</div>];
      if (works) {
        return works.map(work => {
          return (
            <div key={work.id}>
              <h2>{work.title}</h2>
              <p>{work.description}</p>
            </div>
          );
        });
      } else {
        return [<div key={0}>nothing</div>];
      }
    } else {
      return [<div key={0}>loading...</div>];
    }
  }

  render(): JSX.Element {
    return <div>{this.renderWorks}</div>;
  }
}

export const Works = ChildWorks(_Works);

あとはコンポーネントとして利用できるので、どこかで

App.tsx
<Work term={何かstring} />

としてあげれば実行できます。

説明(可能な部分)

ResponseのTypeとVariableのType、PropsのTypeを指定しています。
InputPropsが空になっていますが、Variableに含めないがPropsとして呼び出し元から値を取りたい場合にはここに追記します。今回はProps=Variablesの前提にしているので空です。

interface Work {
  id: string;
  title: string;
  description: string;
}

interface Response {
  works: Work[];
}
type Variables = {
  term: string | undefined;
};

interface InputProps extends Variables {}

type Props = ChildProps<InputProps, Response, Variables>;

optionsの部分でPropsで受け取った値をVariablesにセットしています。
propsの部分はなぜこういう記述になるのかは不明、、、どこか海外のサンプルコードから引っ張ってきたはずですがましたが、参照元のURLもすでにわからず。

const ChildWorks = graphql<InputProps, ResWorks, Variables, Props>(query, {
  options: ({ term }) => ({
    variables: { term }
  }),
  props: ({ data, ownProps }) => ({ data, ...ownProps })
});

GraphQLで取得したデータはthis.props.dataからアクセスできます。

const { loading, works, error } = this.props.data;

定義したReactコンポーネントそのままでは利用できず、さっき定義したChildWorksでラップしてあげる必要があるらしい。クエリとクラスコンポーネントを結びつけてあげるイメージかなと。

export const Works = ChildWorks(_Works);

一応、上記で動作してます。

JSXの配列をreturnする際にkeyを設定しないとWarningが出るので無理やりkey={0}で回避しているのですが、これもどうにかできないものか。。。

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

fetchPolicyをcache-and-networkにした時、毎回Loadingがtrueになる

はじめに

apollo clientを絶賛勉強中です。間違いがあったらお教えください。

問題

cacheを有効にする際、fetchPolicyを設定する場合があると思います。fetchPolicyのそれぞれの違いは、Apollo Client公式docから確認できます。
cache-firstはcacheをまず見にいって、cacheになければデータを取りに行くというものです。そのため一度取ってきた情報が残り続けてしまい、画面が更新されないということが起こります。cache-and-networkはcacheをまず見に行くところは同じですが、常にNetworkから最新のデータを持ってきてcacheに保存します。
そのため、今回cache-and-networkを使おうと思うのですが、cache-and-networkではnetworkからデータを取りに行く際にLoadingがtrueになってしまい、実際にアプリで使ってみるとcacheが有効になっていない場合と差がないように表示されます。
そのため、今回はcacheがある場合はLoadingを流さず、cacheにあるデータを表示し、裏で取ってきたデータに途中で切り替わるというようなものを作りたいと思います。

変更前

簡単な例で今回変更する部分のコードを示します。(元のコードは前回のQiitaで載せたもの)変更前後の差分を見てもらえればと思います。

const COMMENTS_QUERY = gql`
  {
    commentDetail(title:"初めて利用してみた"){
      id
      comment
    }
  }
`;

export default function Comments(){
  const { loading, error, data } = useQuery(COMMENTS_QUERY);

  if (loading) return <p>Loading...</p>;
  if (error) {
    return <p>Error</p>;
  }

  return (
    <div >
      <div>comment:{data.commentDetail.comment}</div>
    </div>
  );
};

変更後

networkからデータを取ってきているかつcacheがない場合のみLoadingが表示されるように変更しました。

const COMMENTS_QUERY = gql`
  {
    commentDetail(title:"初めて利用してみた"){
      id
      comment
    }
  }
`;

export default function Comments(){
  const { loading, error, data } = useQuery(COMMENTS_QUERY);

  if (data && loading) return <p>Loading...</p>
  if (error) {
    return <p>Error</p>;
  }

  return (
    <div >
      <div>comment:{data.commentDetail.comment}</div>
    </div>
  );
};

終わりに

実装することはできたけども、最新のデータがcacheのものと違う場合いきなり切り替わるので、それが嫌な人は結局Loadingを表示させる方がいいのかなと思いました。

参考文献

以下の記事を参考にさせてもらいました。ありがとうございました。
Set network status to 3 with cache-and-network fetch policy #1217

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

Material-UI Pickers×Moment.jsをReactで使う際のtips

業務でMaterial-UI PickersをMoment.jsで使いたいと思った際に
マテリアルUIのサイトはあてにならなかったので参考になればと思います。

環境/パッケージのversion

react 16.12.0
typescript 3.7.3
moment 2.24.0
@material-ui/pickers 3.2.10
@date-io/moment 1.3.13

material-ui-pickers v3ではdate-io系のversionは1.x.xを使わないとうまくいかないみたいなので1.3.13いれてます。
最初これよく読まなかったので、エラーがでまくりで死にそうでした
Material-UI Pickersサイト

実装

実装の詳細について書いていきます。

全体

これで一つのコンポネントです

import React, { FC } from 'react'
import MomentUtils from '@date-io/moment';
import {
  MuiPickersUtilsProvider,
  KeyboardTimePicker,
  KeyboardDatePicker,
} from '@material-ui/pickers'
import moment, { Moment } from 'moment'

const DatePicker: FC = () => {
  const [selectedDate, setSelectedDate] = React.useState<Moment | null>(
    moment()
  );

  const handleDateChange = (date: Moment | null) => {
    setSelectedDate(date)
  };

  return (
    <MuiPickersUtilsProvider utils={MomentUtils}>
      <KeyboardDatePicker
        disableToolbar
        variant="inline"
        format="YYYY/MM/DD"
        value={selectedDate}
        onChange={handleDateChange}
      />
      <KeyboardTimePicker
        value={selectedDate}
        onChange={handleDateChange}
      />
    </MuiPickersUtilsProvider>
  )
}

export default DatePicker

import

import React, { FC } from 'react'
import MomentUtils from '@date-io/moment'
import {
  MuiPickersUtilsProvider,
  KeyboardTimePicker,
  KeyboardDatePicker,
} from '@material-ui/pickers'
import moment, { Moment } from 'moment'
  • React, { FC } FunctionCompornentをいれてます。
  • MomentUtils アダプターです。こちらいれないとMomentのオブジェクトを日付と認識してくれません。
  • MuiPickersUtilsProviderこいつでかこってあげないとMomentUtilsを反映できません。
  • KeyboardTimePicker KeyboardDatePicker keybordでも日付を操作したいので。
  • moment これがないと日時を簡単に扱えません。
  • Moment momentの型interfaceです。typescriptでは定義してあげないと怒られます。

state

stateで行き交うデータは全てMoment型なのを注意してください。

const [selectedDate, setSelectedDate] = React.useState<Moment | null>(
  moment()
);

初期stateにmoment()で現在時刻をsetします。
useState<Moment | null>で型をMoment型にしてあげないとエラーがでます。

const handleDateChange = (date: Moment | null) => {
  setSelectedDate(date)
};

メゾットに関してもpickerから引数で渡されるdateの型はMomentなので定義してあげましょう。

render

return (
    <MuiPickersUtilsProvider utils={MomentUtils}>
      <KeyboardDatePicker
        disableToolbar
        variant="inline"
        format="YYYY/MM/DD"
        value={selectedDate}
        onChange={handleDateChange}
      />
      <KeyboardTimePicker
        value={selectedDate}
        onChange={handleDateChange}
      />
    </MuiPickersUtilsProvider>
  )

<MuiPickersUtilsProvider utils={MomentUtils}> これで囲わないと正しくMomentに変換できずにエラーがでます。utils内にアダプタ名を記述します。

ここで地味につまずいたのはformatで、全部大文字にしないと正しく表示されないので大文字で。
多分Momentが大文字しか認識しないのかと。

まとめ

@date-ioのversionミスしたりstateの型をDateでやってしまったりとなかなかつまずいたので、参考になれば幸いです。
Muiは日本語のtipsが少ないからハマったら長い。。。
改善、修正点ございましたら気軽にコメントくださいませ。

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

Material-UI Pickers×Moment.jsをReact(TypeScript)で使う際のtips

業務でMaterial-UI PickersをMoment.jsで使いたいと思った際に
マテリアルUIのサイトはあてにならなかったので参考になればと思います。

環境/パッケージのversion

react 16.12.0
typescript 3.7.3
moment 2.24.0
@material-ui/pickers 3.2.10
@date-io/moment 1.3.13

material-ui-pickers v3ではdate-io系のversionは1.x.xを使わないとうまくいかないみたいなので1.3.13いれてます。
最初これよく読まなかったので、エラーがでまくりで死にそうでした
Material-UI Pickersサイト

実装

実装の詳細について書いていきます。

全体

これで一つのコンポネントです

import React, { FC } from 'react'
import MomentUtils from '@date-io/moment';
import {
  MuiPickersUtilsProvider,
  KeyboardTimePicker,
  KeyboardDatePicker,
} from '@material-ui/pickers'
import moment, { Moment } from 'moment'

const DatePicker: FC = () => {
  const [selectedDate, setSelectedDate] = React.useState<Moment | null>(
    moment()
  );

  const handleDateChange = (date: Moment | null) => {
    setSelectedDate(date)
  };

  return (
    <MuiPickersUtilsProvider utils={MomentUtils}>
      <KeyboardDatePicker
        disableToolbar
        variant="inline"
        format="YYYY/MM/DD"
        value={selectedDate}
        onChange={handleDateChange}
      />
      <KeyboardTimePicker
        value={selectedDate}
        onChange={handleDateChange}
      />
    </MuiPickersUtilsProvider>
  )
}

export default DatePicker

import

import React, { FC } from 'react'
import MomentUtils from '@date-io/moment'
import {
  MuiPickersUtilsProvider,
  KeyboardTimePicker,
  KeyboardDatePicker,
} from '@material-ui/pickers'
import moment, { Moment } from 'moment'
  • React, { FC } FunctionCompornentをいれてます。
  • MomentUtils アダプターです。こちらいれないとMomentのオブジェクトを日付と認識してくれません。
  • MuiPickersUtilsProviderこいつでかこってあげないとMomentUtilsを反映できません。
  • KeyboardTimePicker KeyboardDatePicker keybordでも日付を操作したいので。
  • moment これがないと日時を簡単に扱えません。
  • Moment momentの型interfaceです。typescriptでは定義してあげないと怒られます。

state

stateで行き交うデータは全てMoment型なのを注意してください。

const [selectedDate, setSelectedDate] = React.useState<Moment | null>(
  moment()
);

初期stateにmoment()で現在時刻をsetします。
useState<Moment | null>で型をMoment型にしてあげないとエラーがでます。

const handleDateChange = (date: Moment | null) => {
  setSelectedDate(date)
};

メゾットに関してもpickerから引数で渡されるdateの型はMomentなので定義してあげましょう。

render

return (
    <MuiPickersUtilsProvider utils={MomentUtils}>
      <KeyboardDatePicker
        disableToolbar
        variant="inline"
        format="YYYY/MM/DD"
        value={selectedDate}
        onChange={handleDateChange}
      />
      <KeyboardTimePicker
        value={selectedDate}
        onChange={handleDateChange}
      />
    </MuiPickersUtilsProvider>
  )

<MuiPickersUtilsProvider utils={MomentUtils}> これで囲わないと正しくMomentに変換できずにエラーがでます。utils内にアダプタ名を記述します。

ここで地味につまずいたのはformatで、全部大文字にしないと正しく表示されないので大文字で。
多分Momentが大文字しか認識しないのかと。

まとめ

@date-ioのversionミスしたりstateの型をDateでやってしまったりとなかなかつまずいたので、参考になれば幸いです。
Muiは日本語のtipsが少ないからハマったら長い。。。
改善、修正点ございましたら気軽にコメントくださいませ。

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

React最低限の基礎メモ

Reactの勉強をしているのですが、仕組みの基礎理解が曖昧なので一度自分なりに最低限必要なところを整理したいと思います。

御三家

とにかく覚えておかなければいけない三つ。

  • component
  • state
  • props

これがわからないとコード書いてても何が何だかって感じでした。逆に言えばここさえわかれば大まかな流れは理解しやすいという印象です。

component

ものすごくざっくり言えばページの各パーツです。

「メニュー」「テキスト入力フォーム」「ナビゲーション」みたいにそれぞれ役割が決まっているところをcomponentとして分けて設定していくといった感じでしょうか。

大抵は「components」というファイルを作成してそこに「menu.js」「navigation.js」「main.js」といった具合にページの各パーツのjsファイルを突っ込んでいきます。

これを組み合わせてindex.jsで表示させると言う流れです。

state

「初期値」と認識するのがわかりやすいと思います。

例えば「+ボタンを押すと表示されている数字が増える」といった機能を作りたいときは、カウントの最初の数値、つまり0をstateで設定します。

class Countor extends Component{
    constroctor(props){
        super(props);
        this.state={count:0};
    }

constroctorやsuperはstate作るときの定型文なんでとりあえずこのまま頭に入れてればおk。

このcountを0から増やしたい時には、stateの状態を変化させるsetStateを使います。

   handlePlusButton{
       this.setState({count:this.state.count+1})
   }

これで準備できたので、表示させるためにrenderとreturnでこいつらを設置してやります。

   render(){

      return(

         <div>

             <div>count:{this.state.count}</div>  //初期値の0

             <button onClick={this.handlePlusButton}>   //ボタンを押すごとに1追加されていく

         </div>

      )

   }

}   //Countorの閉じ括弧

ざっくりですが、stateとsetStateの役割が理解できればなんとなく中身の完成イメージを想像しやすくなるので大事だと思いました。

props

propsは「それぞれのデータを渡すもの」という認識です。

「props名=値」として使います。例えばメニューごとに表示内容を変えたい場合など、その表示データをpropsの値として設定すれば、違ったデータを表示することができます。

class Menu extends Component {
   render() {
       return (
          <div>
             <div>{this.props.name}</div>
          </div>
       );
   }
}



class App extends Component {
   render() {
      const menus=[
         {name:"pasta"},
         {name:"pizza"},
         {name:"juice"},
         {name:"coffee"}
      ]
      {menus.map((menu) => { 
         return (
            <Menu
               name={menu.name}
            />
          )

       })}
   }
}

この場合だとnameがprops名でpizzaやpastaといったものが値になります。

Menuコンポーネントにこれらのデータを渡してAppで表示しています。

正直propsの理解はまだだいぶ怪しいのですが、色々コードを打ってなれていきたいと思います。

まとめ

とりあえずこの三つを押さえておけば、何をしているのか全体の流れはある程度理解できるのかなと思います。

まだまだReactには慣れないのですが、もう少し色々んあコードを打って理解を深めたいと思います。

お付き合いいただき有り難うございました。

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

React基本

JSX

FaceBookが開発したjavascript拡張機能。HTMLタグをjavascriptの中に書ける。実際はBabelを使ってHTMLの部分をjavascriptのプログラムに置き換えている

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>

  <body>
    <div id="app"></div>
    <script src="app.js"></script>
  </body>
</html>
// jsxを使用できるようにreactのモジュールをimport
import React from "react";
import ReactDOM from "react-dom";

// 描画関数のReactDom.renderにh1タグを渡して'app'に描画する
const app = document.getElementById('app');
ReactDOM.render(<h1> Hello </h1>, app);

注意点

  1. コンポーネント名は大文字から始める
  2. class属性は「className」と書く
  3. returnで戻すタグは一つ。複数返したい場合はdivとかでくくる
  4. 閉じタグがない場合はエラー
  5. 属性値は""でくくる。テンプレート文字列(``)は指定できない
  6. {}でくくるとjavascriptが使用できる
import React from "react";
import ReactDOM from "react-dom";

// 関数コンポーネント。大文字から始める
const App = () =>{
  // class属性はclassName
  // 属性値は””でくくる
  // 戻すタグはdivなどで一つにまとめる
  return (
    <div>
      <h1 className="greeting"> Hello </h1>
    </div>
  )
}

const app = document.getElementById('app');
ReactDOM.render(<App>, app);

コンポーネント

独立した再利用可能な部品として分けられたもの

関数コンポーネント

// JSXの項で描いたやつ
const App = () =>{
  return (
    <div>
      <h1 className="greeting"> Hello </h1>
    </div>
  )
}

クラスコンポーネント

// importしたReactモジュールのComponentを継承する
// renderメソッドに描画したい内容を定義する
class App extends React.Component {
  render(){
    return(
      <div>
        <h1 className="greeting"> Hello </h1>
      </div>
    )
  }
}

renderメソッドについて

renderは以下のタイミングでReactが自動的に呼び出す

  • javascriptがブラウザにロードされた直後
  • コンポーネントのpropsが変更された時
  • コンポーネント内でsetState()メソッドを実行してstateが変更された時

※setState()を複数回呼んだからといって毎回render呼び出しされるわけではないみたい

setStateを複数回実行してもrenderは1回しか呼ばれない
【翻訳記事】関数型setStateはReactの未来だ

State

コンポーネントが保持する状態、値のこと

注意点

  • stateはthis.stateというインスタンス変数に格納される
  • stateの更新は必ずthis.setState()メソッドを使用する
  • this.setState()はrender()メソッドで呼び出してはいけない。this.setState()によってrender()が呼び出され再帰呼び出しエラーとなってしまうため
class Human extends React.Component {
  constructor() {
    super();
    // stateの初期処理
    this.state = { name: '' }
  }

  setName(){
    const name = this.getName()

    this.setState({ 
      name: name
    });
  }
   ...

  render(){
    return(
      <div>
        <h1>{this.state.name}</h1>
      </div>
    )
  }
}

Props

コンポーネントに渡されるパラメータのこと

class Human extends React.Component {
   ...

  render(){
    return(
      // Greetingコンポーネントのnameにパラメータを渡す
      <Greeting name={this.state.name}>
    )
  }
}

const Greeting = (props) => {
  const greeting = this.currentTimeGreeting()

  return (
    <div>
      <h1>私は{props.name}です{greeting}!</h1>
    </div>
  )
}

参考

公式ドキュメント
setStateを複数回実行してもrenderは1回しか呼ばれない
【翻訳記事】関数型setStateはReactの未来だ

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