Successfully reported this slideshow.
Your SlideShare is downloading. ×

ルーター自前実装の話

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad

Check these out next

1 of 43 Ad

More Related Content

Similar to ルーター自前実装の話 (20)

Recently uploaded (20)

Advertisement

ルーター自前実装の話

  1. 1. ルーター自前実装のお話
  2. 2. 自己紹介 KazushiKawamura https://github.com/kawamurakazushi https://www.producthunt.com/@kawamurakazushi -- 2020.06~2021.10 AxelspaceCorporation. GroundSystemEngineer 2021.11~ Henry,Inc. FrontendEngineer
  3. 3. 今日話すこと Henryでのナビゲーションの課題 他のルーターのライブラリについて DEMO なにを実現したいのか? どう実現したのか?
  4. 4. Henryでのナビゲーションの課題 タブナビゲーションをネストしたい ページのライフサイクルに合わせた状態管理をしたい スクロールの位置 選択されたタブの位置 検索内容
  5. 5. Henryでのナビゲーションの課題 タブナビゲーションをネストしたい
  6. 6. Henryでのナビゲーションの課題 タブナビゲーションをネストしたい
  7. 7. Henryでのナビゲーションの課題 タブナビゲーションをネストしたい
  8. 8. Henryでのナビゲーションの課題 タブナビゲーションをネストしたい
  9. 9. Henryでのナビゲーションの課題 タブナビゲーションをネストしたい
  10. 10. Henryでのナビゲーションの課題 ページのライフサイクルに合わせた状態管理をしたい
  11. 11. DEMO
  12. 12. ルーターライブラリ react-router / reach-router (普通のwebrouter) queryparameter?parse? react-navigation react-native用のライブラリ 実現したい挙動は近いが、reactnativeforwebを利用する必要がある。 https://reactnavigation.org/docs/web-support/ react-stack-nav https://github.com/tuckerconnelly/react-stack-nav ネストしたタブがサポートされてない 運用されてない
  13. 13. 自前で実装する
  14. 14. 何を実現したいのか? 通常のルーター機能 タブナビゲーションをネストしたい ページのライフサイクルに合わせた状態管理をしたい
  15. 15. どう実現したのか? ナビゲーションに必要な全ての情報をContextに持たせることによって実現
  16. 16. Context interface CoordinatorContextState { coordinator: CoordinatorState; setCoordinator: (coordinator: CoordinatorState) => void; } const CoordinatorContext = createContext<CoordinatorContextState | null>(null);
  17. 17. Type export type CoordinatorState<T extends string = string> = { name: string; currentStack: T; stacks: Record<T, PageState[]>; }; type PageState = { states: Record<string, any>; coordinators: Record<string, CoordinatorState>; } & Page; type Page = | { type: "reception" } | { type: "sessionDetail"; sessionUuid: string } | { type: "patientDetail"; patientUuid: string } | { type: "settingIndex" }; // ...
  18. 18. Type
  19. 19. Type
  20. 20. Type
  21. 21. Example { name: "ROOT", currentStack: "reception", stacks: { reception: [ { type: "reception", states: { sessionListTab: "all", }, coordinators: {}, }, ], patients: [ { type: "patientIndex", states: {}, coordinators: {}, }, ], settings: [ { type: "settingIndex", states: {}, coordinators: {}, }, ], }, };
  22. 22. CoordinatorState から画面を表示
  23. 23. CoordinatorState から画面を表示 CoordinatorContext のselectorやmutation用の関数などを定義する useCoordinator export const useCoordinator = () => { const context = useContext(CoordinatorContext); if (!context) { throw new Error("Context is not initialized yet."); } const { coordinator, setCoordinator } = context; const currentPage = useMemo(() => getCurrentPage(coordinator), [coordinator]); // ... return { ...context, currentPage }; }; const getCurrentPage = (coordinator: CoordinatorState): PageState => { // coordinatorStateのcurrentStackの配列の最後のページを取得する return last(coordinator.stacks[coordinator.currentStack]); };
  24. 24. CoordinatorState から画面を表示 currentPage から場合分けでページを表示をする const CurrentPage: React.FC = () => { const { currentPage } = useCoordinator(); if (!currentPage) return <NotFound />; switch (currentPage.type) { case "reception": return <ReceptionPage />; case "sessionDetail": return <SessionDetailPage uuid={currentPage.sessionUuid} />; case "patientIndex": return <PatientIndex />; case "settingIndex": return <SettingIndexPage />; case "editorTemplateDetail": return <EditorTemplateDetailPage uuid={currentPage.editorTemplateUuid} />; //... } return null; };
  25. 25. CoordinatorState から画面を表示 function App() { return ( <RootCoordinatorProvider> <CurrentPage /> </RootCoordinatorProvider> ); }
  26. 26. URLからCoordinatorState を初期化する ルーターなのでURLから期待する CoordinatorState を初期化する必要がある。
  27. 27. RootCoordinatorProvider に初期化する処理を行う export const RootCoordinatorProvider: React.FC = ({ children }) => { const [coordinator, setCoordinator] = useState(routerInitialState); useEffect(() => { const path = window.location.pathname; // `/reception` だったら // { type: "reception", // states: {}, // coordinators: {} } const pageState = pathToPageState(path); if (pageState) { setCoordinator((c) => { const newCoordinator = // pageStateを使って、ゴニョゴニョ return newCoordinator; }); } }, []); return ( <CoordinatorContext.Provider value={{ coordinator, setCoordinator }}>{children}</CoordinatorContext.Provider> ); }; pathToPageState はURLPatternAPIを用いてる https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API
  28. 28. ページの遷移 useCoordinator に遷移用の関数を定義する
  29. 29. export const useCoordinator = () => { // ... const push = useCallback( (page: Page) => { const newCoordinator = produce(coordinator, (c) => { c.stacks[c.currentStack].push({ ...page, coordinators: {}, states: {}, }); }); setCoordinator(newCoordinator); }, [setCoordinator, coordinator] ); const openPage = useCallback( (page: Page, e?: React.MouseEvent) => { push(page); }, [push] ); return { ...context, currentPage, openPage }; };
  30. 30. BrowserBack/Fowardの対応 history.replaceState 各historyに対してstateobjectを保存することできる https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState popstate event historyが変更されたときに発火される https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event
  31. 31. export const RootCoordinatorProvider: React.FC = ({ children }) => { //... useEffect(() => { window.history.replaceState(coordinator, ""); }, [coordinator]); useEffect(() => { window.addEventListener("popstate", (e) => { if (!e.state) { return; } setCoordinator(e.state); }); }, [setCoordinator]); //... }; coordinator が変更されたら、 history.replaceState で新しい状態に置き換える popState のイベントが発行されてたら e.state から coordinator を取得してセット
  32. 32. Coordinatorをネストする方法 Contextをネストすることで実現
  33. 33. 使ってる Context の型はRootで利用してるものと一緒なので 同様に coordinator と setCoordinator を用意する必要がある。 export const CoordinatorProvider: React.FC<{ coordinator: CoordinatorState; }> = ({ children, coordinator: initialCoordinator }) => { //... return ( <CoordinatorContext.Provider value={{ coordinator, setCoordinator }}> {children} </CoordinatorContext.Provider> ); };
  34. 34. 親のContextから現在のページのcoordinatorを返す export const CoordinatorProvider: React.FC = ({ children, coordinator: initialCoordinator, }) => { const parentCoordinator = useCoordinator(); const coordinator: CoordinatorState | null = useMemo(() => { const name = initialCoordinator.name; const page = getCurrentPage(parentCoordinator.coordinator); if (page && page.coordinators[name]) { return page.coordinators[name]; } return initialCoordinator; }, [parentCoordinator.coordinator]); //... return ( <CoordinatorContext.Provider value={{ coordinator, setCoordinator }}> {children} </CoordinatorContext.Provider> ); };
  35. 35. 親のContextを変更する export const CoordinatorProvider: React.FC<{ coordinator: CoordinatorState; }> = ({ children, coordinator: initialCoordinator }) => { const parentCoordinator = useCoordinator(); // ... const setCoordinator = useCallback( (coordinator: CoordinatorState) => { const newCoordinator = produce(parentCoordinator.coordinator, (draft) => { draft.stacks[draft.currentStack][ draft.stacks[draft.currentStack].length - 1 ].coordinators = { [name]: coordinator }; }); parentCoordinator.setCoordinator(newCoordinator); }, [parentCoordinator] ); return ( <CoordinatorContext.Provider value={{ coordinator, setCoordinator }}> {children} </CoordinatorContext.Provider> ); };
  36. 36. Context をネストすることにより 子CoordinatorProvider の下で useCoordinator を使えば、同じ関数でも 子の CoordinatorState でのページ遷移が実現する
  37. 37. ページでの使用例 const SettingIndexPage: React.FC<{}> = () => { const initialCoordinator: CoordinatorState = { name: "SETTINGS", currentStack: "editorTemplate", stacks: { editorTemplate: [ { type: "editorTemplateIndex", states: {}, coordinators: {}, }, ], nonHealthcareSystemAction: [ { type: "nonHealthcareSystemActionIndex", states: {}, coordinators: {}, }, ], }, }; return ( <Coordinator coordinator={initialCoordinator}> <SettingsTabNav /> <CurrentPage /> </Coordinator> ); };
  38. 38. ページのライフサイクルに合わせた状態管理をしたい
  39. 39. 各Pageに states を用意しており、そこに必要な情報を保存が可能。 { name: "ROOT", currentStack: "reception", stacks: { reception: [ { type: "reception", states: { sessionListTab: "all", // <-- ここ! }, coordinators: {}, }, ], patients: [ { type: "patientIndex", states: {}, coordinators: {}, }, ], settings: [ { type: "settingIndex", states: {}, coordinators: {}, }, ], }, };
  40. 40. 選択されたリストタブをPageState に保存し参照する const ReceptionPage: React.FC<{}> = () => { const { state, setState } = useStates<SessionListTab>( "sessionListTab", "all" ); return ( <Page name="Reception | 受付"> <Tabs index={tabToIndex(state)} onChange={(i) => setState(indexToTab(i))}> <TabList> <Tab>すべて</Tab> <Tab>診察まち</Tab> <Tab>検査まち</Tab> </TabList> <TabPanels> <TabPanel>すべて</TabPanel> <TabPanel>診察まち</TabPanel> <TabPanel>検査まち</TabPanel> </TabPanels> </Tabs> </Page> ); };
  41. 41. export function useStates<T>(id: string, defaultValue: T) { const { currentPage, setCoordinator, coordinator } = useCoordinator(); const state: T = useMemo(() => { return currentPage?.states[id] ?? defaultValue; }, [currentPage, id, defaultValue]); const setState = useCallback((newState: T) => { const newCoordinator = produce(coordinator, (draft) => { const currentPage = last(draft.stacks[draft.currentStack]); currentPage.states = { ...currentPage.states, [id]: newState }; }); setCoordinator(newCoordinator); }, []); return { state, setState }; }
  42. 42. 今日話したこと Henryでのナビゲーションの課題 他のルーターのライブラリについて DEMO なにを実現したいのか? どう実現したのか?
  43. 43. Thanks

×