2019/07/10
React HooksによるFluxでタブを作るまで
LaunchCartのフォームのUIを、将来的にチームで開発することも見据え、フレームワークを使って組んでみようと考えたとき、部分導入が可能で、ルールがしっかりしているReactを使うことに思い至りました。
はじめ何も考えずにReactを使ってUIを作ろうとして、意図した動作をさせることができなかったため、Reactにおけるデータの管理方法について調べた結果、Reactが公式に推奨しているFluxというアーキテクチャの存在を知りました。
Fluxはアプリケーションのデザインパターンの一つであるMVC(Model View Controller)パターンの派生としてFacebookが提唱したもので、よりデータの流れをシンプルにしたものです。
ただ、元々ReactではFluxでアプリケーションを組むためのライブラリなどは内蔵されておらず、自前で組むか、Reduxのような非公式のライブラリを導入する必要がありました。
Reduxによるアプリケーション開発が普及してきている状況で、React HooksというReact標準のライブラリが導入され、React標準でReduxライクな記述が可能になりました。
React Hooks自体は、クラスコンポーネントによる複雑な記述を脱して、関数コンポーネント中心でアプリケーション開発をできるようにするためのものです。
そのReactHooksの機能の中でも、useReducer、useContext、createContextを利用することでReduxライクなFluxによるアプリケーション開発が可能になります。
今回は、タブを組むことを目指し、React HooksによるFluxの組み方を実践してみました。
Action
アクションタイプ
TabActionTypes.ts
1 2 3 4 5 6 | const TabActionTypes = { SELECT_TAB: 'SELECT_TAB', RESET_TABS: 'RESET_TABS' }; export default TabActionTypes; |
アクションの生成関数
TabActions.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import TabActionTypes from './TabActionTypes'; const TabActions = { selectTab(selectedIndex: number) { return { type: TabActionTypes.SELECT_TAB, payload: { selectedIndex: selectedIndex } } } }; export default TabActions; |
Reducerの登録とContextの生成、呼び出し
Reducer関数
TabReducer.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import TabActionTypes from './TabActionTypes'; import { Action, TabState } from './Types'; // 型の呼び出し。別途定義 export const initialTabState: TabState = { selectedIndex: 0 } export const TabReducer = (state: TabState, action: Action) => { switch (action.type) { case TabActionTypes.RESET_TABS: { return initialTabState; } case TabActionTypes.SELECT_TAB: { return { selectedIndex: action.payload.selectedIndex }; } default: { return state; } } } |
トップ階層のコンポーネントでのuseReducerによるReducerの登録と、state, dispatchの生成
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import React from 'react'; import { useReducer, useContext, createContext, Dispatch } from 'react'; import { TabState } from './Types'; import TabActions from './TabActions'; import { TabReducer, initialTabState } from './TabReducer'; import './App.scss'; // Contextの生成 const TabContext = createContext<TabState>(null as any); const TabDispatchContext = createContext<Dispatch<any>>(null as any); const App: React.FC = () => { // Reducerの登録と、state、dispatchの生成 const [state, dispatch] = useReducer(TabReducer, initialTabState); // ViewのコンポーネントをContextのコンポーネントで囲み、stateとdispatchをそれぞれ別のContextに登録 return ( <TabContext.Provider value={state}> <TabDispatchContext.Provider value={dispatch}> <Tabs /> <TabPanels /> </TabDispatchContext.Provider> </TabContext.Provider> ); } ・・・ |
下層コンポーネントでのContextを通じた、state、dispatchの呼び出し
App.tsx
1 2 3 4 5 6 7 8 9 10 | ・・・ /** * Tabs */ function Tabs () { const state = useContext(TabContext); // stateを登録したContextを通してstateを呼び出し const dispatch = useContext(TabDispatchContext); // dispatchを登録したContextを通してdispatchを呼び出し ・・・ } ・・・ |
dispatchの実行とstateの更新
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /** * Tabs */ function Tabs () { const state = useContext(TabContext); const dispatch = useContext(TabDispatchContext); // クリック時にアクションを生成して、dispatchを実行 function onClick (selectedIndex: number) { dispatch(TabActions.selectTab(selectedIndex)) } return ( <ul className='tabs'> { tabItems.map((value, index) => { return ( <li key={index} aria-controls={'tabPanel_' + index} aria-selected={state.selectedIndex === index} role="tab" id={'tab_' + index} className='tabs__item' onClick={() => onClick(index)}>{value}</li> ) }) } </ul> ); } |
stateの反映のみが必要で、アクションの生成を行わないコンポーネントでは、stateのみ呼び出し
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * Tab Panels */ function TabPanels () { const state = useContext(TabContext); // stateの呼び出し return ( <div className='tabPanels'> { tabItems.map((value, index) => { return ( <div key={index} role="tabpanel" aria-hidden={state.selectedIndex !== index} aria-labelledby={'tab_' + index} id={'tabPanel_' + index} className='tabPanels__item'>Tab Panel {index + 1}</div> ) }) } </div> ); } |
実際にやってみた感想としては、しっかりとデータの設計をしておかないと組めないので、とてもプログラムを設計する勉強になりました。
今回はタブというシンプルなUIを例にしたので、ReactやFluxの恩恵は少ないですが、階層が深かったり複雑なデータを扱い様なUIを開発したり、チームでUIを組み合わせていくときに、この仕組はとても重宝するのではと感じました。
参考にさせていただいたサイト
FluxによるReactアプリの状態管理 ReactHooks編 – Qiita
Replacing Redux with react hooks and context (part 1)
App.tsx 全文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | import React from 'react'; import { useReducer, useContext, createContext, Dispatch } from 'react'; import { TabState } from './Types'; import TabActions from './TabActions'; import { TabReducer, initialTabState } from './TabReducer'; import './App.scss'; const TabContext = createContext<TabState>(null as any); const TabDispatchContext = createContext<Dispatch<any>>(null as any); const App: React.FC = () => { const [state, dispatch] = useReducer(TabReducer, initialTabState); return ( <TabContext.Provider value={state}> <TabDispatchContext.Provider value={dispatch}> <Tabs /> <TabPanels /> </TabDispatchContext.Provider> </TabContext.Provider> ); } const tabItems: string[] = [ 'Tab1', 'Tab2', 'Tab3' ] /** * Tabs */ function Tabs () { const state = useContext(TabContext); const dispatch = useContext(TabDispatchContext); function onClick (selectedIndex: number) { dispatch(TabActions.selectTab(selectedIndex)) } return ( <ul className='tabs'> { tabItems.map((value, index) => { return ( <li key={index} aria-controls={'tabPanel_' + index} aria-selected={state.selectedIndex === index} role="tab" id={'tab_' + index} className='tabs__item' onClick={() => onClick(index)}>{value}</li> ) }) } </ul> ); } /** * Tab Panels */ function TabPanels () { const state = useContext(TabContext); return ( <div className='tabPanels'> { tabItems.map((value, index) => { return ( <div key={index} role="tabpanel" aria-hidden={state.selectedIndex !== index} aria-labelledby={'tab_' + index} id={'tabPanel_' + index} className='tabPanels__item'>Tab Panel {index + 1}</div> ) }) } </div> ); } export default App; |
Types.ts
1 2 3 4 5 6 7 8 | export type Action = { type: string; payload: any; } export type TabState = { selectedIndex: number; } |
index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 | import React from 'react'; import ReactDOM from 'react-dom'; import './index.scss'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister(); |
Author Profile
NINOMIYA
Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。
SHARE