11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React18 新機能】Transitionって何?【前編】

Last updated at Posted at 2023-08-18

はじめに

前回はAutomatic Batchingについて解説しましたが,今回はReact新機能のTransitionついて解説していきます!

今回もReact18において重要な機能になっているので、今回の記事を見て概要だけでも
理解しておいてください!

以下前回記事

Transitionとは...?

以下公式より引用

トランジション(transition; 段階的推移)とは React における新たな概念であり、緊急性の高い更新 (urgent update) と高くない更新 (non-urgent update) を区別するためのものです。

・緊急性の高い更新とはタイプ、クリック、プレスといったユーザ操作を直接反映するものです。
・トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。

タイプ、クリック、プレスのような緊急性の高い更新は、物理的な物体の挙動に関する我々の直観に反しないよう、即座に反応する必要があり、そうでないと「おかしい」と認識されてしまいます。一方でトランジション内では、ユーザは画面上であらゆる中間の値が見えることを期待していません。

簡単にいうとこの変更は優先度が低いことをReact側に伝える機能になります。
優先度が高いものをReact側に伝えれるわけではなく、あくまで低いものを伝えることによって
優先度の高い、低いを区別できるようにしてるのがポイントです。

実際にコードを動かしてみよう

transitionの機能を確認するために、
今回はブログの作成者をクリックしたらフィルタリングされるサンプルアプリを作成します。

React18の環境下で以下のファイルを作成して行ってください。

src/components/Transition.tsx
import { useState, useTransition } from 'react';
import { Avatar } from "./Avatar";
import { BlogList } from './BlogList';

export type Blog = {
  id: number;
  title: string;
  author: string;
}

const member = {
  a: '田中',
  b: '山田',
  c: '佐藤',
}

const generateDummyBlogs = (): Blog[] => {
  return Array(5000).fill('').map((_, index) => {
    const addedIndex = index + 1
    return {
      id: addedIndex,
      title: `タイトル${addedIndex}`,
      author: addedIndex % 3 === 0 ? member.a : addedIndex % 3 === 1 ? member.b : member.c,
    }
  })
}

const blogs = generateDummyBlogs();

const filteringAssignee = (author: string) => {
  if (author === '') return blogs;
  return blogs.filter((Blog) => Blog.author === author);
}

export const Transition = () => {
  const [selectedAssignee, setSelectedAssignee] = useState<string>('');
  const [blogList, setBlogList] = useState<Blog[]>(blogs);

  const onClickAssignee = (author: string) => {
    console.log('clickしたよ')
    setSelectedAssignee(author);
    setBlogList(filteringAssignee(author));

  }
  return (
    <div>
      <p>ブログ一覧</p>
      <div style={{ display: 'flex', justifyContent: 'center' }}>
        <Avatar isSelected={selectedAssignee === member.a} onClick={onClickAssignee}>{member.a}</Avatar>
        <Avatar isSelected={selectedAssignee === member.b} onClick={onClickAssignee}>{member.b}</Avatar>
        <Avatar isSelected={selectedAssignee === member.c} onClick={onClickAssignee}>{member.c}</Avatar>
      </div>
      <br />
      <button onClick={() => onClickAssignee('')}>リセット</button>
      <br />
      <br />
      <BlogList blogList={blogList} />
    </div >
  )
}

src/components/Avatar.tsx
import type { Blog } from "./Transition"

type Props = {
  blogList: Blog[]
}

