はじめに
前回はAutomatic Batching
について解説しましたが,今回はReact新機能のTransition
ついて解説していきます!
今回もReact18において重要な機能になっているので、今回の記事を見て概要だけでも
理解しておいてください!
以下前回記事
Transitionとは...?
以下公式より引用
トランジション(transition; 段階的推移)とは React における新たな概念であり、緊急性の高い更新 (urgent update) と高くない更新 (non-urgent update) を区別するためのものです。
・緊急性の高い更新とはタイプ、クリック、プレスといったユーザ操作を直接反映するものです。
・トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。タイプ、クリック、プレスのような緊急性の高い更新は、物理的な物体の挙動に関する我々の直観に反しないよう、即座に反応する必要があり、そうでないと「おかしい」と認識されてしまいます。一方でトランジション内では、ユーザは画面上であらゆる中間の値が見えることを期待していません。
簡単にいうとこの変更は優先度が低いことをReact側に伝える機能になります。
優先度が高いものをReact側に伝えれるわけではなく、あくまで低いものを伝えることによって
優先度の高い、低いを区別できるようにしてるのがポイントです。
実際にコードを動かしてみよう
transitionの機能を確認するために、
今回はブログの作成者をクリックしたらフィルタリングされるサンプルアプリを作成します。
React18の環境下で以下のファイルを作成して行ってください。
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 >
)
}
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 >
))
}
</>
)
}
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は下記のとおり記述してください。
import './App.css';
import { Transition } from './components/Transition';
function App() {
return (
<div className="App">
<Transition />
</div>
);
}
export default App;
そうすると、以下に作者の名前をクリックするとフィルタリングする
簡易的なアプリが作成されたかと思います。
今回作成したアプリケーションは、見かけ上スペックの高いPCでは動作として問題なく見えますが、
性能の低いPCの場合は問題が浮き彫りになります。
今回は擬似的にPCの性能を下げて確認してみます!
Google Chromeの開発者ツールからパフォーマンス→CPU
で6✖︎減速を選ぶと分かりやすいです。
src/components/Transition.tsx
のクリックした際に発火する関数のonClickAssignee
clickしたよという文言を発火するようにして確認します
上記のように、押してからフィルタリング結果が出るまでに1.5秒前後かかってしまいます。
正直言ってユーザー体験が悪いかなと思います。
理想系としては、「田中」を押した際に押したという判定に関してはユーザーにすぐわかるようにしたほうが良いと思います。
フィルタリングに関しては時間がかかるため、ステート更新が出来次第反映していくみたいな動きにすることで
ユーザーにより直感的にアプリを使ってもらえるUXを提供することができます。
それではまた、実際にコードを追加していきましょう。
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
の中に記述します。
あくまで緊急性の高くないものをするということは注意してください。
山田、田中の順にクリックしていきます。
上記を見るとわかると思いますが、時間差でボタンの枠が光った後にフィルタリングされると思います。
authorのstate更新については即座に行われていますが、ブログのフィルタリングについては時間差で
実行されています。
これで、押して反応が出た瞬間にユーザーは反映されたことがすぐにわかりユーザー体験の
向上につながると思います。
これがTransitionという機能になっていきます。
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
を使用するようにしてください。
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として返してくれます。
このフラグを用いてユーザーに対してインタラクションを提供することができます。
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 }}
として、更新中のものは半透明にします。
そうすると...
「山田」を押すとフィルタリングされる前にユーザーに直感的にわかるようになるかと思います。
ここについては、ローディング中用のコンポーネントを表示したり今回のようにCSSを変更したりと
自由に見せることだができます!!
最後に
いかがでしたでしょうか。
今回はtransitionの中のuseTransition
について解説させていただきました。
実際にドキュメントを見てスッと頭に入ってこないことも実コードを用いて、プレビューを表示することによって
理解出来ることも多々あります。
次回はおまけとして、Transitionのhookの一つであるuseDeferredValue
をまとめていこうと思います💪
参考