├── .browserslistrc
├── .eslintrc
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.test.js
├── App.tsx
├── api
│ └── index.ts
├── components
│ └── navigation
│ │ └── index.tsx
├── containers
│ ├── counter-container.tsx
│ ├── my-github-container.tsx
│ └── todo-container.tsx
├── index.tsx
├── modules
│ ├── counter.ts
│ ├── index.tsx
│ ├── my-github.ts
│ └── todo.ts
├── react-app-env.d.ts
├── routes
│ ├── contents
│ │ └── index.tsx
│ ├── counter
│ │ └── index.tsx
│ ├── error
│ │ └── index.tsx
│ ├── home
│ │ └── index.tsx
│ ├── index.ts
│ ├── my-github
│ │ └── index.tsx
│ └── todo
│ │ └── index.tsx
└── utils
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | [production staging]
2 | >0.2%
3 | not dead
4 | not op_mini all
5 |
6 | [development]
7 | last 1 chrome version
8 | last 1 firefox version
9 | last 1 safari version
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "prettier"],
3 | "plugins": ["react-hooks"],
4 | "rules": {
5 | "react-hooks/rules-of-hooks": "error"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "useTabs": false,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "printWidth": 100
8 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "files.autoSave": "onFocusChange",
4 | "eslint.autoFixOnSave": true,
5 | "editor.formatOnType": true
6 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-plate
2 |
3 | Do we still need a redux?
4 |
5 | I think, we can manage state of application with [Hooks API](https://reactjs.org/docs/hooks-intro.html) and [Context API](https://reactjs.org/docs/context.html) of React.
6 |
7 | ## 🔫 Killing Point
8 |
9 | ```ts
10 | const GlobalContext = createContext(defaultValue)
11 |
12 | export const GlobalProvider = ({ children }: { children: React.ReactNode }) => {
13 | const [state, dispatch] = useReducer(reducer, globalState)
14 | const value = useMemo(() => [state, dispatch], [state])
15 |
16 | return {children}
17 | }
18 | ```
19 |
20 | > 💡 If you don't like TypeScript, you can see the [VanillaJS version](https://github.com/JaeYeopHan/react-plate/tree/javascript-version)
21 |
22 | ## ⛑ Structure
23 |
24 | ```
25 | src
26 | ├── api
27 | ├── components
28 | ├── containers
29 | ├── modules
30 | ├── routes
31 | ├── utils
32 | ├── App.tsx
33 | └── index.tsx
34 | ```
35 |
36 | ## 📦 Packages
37 |
38 | ### Main
39 |
40 | - [create-react-app](https://github.com/facebook/create-react-app)
41 | - [react 16.8.x](https://github.com/facebook/react)
42 | - [react-router 5.x](https://github.com/ReactTraining/react-router)
43 | - [TypeScript 3.5.x](https://github.com/microsoft/TypeScript)
44 |
45 | ### etc
46 |
47 | - [loadable-components](https://github.com/smooth-code/loadable-components)
48 |
49 | ### Development Setting
50 |
51 | - [eslint](https://github.com/eslint/eslint)
52 | - [prettier](https://github.com/prettier/prettier)
53 | - [husky](https://github.com/typicode/husky)
54 | - [lint-staged](https://github.com/okonet/lint-staged)
55 | - [browserslist](https://github.com/browserslist/browserslist)
56 |
57 | > [More Details](./pacakge.json)
58 |
59 | ## ✅ TODO
60 |
61 | - [ ] [react-helmet](https://github.com/nfl/react-helmet)
62 | - [ ] [react-i18next](https://github.com/i18next/react-i18next)
63 | - [ ] [react-testing-library](https://github.com/testing-library/react-testing-library)
64 | - [ ] [react-snap](https://github.com/stereobooster/react-snap)
65 |
66 |
67 |
68 |
Project by @Jbee✌
69 |
70 |
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-plate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@loadable/component": "^5.10.1",
7 | "@types/jest": "^24.0.13",
8 | "@types/node": "^12.0.8",
9 | "@types/react": "^16.8.19",
10 | "@types/react-dom": "^16.8.4",
11 | "react": "^16.8.6",
12 | "react-dom": "^16.8.6",
13 | "react-router-dom": "^5.0.1",
14 | "react-scripts": "3.0.1",
15 | "typescript": "^3.5.1"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "lint": "eslint src/**/*.js src/**/*.jsx",
22 | "eject": "react-scripts eject"
23 | },
24 | "devDependencies": {
25 | "@types/loadable__component": "^5.9.0",
26 | "@types/react-router-dom": "^4.3.3",
27 | "eslint-config-prettier": "^4.3.0",
28 | "eslint-plugin-react-hooks": "^1.6.0",
29 | "husky": "^2.4.0",
30 | "lint-staged": "^8.2.0"
31 | },
32 | "husky": {
33 | "hooks": {
34 | "pre-commit": "lint-staged"
35 | }
36 | },
37 | "lint-staged": {
38 | "*.{ts,tsx}": "eslint"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/react-plate/2605b5aa6cc7ef4cf65d7e358d31e99aec422869/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
26 | React App
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div')
7 | ReactDOM.render(, div)
8 | ReactDOM.unmountComponentAtNode(div)
9 | })
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter, Route, Switch } from 'react-router-dom'
3 | import Navigation from './components/navigation'
4 | import { routeConfigs } from './routes'
5 | import { GlobalProvider } from './modules'
6 |
7 | function App() {
8 | return (
9 |
10 |
13 |
14 |
15 |
16 |
17 | {routeConfigs.map((config, index) => (
18 |
19 | ))}
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default App
28 |
--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import { getMockData } from '../utils'
2 |
3 | const phase = process.env.NODE_ENV
4 |
5 | export async function getMyGitHubProfile() {
6 | let result
7 |
8 | if (phase === 'development') {
9 | result = await getMockData()
10 | } else {
11 | const response = await fetch('https://api.github.com/users/JaeYeopHan')
12 | result = await response.json()
13 | }
14 |
15 | return result
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { PAGES } from '../../routes'
4 |
5 | const Navigation = () => {
6 | return (
7 |
24 | )
25 | }
26 |
27 | export default Navigation
28 |
--------------------------------------------------------------------------------
/src/containers/counter-container.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useStore } from '../modules'
3 | import * as counter from '../modules/counter'
4 |
5 | const CounterContainer = () => {
6 | const [state, dispatch] = useStore(counter.NAMESPACE)
7 |
8 | const handleClickUp = () => dispatch(counter.increase())
9 | const handleClickDown = () => dispatch(counter.decrease())
10 |
11 | return (
12 |
13 |
Counter Example
14 |
{state.count}
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default CounterContainer
22 |
--------------------------------------------------------------------------------
/src/containers/my-github-container.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useStore } from '../modules'
3 | import * as myGitHub from '../modules/my-github'
4 | import { getMyGitHubProfile } from '../api'
5 |
6 | const MyGitHubContainer = () => {
7 | const [state, dispatch] = useStore(myGitHub.NAMESPACE)
8 | const { isLoading, contents } = state
9 |
10 | useEffect(() => {
11 | const fetchData = async () => {
12 | dispatch(myGitHub.fetch())
13 |
14 | try {
15 | const response = await getMyGitHubProfile()
16 | dispatch(myGitHub.fetchSuccess(response))
17 | } catch (error) {
18 | dispatch(myGitHub.fetchFail())
19 | }
20 | }
21 |
22 | fetchData()
23 | }, [dispatch])
24 |
25 | if (isLoading) {
26 | return Loading...
27 | }
28 | return (
29 |
30 |
31 | NAME:
32 | {contents.login}
33 |
34 |
35 | URL:
36 | {contents.html_url}
37 |
38 |
39 | BLOG:
40 | {contents.blog}
41 |
42 |

43 |
44 | )
45 | }
46 |
47 | export default MyGitHubContainer
48 |
--------------------------------------------------------------------------------
/src/containers/todo-container.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useStore } from '../modules'
3 | import * as todo from '../modules/todo'
4 | import { Item } from '../modules/todo'
5 |
6 | const TodoContainer = () => {
7 | const [input, setInput] = useState('')
8 | const [state, dispatch] = useStore(todo.NAMESPACE)
9 |
10 | const handleKeyPress = ({ which }: React.KeyboardEvent) => {
11 | if (which === 13) {
12 | dispatch(todo.add(input))
13 | setInput('')
14 | }
15 | }
16 | const handleChangeInput = ({ target }: React.ChangeEvent) => {
17 | setInput(target.value)
18 | }
19 |
20 | return (
21 |
33 | )
34 | }
35 |
36 | export default TodoContainer
37 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render(, document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/src/modules/counter.ts:
--------------------------------------------------------------------------------
1 | // State
2 | export interface ICounterState {
3 | count: number
4 | }
5 |
6 | export const counter = {
7 | count: 0,
8 | }
9 |
10 | export const NAMESPACE = 'counter'
11 |
12 | // Action (NAMESPACE/ACTION_NAME)
13 | const INCREASE = `${NAMESPACE}/INCREASE`
14 | const DECREASE = `${NAMESPACE}/DECREASE`
15 |
16 | // Action Creator
17 | interface IncreaseActionType {
18 | type: typeof INCREASE
19 | }
20 |
21 | interface DecreaseActionType {
22 | type: typeof DECREASE
23 | }
24 |
25 | export type CounterActionType = IncreaseActionType | DecreaseActionType
26 |
27 | export const increase = (): CounterActionType => ({
28 | type: INCREASE,
29 | })
30 |
31 | export const decrease = (): CounterActionType => ({
32 | type: DECREASE,
33 | })
34 |
35 | // Reducer
36 | export default function(state: ICounterState, action: CounterActionType): ICounterState {
37 | const { count } = state
38 |
39 | switch (action.type) {
40 | case INCREASE:
41 | return {
42 | ...state,
43 | count: count + 1,
44 | }
45 | case DECREASE:
46 | return {
47 | ...state,
48 | count: count - 1,
49 | }
50 | default:
51 | return state
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/modules/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useReducer, useMemo } from 'react'
2 | import counterReducer, { counter, ICounterState } from './counter'
3 | import todoReducer, { todo, ITodoState } from './todo'
4 | import myGitHubReducer, { myGitHub, IMyGitHubState } from './my-github'
5 | import { info } from '../utils'
6 |
7 | interface IGlobalState {
8 | counter: ICounterState
9 | todo: ITodoState
10 | myGitHub: IMyGitHubState
11 | }
12 |
13 | // Combine state
14 | const globalState: IGlobalState = {
15 | counter,
16 | todo,
17 | myGitHub,
18 | }
19 |
20 | // Combine reducer
21 | const reducer = ({ counter, todo, myGitHub }: IGlobalState, action: any) => {
22 | info(action)
23 | return {
24 | counter: counterReducer(counter, action),
25 | todo: todoReducer(todo, action),
26 | myGitHub: myGitHubReducer(myGitHub, action),
27 | }
28 | }
29 |
30 | const defaultValue: any[] | never[] = []
31 | const GlobalContext = createContext(defaultValue)
32 |
33 | export const GlobalProvider = ({ children }: { children: React.ReactNode }) => {
34 | const [state, dispatch] = useReducer(reducer, globalState)
35 | const value = useMemo(() => [state, dispatch], [state])
36 |
37 | return {children}
38 | }
39 |
40 | export const useStore = (target: string) => {
41 | const [globalState, dispatch] = useContext(GlobalContext)
42 |
43 | if (!globalState[target]) {
44 | throw Error('Not found store module')
45 | }
46 |
47 | return [globalState[target], dispatch]
48 | }
49 |
--------------------------------------------------------------------------------
/src/modules/my-github.ts:
--------------------------------------------------------------------------------
1 | // state
2 | export interface IMyGitHubState {
3 | isLoading: boolean
4 | contents: any
5 | errorMessage: string
6 | }
7 |
8 | export const myGitHub = {
9 | isLoading: true,
10 | contents: 'contents',
11 | errorMessage: '',
12 | }
13 |
14 | export const NAMESPACE = 'myGitHub'
15 |
16 | // Action (NAMESPACE/ACTION_NAME)
17 | const FETCH = `${NAMESPACE}/FETCH`
18 | const FETCH_SUCCESS = `${NAMESPACE}/FETCH_SUCCESS`
19 | const FETCH_FAIL = `${NAMESPACE}/FETCH_FAIL`
20 |
21 | // Action Creator
22 | interface FetchActionType {
23 | type: typeof FETCH
24 | }
25 |
26 | interface FetchSuccessActionType {
27 | type: typeof FETCH_SUCCESS
28 | payload: any
29 | }
30 |
31 | interface FetchFailActionType {
32 | type: typeof FETCH_FAIL
33 | }
34 |
35 | export type MyGitHubActionType = FetchActionType | FetchFailActionType | FetchSuccessActionType
36 |
37 | export const fetch = (): MyGitHubActionType => ({
38 | type: FETCH,
39 | })
40 |
41 | export const fetchSuccess = (payload: any): MyGitHubActionType => ({
42 | type: FETCH_SUCCESS,
43 | payload,
44 | })
45 |
46 | export const fetchFail = (): MyGitHubActionType => ({
47 | type: FETCH_FAIL,
48 | })
49 |
50 | export default function(state: IMyGitHubState, action: MyGitHubActionType): IMyGitHubState {
51 | switch (action.type) {
52 | case FETCH:
53 | return {
54 | ...state,
55 | isLoading: true,
56 | }
57 | case FETCH_SUCCESS:
58 | return {
59 | ...state,
60 | isLoading: false,
61 | contents: (action as FetchSuccessActionType).payload,
62 | }
63 | case FETCH_FAIL:
64 | return {
65 | ...state,
66 | isLoading: false,
67 | contents: 'error',
68 | }
69 | default:
70 | return state
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/modules/todo.ts:
--------------------------------------------------------------------------------
1 | // State
2 | export interface Item {
3 | name: string
4 | isDone: boolean
5 | }
6 |
7 | export interface ITodoState {
8 | items: Item[]
9 | }
10 |
11 | export const todo = {
12 | items: [{ name: 'A', isDone: false }, { name: 'B', isDone: false }, { name: 'C', isDone: false }],
13 | }
14 |
15 | export const NAMESPACE = 'todo'
16 |
17 | // Action (NAMESPACE/ACTION_NAME)
18 | const ADD = `${NAMESPACE}/ADD`
19 |
20 | // Action Creator
21 | interface AddActionType {
22 | type: typeof ADD
23 | item: Item
24 | }
25 |
26 | export type TodoActionTypes = AddActionType
27 |
28 | export const add = (name: string): TodoActionTypes => ({
29 | type: ADD,
30 | item: { name, isDone: false },
31 | })
32 |
33 | export default function(state: ITodoState, action: TodoActionTypes): ITodoState {
34 | const { items } = state
35 |
36 | switch (action.type) {
37 | case ADD:
38 | const newItems = items.concat(action.item)
39 | return {
40 | ...state,
41 | items: newItems,
42 | }
43 | default:
44 | return state
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/routes/contents/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ContentsSection = () => {
4 | return contents section
5 | }
6 |
7 | export default ContentsSection
8 |
--------------------------------------------------------------------------------
/src/routes/counter/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CounterContainer from '../../containers/counter-container'
3 |
4 | const Counter = () => {
5 | return
6 | }
7 |
8 | export default Counter
9 |
--------------------------------------------------------------------------------
/src/routes/error/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ErrorSection = () => {
4 | return error section
5 | }
6 |
7 | export default ErrorSection
8 |
--------------------------------------------------------------------------------
/src/routes/home/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const HomeSection = () => {
4 | return (
5 | <>
6 | home section
7 | Welcome React Architecture Lab!
8 | I'm finding Best Practice of React.
9 | >
10 | )
11 | }
12 |
13 | export default HomeSection
14 |
--------------------------------------------------------------------------------
/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import loadable from '@loadable/component'
2 |
3 | export enum PAGES {
4 | HOME = '/',
5 | CONTENTS = '/contents',
6 | COUNTER = '/counter',
7 | TODO = '/todo',
8 | MY_GITHUB = '/my-github',
9 | ERROR = '',
10 | }
11 |
12 | export const routeConfigs = [
13 | getRouteConfig(PAGES.MY_GITHUB, './my-github'),
14 | getRouteConfig(PAGES.TODO, './todo'),
15 | getRouteConfig(PAGES.COUNTER, './counter'),
16 | getRouteConfig(PAGES.CONTENTS, './contents'),
17 | getRouteConfig(PAGES.HOME, './home'),
18 | getRouteConfig(PAGES.ERROR, './error'),
19 | ]
20 |
21 | function getRouteConfig(path: PAGES, componentPath: string) {
22 | return {
23 | path,
24 | component: loadable(() => import(/* webpackChunkName: "contents" */ `${componentPath}`)),
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/routes/my-github/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MyGitHubContainer from '../../containers/my-github-container'
3 |
4 | const MyGitHub = () => {
5 | return
6 | }
7 |
8 | export default MyGitHub
9 |
--------------------------------------------------------------------------------
/src/routes/todo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TodoContainer from '../../containers/todo-container'
3 |
4 | const Todo = () => {
5 | return
6 | }
7 |
8 | export default Todo
9 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function delay(timeout: number = 1000) {
2 | return new Promise((resolve, reject) => {
3 | setTimeout(() => {
4 | resolve(true)
5 | }, timeout)
6 | })
7 | }
8 |
9 | export function info(action: any) {
10 | console.log(action)
11 | }
12 |
13 | export function getMockData() {
14 | return new Promise((resolve, reject) => {
15 | setTimeout(() => {
16 | resolve({
17 | login: 'JaeYeopHan',
18 | html_url: 'github.com/JaeYeopHan',
19 | blog: 'jbee.io',
20 | avartar_url: '',
21 | })
22 | }, 3000)
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "preserve"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------