Skip to content
This repository has been archived by the owner on Jun 26, 2023. It is now read-only.

Commit

Permalink
Refactor for using Better Context Api pattern
Browse files Browse the repository at this point in the history
- no need to declare initial state for using context upfront
- SRP satisfied context(providers)

using multiple providers are preferred
- facebook/react#15156 (comment)
  • Loading branch information
godon019 committed Sep 29, 2019
1 parent 7279ed3 commit 4e56da6
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 51 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -62,3 +62,5 @@ coverage/
package-lock.json

ios/Pods/

.vscode
32 changes: 32 additions & 0 deletions .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"
}
11 changes: 7 additions & 4 deletions 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 (
<Provider>
<SwitchNavigator />
</Provider>
<AppProvider>
<ThemeProvider>
<SwitchNavigator />
</ThemeProvider>
</AppProvider>
);
}

Expand Down
17 changes: 5 additions & 12 deletions 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(
{
Expand All @@ -22,12 +21,6 @@ export interface ScreenProps {
}

export default function Navigator(): React.ReactElement {
const { state } = useContext(AppContext);
const { theme } = state;

return (
<ThemeProvider theme={createTheme(theme)}>
<AppContainer screenProps={{ theme: createTheme(theme) }} />
</ThemeProvider>
);
const { theme } = useThemeProvicer();
return <AppContainer screenProps={{ theme }} />;
}
2 changes: 0 additions & 2 deletions src/contexts/index.ts
@@ -1,3 +1 @@
import * as React from 'react';

export const AppContext = React.createContext<any>(null);
79 changes: 46 additions & 33 deletions src/providers/AppProvider.tsx
@@ -1,60 +1,73 @@
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<Context>();

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,
job: '',
},
};

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<Action>) => (user: User) => {
dispatch({
type: 'set-user',
payload: { user },
});
};

const resetUser = (dispatch: React.Dispatch<Action>) => () => {
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>(reducer, initialState);

const actions = {
setUser: setUser(dispatch),
resetUser: resetUser(dispatch),
};

return (
<AppContext.Provider value={value}>{props.children}</AppContext.Provider>
);
return <Provider value={{ state, ...actions }}>{props.children}</Provider>;
}

export { AppConsumer, AppProvider, AppContext };
export { useCtx as useAppContext, AppProvider };
38 changes: 38 additions & 0 deletions 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<React.SetStateAction<ThemeType>>;
}
const [useCtx, Provider] = createCtx<Context>();

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 (
<Provider
value={{
changeTheme,
themeType,
theme,
}}
>
<OriginalThemeProvider theme={theme}>
{props.children}
</OriginalThemeProvider>
</Provider>
);
}

export { useCtx as useThemeProvicer, ThemeProvider };
18 changes: 18 additions & 0 deletions 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<A>(): readonly [
() => A,
React.ProviderExoticComponent<React.ProviderProps<A | undefined>>,
] {
const ctx = React.createContext<A | undefined>(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;

0 comments on commit 4e56da6

Please sign in to comment.