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 (#102)
Browse files Browse the repository at this point in the history
* 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
- facebook/react#15156 (comment)

* Fix lint error

* Fix import error

* Refactor ThemeProvider

* Fix Intro test error

* Add using provider guide to Readme

* Add AllProviders and small fix

* Add  tests for ThemeProvider and AppProvider

* Fix import error
  • Loading branch information
godon019 committed Sep 30, 2019
1 parent 742e2a8 commit 30ed035
Show file tree
Hide file tree
Showing 21 changed files with 1,238 additions and 1,353 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -62,3 +62,5 @@ coverage/
package-lock.json

ios/Pods/

.vscode
38 changes: 38 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,38 @@
{
//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",
"files.exclude": {
"**/.classpath": true,
"**/.project": true,
"**/.settings": true,
"**/.factorypath": true
}
}
40 changes: 39 additions & 1 deletion README.md
Expand Up @@ -53,7 +53,7 @@ app/
│ └─ navigations
│ └─ screen
│ └─ shared
│ └─ contexts
│ └─ providers
│ └─ utils
│ └─ App.tsx
├─ test/
Expand Down Expand Up @@ -144,6 +144,44 @@ We've created test examples with jest-ts in `src/components/screen/__tests__` an
"prettier.arrowParens": "always",
"prettier.jsxSingleQuote": true
```
### Using Context Api
Whenever you add your own Context provider you can add it to `providers/` and use it inside of `providers/index.tsx`
- [Splitting provider is preferred](https://github.com/facebook/react/issues/15156#issuecomment-474590693)
```tsx
// Add providers here
const AllProviders = ({ isTest, children }: Props): React.ReactElement => {
return (
<AppProvider>
<ThemeProvider initialThemeType={isTest ? ThemeType.LIGHT : undefined}>
{children}
</ThemeProvider>
</AppProvider>
);
};
```
The `AllProviders` is being used at `App.tsx` and test files easily
```tsx
// App.tsx
function App(): React.ReactElement {
return (
<AllProviders>
<SwitchNavigator />
</AllProviders>
);
}
```
```tsx
// test files
const component = (props): React.ReactElement => {
return (
<AllProviders isTest>
<Intro {...props} />
</AllProviders>
);
};
```
> using consistent theme(ThemeType.LIGHT as default) explicitly is encouraged in testing for avoiding unexpected snapshot test errors

### Localization

Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Expand Up @@ -351,6 +351,6 @@ SPEC CHECKSUMS:
RNReanimated: 6abbbae2e5e72609d85aabd92a982a94566885f1
Yoga: d8c572ddec8d05b7dba08e4e5f1924004a177078

PODFILE CHECKSUM: fb138764701c547337419aaabee4fe89f2d68a64
PODFILE CHECKSUM: e30dfdd1fd9e5d0ba3c848277e4d1c2e269ffbab

COCOAPODS: 1.7.5
6 changes: 3 additions & 3 deletions src/App.tsx
@@ -1,12 +1,12 @@
import { AppProvider as Provider } from './providers';
import AllProviders from './providers';
import React from 'react';
import SwitchNavigator from './components/navigation/SwitchNavigator';

function App(): React.ReactElement {
return (
<Provider>
<AllProviders>
<SwitchNavigator />
</Provider>
</AllProviders>
);
}

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 { useThemeContext } 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 } = useThemeContext();
return <AppContainer screenProps={{ theme }} />;
}
29 changes: 7 additions & 22 deletions src/components/screen/Intro.tsx
Expand Up @@ -4,15 +4,15 @@ import {
NavigationState,
} from 'react-navigation';

import { AppContext } from '../../providers';
import Button from '../shared/Button';
import { IC_MASK } from '../../utils/Icons';
import React from 'react';
import { ThemeType } from '../../theme';
import { User } from '../../types';
import { View } from 'react-native';
import { getString } from '../../../STRINGS';
import styled from 'styled-components/native';
import { useAppContext } from '../../providers/AppProvider';
import { useThemeContext } from '../../providers/ThemeProvider';

const Container = styled.View`
flex: 1;
Expand Down Expand Up @@ -54,7 +54,8 @@ interface Props {

function Intro(props: Props): React.ReactElement {
let timer: number;
const { state, dispatch } = React.useContext(AppContext);
const { state, setUser } = useAppContext();
const { changeThemeType } = useThemeContext();
const [isLoggingIn, setIsLoggingIn] = React.useState<boolean>(false);

const onLogin = (): void => {
Expand All @@ -65,29 +66,13 @@ function Intro(props: Props): React.ReactElement {
age: 30,
job: 'developer',
};
dispatch({ type: 'set-user', payload: { user: user } });
// dispatch({ type: 'set-user', payload: { user: user } });
setUser(user);
setIsLoggingIn(false);
clearTimeout(timer);
}, 1000);
};

const changeTheme = (): void => {
let payload: object;
if (state.theme === ThemeType.LIGHT) {
payload = {
theme: ThemeType.DARK,
};
} else {
payload = {
theme: ThemeType.LIGHT,
};
}
dispatch({
type: 'change-theme-mode',
payload,
});
};

return (
<Container>
<ContentWrapper>
Expand Down Expand Up @@ -118,7 +103,7 @@ function Intro(props: Props): React.ReactElement {
<View style={{ marginTop: 8 }} />
<Button
testID='btn3'
onClick={(): void => changeTheme()}
onClick={(): void => changeThemeType()}
text={getString('CHANGE_THEME')}
/>
</ButtonWrapper>
Expand Down
51 changes: 19 additions & 32 deletions src/components/screen/__tests__/Intro.test.tsx
@@ -1,5 +1,3 @@
import 'react-native';

import * as React from 'react';

import {
Expand All @@ -8,15 +6,14 @@ import {
fireEvent,
render,
} from '@testing-library/react-native';
import { ThemeType, createTheme } from '../../../theme';

import { AppProvider } from '../../../providers';
import AllProviders from '../../../providers';
import Button from '../../shared/Button';
import Intro from '../Intro';
// Note: test renderer must be required after react-native.
import { ThemeProvider } from 'styled-components/native';
import renderer from 'react-test-renderer';

// Note: test renderer must be required after react-native.

const createTestProps = (obj: object): object => ({
navigation: {
navigate: jest.fn(),
Expand All @@ -27,45 +24,38 @@ const createTestProps = (obj: object): object => ({
// `any` here is necessary for test, so turn off eslint rule for this line
const props: any = createTestProps({}); // eslint-disable-line @typescript-eslint/no-explicit-any

// test for the container page in dom
describe('[Intro] screen rendering test', () => {
const component = (
<AppProvider>
<ThemeProvider theme={createTheme(ThemeType.LIGHT)}>
<Intro {...props} />
</ThemeProvider>
</AppProvider>
const component = (props): React.ReactElement => {
return (
<AllProviders isTest>
<Intro {...props} />
</AllProviders>
);
let json: renderer.ReactTestRendererJSON;
};

describe('[Intro] screen rendering test', () => {
it('should render outer component and snapshot matches', () => {
json = renderer.create(component).toJSON();
const json = renderer.create(component(props)).toJSON();
expect(json).toMatchSnapshot();
expect(json).toBeTruthy();
});
});

describe('[Intro] Interaction', () => {
const component = (
<AppProvider>
<ThemeProvider theme={createTheme(ThemeType.LIGHT)}>
<Intro {...props} />
</ThemeProvider>
</AppProvider>
);

let testingLib: RenderResult;
let rendered: renderer.ReactTestRenderer;
let root: renderer.ReactTestInstance;
let testingLib: RenderResult;

it('should simulate [onLogin] click', () => {
rendered = renderer.create(component);
testingLib = render(component(props));
rendered = renderer.create(component(props));
root = rendered.root;
testingLib = render(component);

jest.useFakeTimers();
const buttons = root.findAllByType(Button);

const button = testingLib.getByTestId('btn1');
act(() => {
fireEvent.press(testingLib.queryByTestId('btn1'));
fireEvent.press(button);
expect(setTimeout).toHaveBeenCalledTimes(1);
jest.runAllTimers();
});
Expand All @@ -75,11 +65,8 @@ describe('[Intro] Interaction', () => {
});

it('should simulate [navigate] click', () => {
rendered = renderer.create(component);
root = rendered.root;
testingLib = render(component(props));

// const buttons = root.findAllByType(Button);
// buttons[1].props.onClick();
act(() => {
fireEvent.press(testingLib.getByTestId('btn2'), 'click');
});
Expand Down
9 changes: 4 additions & 5 deletions src/components/screen/__tests__/Temp.test.tsx
Expand Up @@ -8,10 +8,9 @@ import {
fireEvent,
render,
} from '@testing-library/react-native';
import { ThemeType, createTheme } from '../../../theme';

import AllProviders from '../../../providers';
import Temp from '../Temp';
import { ThemeProvider } from 'styled-components/native';
import renderer from 'react-test-renderer';

const props = {
Expand All @@ -21,9 +20,9 @@ const props = {
};

const component = (
<ThemeProvider theme={createTheme(ThemeType.LIGHT)}>
<Temp {...props} />
</ThemeProvider>
<AllProviders isTest>
<Temp {...(props as any)} />
</AllProviders>
);

describe('[Temp] render', () => {
Expand Down
3 changes: 0 additions & 3 deletions src/contexts/index.ts

This file was deleted.

0 comments on commit 30ed035

Please sign in to comment.