LoginSignup
5
1

More than 3 years have passed since last update.

これからはFunction Componentですべて解決できる――というのはどうやら幻想だったようです。

Posted at

何がしたかったのか

Reactには、Lazy Componentというものがあります。

MyComponent.tsx
import React, { FC } from 'react';

const MyComponent: FC = () => (
  <div>Hello LazyComponent!</div>
);

export default MyComponent;
MyApp.tsx
import React, { FC, Suspense, lazy } from 'react';

const MyComponent = lazy(() => import('./MyComponent'));

const MyApp: FC = () => (
  <div>
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  </div>
);

export default MyApp;

とすると、MyComponentのロードが完了するまでfallbackに設定された<div>Loading...</div>を代わりにレンダリングしてくれるというものです。

で、いろいろ調べてたらこんなこともできると判明。

LazyComponent.js
import React, { Component } from 'react';

let result = null;
const timeout = (msec) => new Promise(resolve => {
  setTimeout(resolve, msec)
});

const LazyComponent = () => {
  if (result !== null) {
    return (
      <div>{result}</div>
    )
  }
  throw new Promise(async(resolve) => {
    await timeout(1000);
    result = 'Done'
    resolve();
  })
};

export default LazyComponent;

React 16.6の新機能、React.lazyとReact.Suspense を使った非同期処理

こう書いたら、throwしたPromiseがresolveされたときにもう1回レンダリングされるらしく。私の探し方が悪いのか何なのか、この仕様はReactのドキュメント上で見つけることができませんでした。どこに書いてあるのか知っている人がいたらこっそり教えてほしいです。

それはさておきこの仕様、ドキュメントで見つからなかったので動かない前提で試しに書いてみました。

試しに書いたコード
import React, { FC, lazy, Suspense } from 'react';

const PromiseTest= lazy(async () => {
  let state = 0;
  const TestInner: FC = () => {
    if(state) {
      return (
        <div>Done! {state}</div>
      )
    }
    throw new Promise((res) => {
      setTimeout(() => {
        state = 5;
        res();
      }, 5000);
    });
  };
  return {
    default: TestInner,
  };
});

const TestApp: FC = () => {
  return (
    <div>
      <Suspense fallback={<div>WAITING...</div>}>
        <PromiseTest />
      </Suspense>
    </div>
  );
}

やってみた結果……動く!動くぞ!

さて、問題のコードに移ろうじゃないか

さて、Promiseをthrowしたら期待通りに動くことが分かったんですけれど。state = 5ってPromiseの中で変数に代入しちゃってるじゃないですか。ぶっちゃけキモいですよね。
useStateフックに置き換えてもいけるんじゃね?って思った私、置き換えてみました。

置き換えてみた
import React, { FC, lazy, Suspense, useState } from 'react';

const PromiseTest= lazy(async () => {
  const TestInner: FC = () => {
    const [state, setter] = useState(0);
    if(state) {
      return (
        <div>Done! {state}</div>
      )
    }
    throw new Promise((res) => {
      setTimeout(() => {
        setter(5);
        res();
      }, 5000);
    });
  };
  return {
    default: TestInner,
  };
});

const TestApp: FC = () => {
  return (
    <div>
      <Suspense fallback={<div>WAITING...</div>}>
        <PromiseTest />
      </Suspense>
    </div>
  );
}

あれ、動かん:thinking::thinking::thinking:
動かんぞ。

useStateに置き換える前は動いたコードが、置き換えた瞬間動かなくなりました。てゆうか、setterは普通に呼ばれているはずなのに、stateの値は0のまま。なんでや、、、。

諦めてComponent classにしてみた

というわけで、PromiseTestの実装をComponent classに変えてみました。statethis.state.statesetterthis.setStateに変えただけですけどね。

classに書き換えてみた
class TestInner extends React.Component<{}, { state: number }> {
  constructor(props: {}) {
    super(props);

    this.state = {
      state: 0,
    };
  }

  render() {
    if(this.state.state) {
      return (
        <li>Done! {this.state.state}</li>
      );
    }
    throw new Promise((res) => {
      setTimeout(() => {
        console.log('resolved');
        this.setState({ state: 5 });
        res();
      }, 5000);
    });
  }
}

const PromiseTest = lazy(async () => {
  return {
    default: TestInner,
  };
});

const TestApp: FC = () => {
  return (
    <div className='board-list-container'>
      <Suspense fallback={<div>WAITING...</div>}>
        <PromiseTest />
      </Suspense>
    </div>
  );
}

こうすると、動きました。動いてしまいました。え、なんでなんや、、、:thinking:
……Function Componentが使えない極めてまれなケースの1つを発見した身としては、非常に頭が痛いです。こういう重要なことはもっとわかりやすくドキュメントに書いておいて下せぇ……。

結論

React、なんもわからん。

5
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1