Closed15

Reduxに再入門する

nissy-devnissy-dev

https://redux.js.org/tutorials/essentials/part-1-overview-concepts

Reduxが役に立つところ

  • アプリケーションの状態が多い
  • アプリケーションの状態が頻繁に更新される
  • アプリケーションの状態の更新ロジックが複雑

ツール

  • Redux Toolkit でロジックを書くことがおすすめ
  • デバッグは、Redux DevTools Extension​のChrome拡張でやる

Reduxのコンセプト

By defining and separating the concepts involved in state management and enforcing rules that maintain independence between views and states, we give our code more structure and maintainability.

ViewとStateの分離を可能にする。状態を単一集中で管理し、状態更新の方法に規約を設けることで、状態やコードを予測しやすくする。

Reduxの基本用語

  • Action
    • type と payload フィールドを持つオブジェクト
    • typeは、アプリケーションに対する操作の名前をつける
      • "domain/eventName"が良い、"todos/todoAdded"
      • ツールが自動生成したりするから意識するところではないかも...
    • Action CreatorsはActionを作成する関数のこと
  • Reducer
    • イベントを受け取って新しい状態を定義する
    • 3つの原則
      • stateとactionのみを使って状態を更新
      • 更新は必ずimmutableに行う (これはreactのshallw equalの仕組みと関係があるんだろうな)
      • 非同期なロジックを定義することや副作用を起こすのは禁止
  • Dispatch
    • stateを更新する唯一の手段
    • 引数にActionを渡し、Dispatchを呼ぶことでstateを更新できる
nissy-devnissy-dev

https://redux.js.org/tutorials/essentials/part-2-app-structure

Immutableな更新をなぜするのか

  • UIの更新にバグが生まれる可能性がある
  • どのようにstateが更新されたのか、理解しにくくなる
  • テスト、デバッグがしにくい

Redux Toolkitは、immutableな更新のためにimmerを使っている。immerはProxy APIを使って、通常のmutable操作をimmutableな操作に変えている。

非同期ロジックについて

  • デフォルトでは Redux Thunk を使う
  • Thunk Action
    • dispatch, getStateを受け取って、関数内で非同期にdispatchする
  • 呼び出し側では通常のアクションと同様に扱える
    • dispatch(asyncThunkAction()) のように
    • UIに非同期であることを意識させない

Reduxで扱うstateについて

  • 一箇所でしか使わない揮発性のあるstateについては、Reduxでは扱わないようにすべき
  • いくつかのチェックリストがある
    • アプリケーションの他の部分がこのデータを気にするか?
    • 同じデータが複数のコンポーネントを動かすために使用されていますか?
    • stateをある時点に戻すことに価値がありますか?
    • データをキャッシュしたいですか?
nissy-devnissy-dev

https://redux.js.org/tutorials/essentials/part-5-async-logic

createAsyncThunkextraReducersを使うと非同期の処理がこんな感じでかける。
個人的には、普通にThunkAction書いた方がわかりやすい気がする。。。ducksパターンのように、operationとして一層レイヤーがあれば、そんなにUIに影響はないし

export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const response = await client.get('/fakeApi/posts')
  return response.data
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // omit existing reducers here
  },
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state, action) => {
        state.status = 'loading'
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        // Add any fetched posts to the array
        state.posts = state.posts.concat(action.payload)
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})
nissy-devnissy-dev

https://redux.js.org/tutorials/essentials/part-6-performance-normalization

useLayoutEffect

The signature is identical to useEffect, but it fires synchronously after all DOM mutations.

DOMのレンダリング終わってから同期的に副作用を起こすのがuseEffectとの違いみたい

Selectorのメモ化は大事、filterとか使っていたら参照が毎回変わるので注意が必要
Redux Toolkitだと、createSelectorを使うとReselectでメモ化されたセレクターを定義できる

細かい引数の意味は忘れてたけど以下が参考になった

const selectItemsByCategory = createSelector(
  [
    // Usual first input - extract value from `state`
    state => state.items,
    // Take the second arg, `category`, and forward to the output selector
    (state, category) => category
  ],
  // Output selector gets (`items, category)` as args
  (items, category) => items.filter(item => item.category === category)
)

