From 4e56da6b0549f5082c21b72ff295ce2f8fea0c40 Mon Sep 17 00:00:00 2001 From: Gordon Freeman Date: Sun, 29 Sep 2019 17:15:08 +0900 Subject: [PATCH] Refactor for using Better Context Api pattern - no need to declare initial state for using context upfront - SRP satisfied context(providers) using multiple providers are preferred - https://github.com/facebook/react/issues/15156#issuecomment-474590693 --- .gitignore | 2 + .vscode/settings.json | 32 ++++++++ src/App.tsx | 11 ++- src/components/navigation/SwitchNavigator.tsx | 17 ++-- src/contexts/index.ts | 2 - src/providers/AppProvider.tsx | 79 +++++++++++-------- src/providers/ThemeProvider.tsx | 38 +++++++++ src/utils/createCtx.ts | 18 +++++ 8 files changed, 148 insertions(+), 51 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/providers/ThemeProvider.tsx create mode 100644 src/utils/createCtx.ts diff --git a/.gitignore b/.gitignore index 52326e42..41f14643 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ coverage/ package-lock.json ios/Pods/ + +.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..87301988 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + //eslint extension options + "eslint.enable": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + + // prettier extension setting + "editor.formatOnSave": true, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "prettier.singleQuote": true, + "prettier.trailingComma": "all", + "prettier.arrowParens": "always", + "prettier.jsxSingleQuote": true, + // relative path is preferred + "javascript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifier": "relative" +} diff --git a/src/App.tsx b/src/App.tsx index 589efd30..99c8b1ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,15 @@ -import { AppProvider as Provider } from './providers'; +import { AppProvider } from './providers'; import React from 'react'; import SwitchNavigator from './components/navigation/SwitchNavigator'; +import { ThemeProvider } from 'providers/ThemeProvider'; function App(): React.ReactElement { return ( - - - + + + + + ); } diff --git a/src/components/navigation/SwitchNavigator.tsx b/src/components/navigation/SwitchNavigator.tsx index ae115392..1329e00f 100644 --- a/src/components/navigation/SwitchNavigator.tsx +++ b/src/components/navigation/SwitchNavigator.tsx @@ -1,10 +1,9 @@ -import React, { useContext } from 'react'; -import { Theme, createTheme } from '../../theme'; import { createAppContainer, createSwitchNavigator } from 'react-navigation'; -import { AppContext } from '../../contexts'; +import React from 'react'; import RootNavigator from './RootStackNavigator'; -import { ThemeProvider } from 'styled-components'; +import { Theme } from '../../theme'; +import { useThemeProvicer } from 'providers/ThemeProvider'; const SwitchNavigator = createSwitchNavigator( { @@ -22,12 +21,6 @@ export interface ScreenProps { } export default function Navigator(): React.ReactElement { - const { state } = useContext(AppContext); - const { theme } = state; - - return ( - - - - ); + const { theme } = useThemeProvicer(); + return ; } diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 3eb4b0b0..6ed986e6 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,3 +1 @@ import * as React from 'react'; - -export const AppContext = React.createContext(null); diff --git a/src/providers/AppProvider.tsx b/src/providers/AppProvider.tsx index 3f0aa5fc..f71f8c40 100644 --- a/src/providers/AppProvider.tsx +++ b/src/providers/AppProvider.tsx @@ -1,34 +1,22 @@ import React, { useReducer } from 'react'; -import { AppContext } from '../contexts'; -import { ThemeType } from '../theme'; import { User } from '../types'; +import createCtx from 'utils/createCtx'; -const AppConsumer = AppContext.Consumer; - -interface Action { - type: 'reset-user' | 'set-user' | 'change-theme-mode'; - payload: { - theme: ThemeType; - user: { - displayName: string; - age: number; - job: string; - }; - }; +interface Context { + state: State; + setUser: (user: User) => void; + resetUser: () => void; } +const [useCtx, Provider] = createCtx(); -interface Props { - children?: React.ReactElement; -} +type dispatchType = 'reset-user' | 'set-user'; export interface State { user: User; - theme: ThemeType; } const initialState: State = { - theme: ThemeType.LIGHT, user: { displayName: '', age: 0, @@ -36,25 +24,50 @@ const initialState: State = { }, }; -const reducer = (state: State, action: Action): State => { - // prettier-ignore +interface Action { + type: dispatchType; + payload: State; +} + +interface Props { + children?: React.ReactElement; +} + +type Reducer = (state: State, action: Action) => State; + +const setUser = (dispatch: React.Dispatch) => (user: User) => { + dispatch({ + type: 'set-user', + payload: { user }, + }); +}; + +const resetUser = (dispatch: React.Dispatch) => () => { + dispatch({ + type: 'reset-user', + payload: initialState, + }); +}; + +const reducer: Reducer = (state = initialState, action) => { switch (action.type) { - case 'change-theme-mode': - return { ...state, theme: action.payload.theme }; - case 'reset-user': - return { ...state, user: initialState.user }; - case 'set-user': - return { ...state, user: action.payload.user }; + case 'reset-user': + case 'set-user': + return { ...state, user: action.payload.user }; + default: + return state; } }; function AppProvider(props: Props): React.ReactElement { - const [state, dispatch] = useReducer(reducer, initialState); - const value = { state, dispatch }; + const [state, dispatch] = useReducer(reducer, initialState); + + const actions = { + setUser: setUser(dispatch), + resetUser: resetUser(dispatch), + }; - return ( - {props.children} - ); + return {props.children}; } -export { AppConsumer, AppProvider, AppContext }; +export { useCtx as useAppContext, AppProvider }; diff --git a/src/providers/ThemeProvider.tsx b/src/providers/ThemeProvider.tsx new file mode 100644 index 00000000..bbce275a --- /dev/null +++ b/src/providers/ThemeProvider.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { Theme, ThemeType, createTheme } from 'theme'; + +import { ThemeProvider as OriginalThemeProvider } from 'styled-components'; +import createCtx from 'utils/createCtx'; + +interface Context { + theme: Theme; + themeType: ThemeType; + changeTheme: React.Dispatch>; +} +const [useCtx, Provider] = createCtx(); + +const initialThemeType: ThemeType = ThemeType.LIGHT; + +interface Props { + children?: React.ReactElement; +} + +function ThemeProvider(props: Props): React.ReactElement { + const [themeType, changeTheme] = useState(initialThemeType); + const theme = createTheme(themeType); + return ( + + + {props.children} + + + ); +} + +export { useCtx as useThemeProvicer, ThemeProvider }; diff --git a/src/utils/createCtx.ts b/src/utils/createCtx.ts new file mode 100644 index 00000000..574d5179 --- /dev/null +++ b/src/utils/createCtx.ts @@ -0,0 +1,18 @@ +import React from 'react'; + +// create context with no upfront defaultValue +// without having to do undefined check all the time +// prettier-ignore +function createCtx(): readonly [ + () => A, + React.ProviderExoticComponent>, + ] { + const ctx = React.createContext(undefined); + function useCtx(): A { + const c = React.useContext(ctx); + if (!c) throw new Error('useCtx must be inside a Provider with a value'); + return c; + } + return [useCtx, ctx.Provider] as const; // make TypeScript infer a tuple, not an array of union types +} +export default createCtx;