export const BlogList = ({ blogList }: Props) => {
  return (
    <>
      {
        blogList.map((blog) => (
          <div key={blog.id} style={{ width: '500px', margin: 'auto ', background: 'pink ', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', borderRadius: '10px' }}>
            <p> タイトル:{blog.title}</p>
            <p>作成者:{blog.author}</p>
          </div >
        ))
      }
    </>
  )
}
src/components/BlogList.tsx
import type { Blog } from "./Transition"

type Props = {
  blogList: Blog[]
}

export const BlogList = ({ blogList }: Props) => {
  return (
    <>
      {
        blogList.map((blog) => (
          <div key={blog.id} style={{ width: '500px', margin: 'auto ', background: 'pink ', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', borderRadius: '10px' }}>
            <p> タイトル:{blog.title}</p>
            <p>作成者:{blog.author}</p>
          </div >
        ))
      }
    </>
  )
}

呼び出しも元のApp.tsxは下記のとおり記述してください。

App.tsx
import './App.css';
import { Transition } from './components/Transition';

function App() {


  return (
    <div className="App">
      <Transition />
    </div>
  );
}

export default App;

そうすると、以下に作者の名前をクリックするとフィルタリングする
簡易的なアプリが作成されたかと思います。

download.gif

今回作成したアプリケーションは、見かけ上スペックの高いPCでは動作として問題なく見えますが、
性能の低いPCの場合は問題が浮き彫りになります。

今回は擬似的にPCの性能を下げて確認してみます!

スクリーンショット 2023-08-18 1.01.29.png

Google Chromeの開発者ツールからパフォーマンス→CPUで6✖︎減速を選ぶと分かりやすいです。

src/components/Transition.tsxのクリックした際に発火する関数のonClickAssignee
clickしたよという文言を発火するようにして確認します

GIF トラン.gif

上記のように、押してからフィルタリング結果が出るまでに1.5秒前後かかってしまいます。
正直言ってユーザー体験が悪いかなと思います。

理想系としては、「田中」を押した際に押したという判定に関してはユーザーにすぐわかるようにしたほうが良いと思います。
フィルタリングに関しては時間がかかるため、ステート更新が出来次第反映していくみたいな動きにすることで
ユーザーにより直感的にアプリを使ってもらえるUXを提供することができます。

それではまた、実際にコードを追加していきましょう。

src/components/Transition.tsx
import { useState, startTransition } from 'react';
import { Avatar } from "./Avatar";
import { BlogList } from './BlogList';

export type Blog = {
  id: number;
  title: string;
  author: string;
}

const member = {
  a: '田中',
  b: '山田',
  c: '佐藤',
}

const generateDummyBlogs = (): Blog[] => {
  return Array(5000).fill('').map((_, index) => {
    const addedIndex = index + 1
    return {
      id: addedIndex,
      title: `タイトル${addedIndex}`,
      author: addedIndex % 3 === 0 ? member.a : addedIndex % 3 === 1 ? member.b : member.c,
    }
  })
}

const blogs = generateDummyBlogs();

const filteringAssignee = (author: string) => {
  if (author === '') return blogs;
  return blogs.filter((Blog) => Blog.author === author);
}

export const Transition = () => {
  const [selectedAssignee, setSelectedAssignee] = useState<string>('');
  const [blogList, setBlogList] = useState<Blog[]>(blogs);

  const onClickAssignee = (author: string) => {
    console.log('clickしたよ')
    setSelectedAssignee(author);
    startTransition(() => {
      setBlogList(filteringAssignee(author));
    });
  }
  return (
    <div>
      <p>ブログ一覧</p>
      <div style={{ display: 'flex', justifyContent: 'center' }}>
        <Avatar isSelected={selectedAssignee === member.a} onClick={onClickAssignee}>{member.a}</Avatar>
        <Avatar isSelected={selectedAssignee === member.b} onClick={onClickAssignee}>{member.b}</Avatar>
        <Avatar isSelected={selectedAssignee === member.c} onClick={onClickAssignee}>{member.c}</Avatar>
      </div>
      <br />
      <button onClick={() => onClickAssignee('')}>リセット</button>
      <br />
      <br />
      <BlogList blogList={blogList} />
    </div >
  )
}

使い方は簡単で、中に関数をとってトランジションしたい関数をstartTransitionの中に記述します。
あくまで緊急性の高くないものをするということは注意してください。

山田、田中の順にクリックしていきます。

startTran.gif

上記を見るとわかると思いますが、時間差でボタンの枠が光った後にフィルタリングされると思います。
authorのstate更新については即座に行われていますが、ブログのフィルタリングについては時間差で
実行されています。

これで、押して反応が出た瞬間にユーザーは反映されたことがすぐにわかりユーザー体験の
向上につながると思います。

これがTransitionという機能になっていきます。

src/components/Transition.tsx
import { useState, useTransition } from 'react';
import { Avatar } from "./Avatar";
import { BlogList } from './BlogList';

export type Blog = {
  id: number;
  title: string;
  author: string;
}

const member = {
  a: '田中',
  b: '山田',
  c: '佐藤',
}

const generateDummyBlogs = (): Blog[] => {
  return Array(5000).fill('').map((_, index) => {
    const addedIndex = index + 1
    return {
      id: addedIndex,
      title: `タイトル${addedIndex}`,
      author: addedIndex % 3 === 0 ? member.a : addedIndex % 3 === 1 ? member.b : member.c,
    }
  })
}

const blogs = generateDummyBlogs();

const filteringAssignee = (author: string) => {
  if (author === '') return blogs;
  return blogs.filter((Blog) => Blog.author === author);
}

export const Transition = () => {
  const [isPending, startTransition] = useTransition();
  const [selectedAssignee, setSelectedAssignee] = useState<string>('');
  const [blogList, setBlogList] = useState<Blog[]>(blogs);
  // const [isShowList, setIsShowList] = useState<boolean>(false);

  const onClickAssignee = (author: string) => {
    console.log('clickしたよ')
    setSelectedAssignee(author);
    startTransition(() => {
      setBlogList(filteringAssignee(author));
    });
  }
  return (
    <div>
      <p>ブログ一覧</p>
      <div style={{ display: 'flex', justifyContent: 'center' }}>
        <Avatar isSelected={selectedAssignee === member.a} onClick={onClickAssignee}>{member.a}</Avatar>
        <Avatar isSelected={selectedAssignee === member.b} onClick={onClickAssignee}>{member.b}</Avatar>
        <Avatar isSelected={selectedAssignee === member.c} onClick={onClickAssignee}>{member.c}</Avatar>
      </div>
      <br />
      <button onClick={() => onClickAssignee('')}>リセット</button>
      <br />
      <br />
      {/* <button onClick={() => setIsShowList(!isShowList)}>表示/非表示</button> */}
      <BlogList blogList={blogList} />
    </div >
  )
}

useTransitionについて

useTransitionとはトランジション中にユーザーにインタラクションを返すを返すためのフックになります。
下記のようにコードを変更してisPendingを使用するようにしてください。

src/components/Transition.tsx
import { useState, useTransition } from 'react';
import { Avatar } from "./Avatar";
import { BlogList } from './BlogList';

export type Blog = {
  id: number;
  title: string;
  author: string;
}

const member = {
  a: '田中',
  b: '山田',
  c: '佐藤',
}

const generateDummyBlogs = (): Blog[] => {
  return Array(5000).fill('').map((_, index) => {
    const addedIndex = index + 1
    return {
      id: addedIndex,
      title: `タイトル${addedIndex}`,
      author: addedIndex % 3 === 0 ? member.a : addedIndex % 3 === 1 ? member.b : member.c,
    }
  })
}

const blogs = generateDummyBlogs();

const filteringAssignee = (author: string) => {
  if (author === '') return blogs;
  return blogs.filter((Blog) => Blog.author === author);
}

export const Transition = () => {
  const [isPending, startTransition] = useTransition();
  const [selectedAssignee, setSelectedAssignee] = useState<string>('');
  const [blogList, setBlogList] = useState<Blog[]>(blogs);

  const onClickAssignee = (author: string) => {
    console.log('clickしたよ')
    setSelectedAssignee(author);
    startTransition(() => {
      setBlogList(filteringAssignee(author));
    });
  }
  return (
    <div>
      <p>ブログ一覧</p>
      <div style={{ display: 'flex', justifyContent: 'center' }}>
        <Avatar isSelected={selectedAssignee === member.a} onClick={onClickAssignee}>{member.a}</Avatar>
        <Avatar isSelected={selectedAssignee === member.b} onClick={onClickAssignee}>{member.b}</Avatar>
        <Avatar isSelected={selectedAssignee === member.c} onClick={onClickAssignee}>{member.c}</Avatar>
      </div>
      <br />
      <button onClick={() => onClickAssignee('')}>リセット</button>
      <br />
      <br />
      <BlogList blogList={blogList} isPending={isPending} />
    </div >
  )
}

isPendingはstartTransision内でset関数のstateが更新中かどうかをフラグをbooleanとして返してくれます。
このフラグを用いてユーザーに対してインタラクションを提供することができます。

src/components/BlogList.tsx
import type { Blog } from "./Transition"

type Props = {
  blogList: Blog[]
  isPending: boolean
}

export const BlogList = ({ blogList, isPending }: Props) => {
  return (
    <>
      {
        blogList.map((blog) => (
          <div key={blog.id} style={{ width: '500px', margin: 'auto ', background: 'pink ', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', borderRadius: '10px', opacity: isPending ? 0.5 : 1 }}> 
            <p> タイトル:{blog.title}</p>
            <p>作成者:{blog.author}</p>
          </div >
        ))
      }
    </>
  )
}

opacity: isPending ? 0.5 : 1 }}として、更新中のものは半透明にします。

そうすると...

ispending.gif

「山田」を押すとフィルタリングされる前にユーザーに直感的にわかるようになるかと思います。
ここについては、ローディング中用のコンポーネントを表示したり今回のようにCSSを変更したりと
自由に見せることだができます!!

最後に

いかがでしたでしょうか。
今回はtransitionの中のuseTransitionについて解説させていただきました。

実際にドキュメントを見てスッと頭に入ってこないことも実コードを用いて、プレビューを表示することによって
理解出来ることも多々あります。

次回はおまけとして、Transitionのhookの一つであるuseDeferredValueをまとめていこうと思います💪

参考

11
8
0

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
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?