再レンダリングを防ぐ方法として、必要なデータ以外を渡さないという話があるけど、
IDとかを子コンポーネントに渡して、必要なデータはそこで取得させる方法がある

List rendering can be optimized by having list parent components read just an array of item IDs, passing the IDs to list item children, and retrieving items by ID in the children

https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions

React DevtoolのProfileの使い方は細かく調べるか
https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html

nissy-devnissy-dev

https://redux.js.org/tutorials/essentials/part-7-rtk-query-basics

RTK Query vs React Query vs SWR
どれもdata fetchingとcachingをいい感じに管理してくれるAPIを提供するライブラリ

https://react-query.tanstack.com/comparison

やっぱり機能的にはほぼ一緒か... バンドルサイズは、SWRがかなり小さい

https://www.npmtrends.com/swr-vs-react-query

RTK Queryは以下の原則に基づいている

data fetching and caching" is really a different set of concerns than "state management"

RTK query を追加すると、今までのようにローディングの状態 (Pending/Success/Fail) などに注意を向けるのではなく、キャッシュされるデータをどう管理するのかという視点に移行する。

RTK Queryの使い方は以下の通り

createApiでsliceを定義して

// Import the RTK Query methods from the React-specific entry point
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Define our single API slice object
export const apiSlice = createApi({
  // The cache reducer expects to be added at `state.api` (already default - this is optional)
  reducerPath: 'api',
  // All of our requests will have URLs starting with '/fakeApi'
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  // The "endpoints" represent operations and requests for this server
  endpoints: builder => ({
    // The `getPosts` endpoint is a "query" operation that returns data
    getPosts: builder.query({
      // The URL for the request is '/fakeApi/posts'
      query: () => '/posts'
    })
  })
})

// Export the auto-generated hook for the `getPosts` query endpoint
export const { useGetPostsQuery } = apiSlice

Storeに統合する。キャッシュの制御のためにmiddlewareも追加する

export default configureStore({
  reducer: {
    posts: postsReducer,
    users: usersReducer,
    notifications: notificationsReducer,
    [apiSlice.reducerPath]: apiSlice.reducer
  },
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware().concat(apiSlice.middleware)
})

クエリとエンドポイントの組み合わせでレスポンスをキャッシュするので、ちゃんとキャッシュをrefetchする操作をどこかに入れないと、mutation操作した内容が反映されない
→ 通常、Mutation操作とrefetchは依存関係にあるので、RTK Queryはタグの機能を使って自動でキャッシュを更新することもできる

タグだけで自動でキャッシュの更新できると言っているけど、キャッシュの更新は複雑になりそうなので、そこまでうまくいくのか....?という感じはする。そもそもこれだと他のユーザーが更新したときもキャッシュ更新してくれるの....? (してくれない気がする)

nissy-devnissy-dev

一旦休憩で、スタイルガイドを眺める

Name State Slices Based On the Stored Data

あんまり意識してなかった

Your object should look like {users: {}, posts: {}}, rather than {usersReducer: {}, postsReducer: {}}

Organize State Structure Based on Data Types, Not Components​

loginScreen, usersList, postsListのような単位でstateを作らない

Normalize Complex Nested/Relational State

パフォーマンスのためにも、createEntityAdaptorがあるのでやりましょう

Model Actions as Events, Not Setters
Write Meaningful Action Names

アクションは何が起きているのかを関係に表したほうが良い。domain/eventName を推奨している。基本的に、setXXX or updateXXX はあんまり推奨されない。

Avoid Dispatching Many Actions Sequentially

アクションをたくさん送るのではなく、なるべくまとめましょう。自分もそうだったけど、setXXX とかの単位でアクションをつくると、アクションを作りがちになる気がする。
結局、操作単位でアクションを作るべきという↑の話と結構ちかいなー

Connect More Components to Read Data from the Store

