TypeScript
reactjs
Swift
React

Reactでローディング中のデータを表現する方法の案

概要

レスポンスデータとloadingのステートをセットにして表現すると良さそうではないですか!という提案の記事です

// typescriptです

enum Status {
  loading = 'loading',
  success = 'success',
  failure = 'failure',
}

interface IResult<T> {
  status: Status
  response: T | Error | null
}

interface IHit {
  objectID: string
  url: string
  title: string
}

interface IState {
  hits: IResult<IHit[]>
}

class App extends Component<{}, IState> {

  componentDidMount() {
    this.setState({
      hits: {
        status: Status.loading,
        hits: null
      }
    });

    fetch(API + DEFAULT_QUERY)
      .then(response => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('Something went wrong ...');
        }
      })
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ hits: error, isLoading: false }));
  }

  render() {
    const { hits } = this.state;

    if (hits.status === Status.failure) {
      return <p>{hits.response.message}</p>;
    }

    if (hits.status === Status.loading) {
      return <p>Loading ...</p>;
    }

    return (
      <ul>
        {hits.response.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}

背景

普通は下記のような方法で表現することが多いかなと思います

参考: https://medium.com/@ghengeveld/async-data-loading-in-react-94661e23cd3d

class App extends Component {

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('Something went wrong ...');
        }
      })
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));
  }

  render() {
    const { hits, isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <ul>
        {hits.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}

これだけ見るとあんまり違いはないのですが、fetchするデータの種類が増えてくると isLoadingHits, isLoadingUsers みたいに管理すべきフラグが増えて複雑になってしまうなと感じていました

そこで私は下記のような書き方することもあったのですが、わかりにくいしあんまりよくないなと感じていました

interface State {
  // undefined: 未取得
  // null: 取得失敗したとき
  // IHit[]: 取得成功したとき
  hits: IHit[] | undefined | null
}

提案しているIResultはswiftを真似ています

https://github.com/ishkawa/APIKit#apikit

// SearchRepositoriesRequest conforms to Request protocol.
let request = SearchRepositoriesRequest(query: "swift")

// Session receives an instance of a type that conforms to Request.
Session.send(request) { result in
    switch result {
    case .success(let response):
        // Type of `response` is `[Repository]`,
        // which is inferred from `SearchRepositoriesRequest`.
        print(response)

    case .failure(let error):
        self.printError(error)
    }
}

swiftだと↑こんな感じでレスポンスの結果をenumで表現する文化があるのですが、これが結構わかりやすくて好きで、同じ事をやりたいなというモチベーションです

typescriptの型システム的に値付きenumっぽい表現ができなさそうだったので、ちょっと妥協して今回のような型になっています
(本当はこういう風に表現したかった)

redux

この例ではreactのstateにデータを保持していますが、reduxのstoreでも全く同じ要領でやれるかなと思っています

今後

個人のPJで試しにやってみているので、コードが公開できるようになったら、実際にどんな感じか感想を書こうと思います