これ意外だった。無駄な再レンダリングをへらすため。チュートリアルにもあったけど、投稿一覧表示するために、投稿のすべての情報いらないよねみたいな話。余計な情報を含んでいるせいで、表示に必要のない情報の更新によって無駄な再レンダリングが発生する。

Call useSelector Multiple Times in Function Components

これも無駄な再レンダリングをへらすため。

nissy-devnissy-dev

https://redux.js.org/tutorials/essentials/part-8-rtk-query-advanced

RTK Queryは、fetchして取得したデータについて、アクティブなサブスクリプション数を管理している。サブスクリプションしているコンポーネントが0になったら、60sでキャッシュから削除するようになっている。60sのところは、コンポーネント単位で設定をoverrideすることも可能。

また、キャッシュの細かい依存関係の定義も、タグだけではなく、IDも使ってできる。
ただ、思ったより複雑だ.... ただ、キャッシュ管理そのものが複雑なので、しょうがない気もする...

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  tagTypes: ['Post'],
  endpoints: builder => ({
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: (result = [], error, arg) => [
        'Post',  // 投稿全体のキャッシュ
        ...result.map(({ id }) => ({ type: 'Post', id })) // 投稿1つ1つのキャッシュ
      ]
    }),
    getPost: builder.query({
      query: postId => `/posts/${postId}`,
      providesTags: (result, error, arg) => [{ type: 'Post', id: arg }]
    }),
    addNewPost: builder.mutation({
      query: initialPost => ({
        url: '/posts',
        method: 'POST',
        body: initialPost
      }),
      invalidatesTags: ['Post']
    }),
    editPost: builder.mutation({
      query: post => ({
        url: `posts/${post.id}`,
        method: 'PATCH',
        body: post
      }),
      invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }]
    })
  })
})

レスポンスも加工できるので、Normalizeも簡単に組み込める

Normalized vs Document Caches

RTK Queryはドキュメントキャッシュの方式をとっている。Document Cachesは、エンドポイント + クエリの組み合わせで決まる。Normalized Cachesは、なるべくキャッシュするデータの重複をへらすように管理する。Apolloはこの方式をとっているらしい。

https://www.apollographql.com/blog/apollo-client/caching/demystifying-cache-normalization/

配列を返却するようなクエリ全体をキャッシュするときも、各要素をhash tableに突っ込んで、クエリ全体はhash値のみを参照する。

現状のRTK Queryは、ここまでチューニングするモチベーションや時間等はないので、対応していない。

Optimistic Updatesとは

https://kaminashi-developer.hatenablog.jp/entry/optimistic-update-in-spa

要はサーバーのレスポンスを待たずにUIを更新しちゃうこと
レスポンスの結果を予測できる、エラーになる可能性を事前にフロント側で排除できるときに使える

nissy-devnissy-dev

使い方

https://ja.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html

Browsing commits

  • render phase
    • 新しいrender結果と現在のrender結果のDOMの変化を探索する
  • commit phase
    • 新しい render 結果による差分をDOMに適用する

Dev Tool は、commit 単位でパフォーマンス結果を保存する。右上に出る横並びのバーは、commit 単位でパフォーマンス結果を測定したものであり、バーの高さがレンダリングにかかった時間に対応している。またバーは以下のように色分けされている

青色バー:現在選択している commit
黄色のバー:時間がかかった commit
緑色のバー:時間があまりかかっていない commit

Filtering commits

バーの左横にある設定ボタンで、閾値以上の時間がかかっているコミットだけを表示させたりできる。

Flame chart

特定のcommitにおけるアプリケーションの状態を表す。チャートの各バーは、Reactコンポーネント(例:App、Nav)を表していて、バーの長さと色はコンポーネントとその子のレンダリングにかかった時間を表す。

Rank chart

Flame chartをレンダリングに最も時間がかかったコンポーネントが一番上になるように並べ変えたもの。
(子コンポーネントの時間も含まれた時間が表示されるので、必然的にルートコンポーネントが上に来る)

他のAPIもあるけど、今は使えなさそう...

このスクラップは2022/02/05にクローズされました