├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── README.md ├── assets └── screen-shot.png ├── dev-machine-setup.md ├── package.json ├── public ├── env.js ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.tsx ├── components │ ├── Header │ │ ├── BackButton.tsx │ │ ├── ExamplesButton.tsx │ │ ├── Header.tsx │ │ ├── HomeButton.tsx │ │ ├── ToggleTheme.tsx │ │ └── index.ts │ └── index.ts ├── contexts │ ├── RootStoreContext.ts │ └── index.ts ├── features │ ├── Examples │ │ ├── ExamplesPage.tsx │ │ ├── ProductEditor │ │ │ ├── ProductContext.tsx │ │ │ ├── ProductDetail.tsx │ │ │ ├── ProductEditor.tsx │ │ │ ├── ProductForm.tsx │ │ │ ├── ProductMaster.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── Home │ │ ├── HomePage.tsx │ │ ├── PersonList │ │ │ ├── PersonList.tsx │ │ │ ├── index.ts │ │ │ └── usePeople.ts │ │ └── index.ts │ └── index.ts ├── index.tsx ├── init.ts ├── models │ ├── Person.ts │ └── index.ts ├── react-app-env.d.ts ├── stores │ ├── LsKeys.ts │ ├── PrefStore.ts │ ├── RootStore.ts │ ├── index.ts │ └── routes.ts ├── utils │ ├── constants.ts │ ├── index.ts │ └── math-utils.ts └── viewMap.tsx ├── tsconfig.json └── yarn.lock /.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 | # IDEs and editors 15 | /.idea 16 | /.vscode 17 | 18 | # misc 19 | .DS_Store 20 | .eslintcache 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | singleQuote: true, 4 | proseWrap: 'always' 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Force Template 2 | 3 | This template is intended to be a starting point for serious React applications. 4 | It offers an opinionated directory structure and demonstrates best practices in 5 | layout, error handling, hooks and more. 6 | 7 | ![Screen Shot](assets/screen-shot.png) 8 | 9 | Check out the following repos for more complex examples using this template: 10 | 11 | - [Stock Charts](https://github.com/nareshbhatia/stock-charts) 12 | - [Manage My Money](https://github.com/nareshbhatia/manage-my-money) 13 | - [GraphQL Bookstore](https://github.com/nareshbhatia/graphql-bookstore) 14 | 15 | ## Getting Started 16 | 17 | Make sure your development machine is set up for building React apps. See the 18 | recommended setup procedure [here](./dev-machine-setup.md). 19 | 20 | Now execute the following commands to build and run the template. 21 | 22 | ```bash 23 | $ yarn 24 | $ yarn start 25 | ``` 26 | 27 | Now point your browser to http://localhost:3000/. 28 | 29 | ## Folder Structure 30 | 31 | This example follows best practices adopted by popular React projects including 32 | Material-UI. 33 | 34 | ``` 35 | /src 36 | /components 37 | /contexts 38 | /features 39 | /models 40 | /services 41 | /utils 42 | ``` 43 | 44 | - `components:` This is where we keep components that are reused across 45 | application features. Components are arranged in sub-folders - one or more 46 | tightly related components per sub-folder. 47 | 48 | - `contexts:` This folder contains the React contexts we need for our app. 49 | 50 | - `features:` Contains domain-specific features of the app - one folder per 51 | feature. Feature is an abstract concept which could map to a reasonable 52 | level of granularity for your application. For example, feature could map to 53 | a page, a group of pages or a tab on your app. The feature folder can hold 54 | multiple components that are tightly related to the feature and are not 55 | reusable across features. Keep the folder structure flat as long as 56 | possible. Consider creating sub-folders when the folder reaches seven or 57 | more files. 58 | 59 | - `models`: This is where we keep the application's domain entities, value 60 | objects, interfaces and types. Please refer to 61 | [Domain-Driven Design](https://archfirst.org/domain-driven-design/) to 62 | understand the meaning of these terms. 63 | 64 | - `services:` Contains functionality to access the outside world. This could 65 | be services for accessing REST or GraphQL APIs, listeners for WebSockets, 66 | access to data stores etc. This layer is also known as the `adapter` layer 67 | in the 68 | [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture) 69 | or the 70 | [Onion Architecture](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/). 71 | 72 | - `utils:` - contains general purpose utilities such as date/time utilities 73 | and number parsing & formatting utilities. 74 | 75 | When reviewing the above folder structure, note how we control the items exposed 76 | by a folder. Each folder has an `index.ts` file which exports only what is 77 | needed by external consumers - nothing more, nothing less! If everything needs 78 | to be exported, we simply use `export * from './xyz';` - instead of repeating 79 | each item individually. This makes it easier to manage the index files. 80 | -------------------------------------------------------------------------------- /assets/screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nareshbhatia/react-force-template/dba600d6ac444ef92ae17b7df556daa0083d3839/assets/screen-shot.png -------------------------------------------------------------------------------- /dev-machine-setup.md: -------------------------------------------------------------------------------- 1 | # Development Machine Setup 2 | 3 | The instructions below are specific to MacOS. If you are on a Windows machine, 4 | some steps will be different. 5 | 6 | ### Install Homebrew & Required Packages 7 | 8 | ```bash 9 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 10 | brew install git wget 11 | ``` 12 | 13 | ### Install Z Shell 14 | 15 | Starting MacOS 10.15 (Catalina), the default shell is zsh. If your default shell 16 | is not zsh, I highly recommend changing it to zsh. 17 | 18 | Execute the following command in your terminal window to find out your default 19 | shell: 20 | 21 | ```bash 22 | echo $SHELL 23 | ``` 24 | 25 | If you see something other than `/bin/zsh`, then zsh is not your default shell. 26 | Change it using the following command: 27 | 28 | ```bash 29 | chsh -s /bin/zsh 30 | ``` 31 | 32 | Now close the terminal and reopen it. Type `echo $SHELL` to make sure that zsh 33 | is the default. 34 | 35 | ### Install Oh My Zsh 36 | 37 | Oh My Zsh is a delightful framework for managing your Zsh configuration. It 38 | comes bundled with thousands of helpful functions, helpers, plugins and themes. 39 | Enter the following command in your shell to install Oh My Zsh: 40 | 41 | ```bash 42 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 43 | ``` 44 | 45 | The installation script creates a backup of your .zshrc file, and then replaces 46 | it with its own version. If you had any important configuration in your original 47 | .zshrc, copy it over to your new .zshrc. 48 | 49 | ### Install Node Version Manager & Node 50 | 51 | Node Version Manager (nvm) is a bash script to manage multiple node.js versions. 52 | It is better than using the Node.js installer or Homebrew (see 53 | [this article](https://pawelgrzybek.com/install-nodejs-installer-vs-homebrew-vs-nvm/)). 54 | 55 | ```bash 56 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash 57 | ``` 58 | 59 | Now install the latest LTS version of Node.js 60 | 61 | ```bash 62 | nvm install 12.18.2 63 | source "$HOME/.zshrc" 64 | node -v # should print v12.18.2 65 | ``` 66 | 67 | If you get the error `nvm: command not found`, then follow the instructions 68 | under 69 | [Troubleshooting on macOS](https://github.com/nvm-sh/nvm#troubleshooting-on-macos). 70 | 71 | Now install Yarn. 72 | 73 | ```bash 74 | curl -o- -L https://yarnpkg.com/install.sh | bash 75 | source "$HOME/.zshrc" 76 | yarn -v # should print a version number like v1.22.4 77 | ``` 78 | 79 | Note: Do note use Homebrew to install Yarn because we did not use it to install 80 | node. See [this issue](https://github.com/yarnpkg/website/issues/913). 81 | 82 | ### Verify ~/.zshrc 83 | 84 | At this point, the end of your .zshrc file should look something like this. Make 85 | sure that each section is in the correct order. 86 | 87 | ```bash 88 | # Node Version Manager (NVM) 89 | export NVM_DIR="$HOME/.nvm" 90 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 91 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion 92 | 93 | # Yarn 94 | export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" 95 | ``` 96 | 97 | ### Try building a React app 98 | 99 | Clone the following repo wherever you keep projects and verify that you can 100 | build and run a React app. I recommend keeping all your projects under 101 | ~/projects. 102 | 103 | ```bash 104 | cd ~/projects 105 | git clone https://github.com/nareshbhatia/react-force-template.git 106 | cd react-force-template 107 | yarn 108 | yarn start 109 | ``` 110 | 111 | Congratulations! You machine is now certified to build React apps! 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-force-template", 3 | "description": "Template intended to be a starting point for serious React applications", 4 | "version": "0.1.0", 5 | "author": "Naresh Bhatia", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/nareshbhatia/react-force-template.git" 10 | }, 11 | "scripts": { 12 | "analyze": "source-map-explorer 'build/static/js/*.js'", 13 | "build": "react-scripts build", 14 | "eject": "react-scripts eject", 15 | "format": "prettier --write README.md 'src/**/{*.md,*.json,*.ts*}'", 16 | "start": "react-scripts start", 17 | "test": "react-scripts test", 18 | "test:coverage": "react-scripts test --coverage --watchAll=false" 19 | }, 20 | "dependencies": { 21 | "@material-ui/core": "^4.11.3", 22 | "@material-ui/icons": "^4.11.2", 23 | "@material-ui/lab": "4.0.0-alpha.57", 24 | "@react-force/core": "^3.3.1", 25 | "@react-force/date-utils": "^3.4.0", 26 | "@react-force/formik-mui": "^3.1.0", 27 | "@react-force/http-utils": "^1.2.0", 28 | "@react-force/mock-data": "^1.1.0", 29 | "@react-force/models": "^1.2.0", 30 | "@react-force/number-utils": "^2.1.0", 31 | "@react-force/utils": "^2.3.0", 32 | "@react-force/web-utils": "^2.1.0", 33 | "axios": "^0.21.1", 34 | "classnames": "^2.2.6", 35 | "mobx": "^5.15.7", 36 | "mobx-react": "^6.3.1", 37 | "mobx-state-router": "^5.2.0", 38 | "react": "^17.0.1", 39 | "react-dom": "^17.0.1", 40 | "react-query": "^2.23.1", 41 | "react-query-devtools": "^2.6.3", 42 | "uuid": "^8.3.2", 43 | "yup": "^0.32.8" 44 | }, 45 | "devDependencies": { 46 | "@testing-library/jest-dom": "^5.11.9", 47 | "@testing-library/react": "^11.2.3", 48 | "@testing-library/user-event": "^12.6.2", 49 | "@types/classnames": "^2.2.11", 50 | "@types/jest": "26.0.20", 51 | "@types/node": "14.14.22", 52 | "@types/react": "17.0.0", 53 | "@types/react-dom": "17.0.0", 54 | "@types/yup": "^0.29.11", 55 | "husky": "^4.3.8", 56 | "prettier": "^2.2.1", 57 | "pretty-quick": "^3.1.0", 58 | "react-scripts": "4.0.1", 59 | "source-map-explorer": "^2.5.2", 60 | "typescript": "4.1.3" 61 | }, 62 | "eslintConfig": { 63 | "extends": "react-app" 64 | }, 65 | "husky": { 66 | "hooks": { 67 | "pre-commit": "pretty-quick --staged" 68 | } 69 | }, 70 | "browserslist": { 71 | "production": [ 72 | ">0.2%", 73 | "not dead", 74 | "not op_mini all" 75 | ], 76 | "development": [ 77 | "last 1 chrome version", 78 | "last 1 firefox version", 79 | "last 1 safari version" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /public/env.js: -------------------------------------------------------------------------------- 1 | window._env_ = { 2 | API_URL: 'https://jsonplaceholder.typicode.com', 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nareshbhatia/react-force-template/dba600d6ac444ef92ae17b7df556daa0083d3839/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | 24 | React Force Template 25 | 26 | 29 | 30 | 31 | 34 | 38 | 42 | 43 | 44 | 45 |
46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Force Template", 3 | "name": "React Force Template", 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.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import CssBaseline from '@material-ui/core/CssBaseline'; 3 | import { ThemeProvider } from '@material-ui/core/styles'; 4 | import { 5 | ErrorBoundary, 6 | Loading, 7 | MessageProvider, 8 | MessageRenderer, 9 | } from '@react-force/core'; 10 | import { observer } from 'mobx-react'; 11 | import { RouterContext, RouterView } from 'mobx-state-router'; 12 | // import { ReactQueryDevtools } from 'react-query-devtools'; 13 | import { RootStoreContext } from './contexts'; 14 | import { initApp } from './init'; 15 | import { viewMap } from './viewMap'; 16 | 17 | // Initialize the app 18 | const rootStore = initApp(); 19 | const { prefStore, routerStore } = rootStore; 20 | 21 | // Observer theme changes 22 | export const App = observer(() => { 23 | return ( 24 | 25 | }> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {/* */} 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/Header/BackButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import ArrowBack from '@material-ui/icons/ArrowBack'; 4 | import { browserHistory } from 'mobx-state-router'; 5 | 6 | export const BackButton = () => { 7 | return ( 8 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Header/ExamplesButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import { useRouterStore } from 'mobx-state-router'; 4 | 5 | export const ExamplesButton = () => { 6 | const routerStore = useRouterStore(); 7 | 8 | const handleClick = () => { 9 | routerStore.goTo('examples', { 10 | params: { 11 | navItemId: 'master-detail', 12 | }, 13 | }); 14 | }; 15 | 16 | return ( 17 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header as BaseHeader, HeaderTitle } from '@react-force/core'; 3 | import { BackButton } from './BackButton'; 4 | import { ExamplesButton } from './ExamplesButton'; 5 | import { ToggleTheme } from './ToggleTheme'; 6 | import { HomeButton } from './HomeButton'; 7 | 8 | export enum NavButtonEnum { 9 | None, 10 | Home, 11 | Back, 12 | } 13 | 14 | export interface HeaderProps { 15 | navButtonEnum?: NavButtonEnum; 16 | title?: string; 17 | } 18 | 19 | export const Header = ({ 20 | navButtonEnum = NavButtonEnum.Home, 21 | title = 'React Force Template', 22 | }: HeaderProps) => { 23 | return ( 24 | 25 | {navButtonEnum === NavButtonEnum.Home && } 26 | {navButtonEnum === NavButtonEnum.Back && } 27 | {title} 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/Header/HomeButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import Apps from '@material-ui/icons/Apps'; 4 | import { useRouterStore } from 'mobx-state-router'; 5 | 6 | export const HomeButton = () => { 7 | const routerStore = useRouterStore(); 8 | 9 | const handleClick = () => { 10 | routerStore.goTo('home'); 11 | }; 12 | 13 | return ( 14 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Header/ToggleTheme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import DarkIcon from '@material-ui/icons/Brightness2'; 4 | import LightIcon from '@material-ui/icons/Brightness2Outlined'; 5 | import { observer } from 'mobx-react'; 6 | import { useRootStore } from '../../contexts'; 7 | 8 | // Observe paletteType changes 9 | export const ToggleTheme = observer(() => { 10 | const rootStore = useRootStore(); 11 | const { prefStore } = rootStore; 12 | const { paletteType } = prefStore; 13 | 14 | const handleToggleTheme = () => { 15 | prefStore.toggleTheme(); 16 | }; 17 | 18 | return ( 19 | 24 | {paletteType === 'light' ? : } 25 | 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /src/contexts/RootStoreContext.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { RootStore } from '../stores'; 3 | 4 | // ---------- RootStoreContext ---------- 5 | export const RootStoreContext = React.createContext( 6 | undefined 7 | ); 8 | 9 | // ---------- useRootStore ---------- 10 | export function useRootStore(): RootStore { 11 | const rootStore = useContext(RootStoreContext); 12 | if (rootStore === undefined) { 13 | /* istanbul ignore next */ 14 | throw new Error('useRootStore must be used within a RootStoreProvider'); 15 | } 16 | return rootStore; 17 | } 18 | -------------------------------------------------------------------------------- /src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RootStoreContext'; 2 | -------------------------------------------------------------------------------- /src/features/Examples/ExamplesPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import MasterDetailIcon from '@material-ui/icons/List'; 3 | import { observer } from 'mobx-react'; 4 | import { useRouterStore } from 'mobx-state-router'; 5 | import { 6 | HorizontalContainer, 7 | NavComponent, 8 | SideBar, 9 | ViewVerticalContainer, 10 | } from '@react-force/core'; 11 | import { Header } from '../../components'; 12 | import { ProductEditor } from './ProductEditor'; 13 | 14 | const navComponents: Array = [ 15 | { 16 | type: 'group', 17 | items: [ 18 | { 19 | id: 'master-detail', 20 | title: 'Master-Detail', 21 | icon: , 22 | }, 23 | ], 24 | }, 25 | ]; 26 | 27 | const navItemMap: { [navItemId: string]: ReactNode } = { 28 | 'master-detail': , 29 | }; 30 | 31 | // Observe routerState 32 | export const ExamplesPage = observer(() => { 33 | const routerStore = useRouterStore(); 34 | 35 | const { navItemId } = routerStore.routerState.params; 36 | 37 | const handleNavItemSelected = (navItemId: string) => { 38 | routerStore.goTo('examples', { params: { navItemId } }); 39 | }; 40 | 41 | return ( 42 | 43 |
44 | 45 | 50 | {navItemMap[navItemId]} 51 | 52 | 53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /src/features/Examples/ProductEditor/ProductContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useContext, useState } from 'react'; 2 | import { Product, productData } from '@react-force/mock-data'; 3 | 4 | // ---------- ProductStore ---------- 5 | interface ProductStore { 6 | products: Array; 7 | } 8 | 9 | const defaultProductStore: ProductStore = { 10 | products: productData, 11 | }; 12 | 13 | // ---------- ProductStoreContext ---------- 14 | type ProductStoreSetter = (store: ProductStore) => void; 15 | 16 | const ProductStoreContext = React.createContext( 17 | undefined 18 | ); 19 | const ProductStoreSetterContext = React.createContext< 20 | ProductStoreSetter | undefined 21 | >(undefined); 22 | 23 | // ---------- Hooks ---------- 24 | function useProductStore(): ProductStore { 25 | const productStore = useContext(ProductStoreContext); 26 | if (productStore === undefined) { 27 | /* istanbul ignore next */ 28 | throw new Error( 29 | 'useProductStore must be used within a ProductStoreProvider' 30 | ); 31 | } 32 | return productStore; 33 | } 34 | 35 | function useProductStoreSetter(): ProductStoreSetter { 36 | const setProductStore = useContext(ProductStoreSetterContext); 37 | if (setProductStore === undefined) { 38 | /* istanbul ignore next */ 39 | throw new Error( 40 | 'useProductStoreSetter must be used within a ProductStoreProvider' 41 | ); 42 | } 43 | return setProductStore; 44 | } 45 | 46 | // ---------- ProductStoreProvider ---------- 47 | interface ProductStoreProviderProps { 48 | children: ReactNode; 49 | value?: ProductStore; 50 | } 51 | 52 | const ProductStoreProvider = ({ 53 | children, 54 | value = defaultProductStore, 55 | }: ProductStoreProviderProps) => { 56 | const [productStore, setProductStore] = useState(value); 57 | 58 | return ( 59 | 60 | 61 | {children} 62 | 63 | 64 | ); 65 | }; 66 | 67 | export { ProductStoreProvider, useProductStore, useProductStoreSetter }; 68 | -------------------------------------------------------------------------------- /src/features/Examples/ProductEditor/ProductDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Box from '@material-ui/core/Box'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import { MasterDetailChildProps, useMessageSetter } from '@react-force/core'; 5 | import { 6 | addProduct, 7 | findProduct, 8 | newProduct, 9 | Product, 10 | updateProduct, 11 | } from '@react-force/mock-data'; 12 | import { MessageFactory } from '@react-force/models'; 13 | import { useProductStore, useProductStoreSetter } from './ProductContext'; 14 | import { ProductForm } from './ProductForm'; 15 | 16 | export const ProductDetail = ({ 17 | selectionContext, 18 | onEntitySelected, 19 | onEntityUpdated, 20 | }: MasterDetailChildProps) => { 21 | const { isNew, entityId } = selectionContext; 22 | const setMessage = useMessageSetter(); 23 | const productStore = useProductStore(); 24 | const setProductStore = useProductStoreSetter(); 25 | 26 | const { products, ...rest } = productStore; 27 | const product = isNew ? newProduct() : findProduct(products, entityId); 28 | if (!product) { 29 | return null; 30 | } 31 | 32 | const handleSave = async (product: Product) => { 33 | try { 34 | if (isNew) { 35 | setProductStore({ 36 | products: addProduct(products, product), 37 | ...rest, 38 | }); 39 | setMessage(MessageFactory.success('Product added')); 40 | onEntitySelected(product.id); 41 | } else { 42 | setProductStore({ 43 | products: updateProduct(products, product), 44 | ...rest, 45 | }); 46 | setMessage(MessageFactory.success('Product saved')); 47 | onEntityUpdated(); 48 | } 49 | } catch (error) { 50 | setMessage(MessageFactory.error(error.message)); 51 | } 52 | }; 53 | 54 | return ( 55 | 56 | 57 | {isNew ? 'Add Product' : 'Edit Product'} 58 | 59 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/features/Examples/ProductEditor/ProductEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MasterDetail } from '@react-force/core'; 3 | import { ProductMaster } from './ProductMaster'; 4 | import { ProductDetail } from './ProductDetail'; 5 | import { ProductStoreProvider } from './ProductContext'; 6 | 7 | export const ProductEditor = () => { 8 | return ( 9 | 10 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/features/Examples/ProductEditor/ProductForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import { FormActions, TextField } from '@react-force/formik-mui'; 4 | import { Product } from '@react-force/mock-data'; 5 | import { Form, Formik } from 'formik'; 6 | import * as yup from 'yup'; 7 | 8 | const useStyles = makeStyles(() => ({ 9 | form: { 10 | display: 'flex', 11 | flexDirection: 'column', 12 | }, 13 | })); 14 | 15 | export interface ProductFormProps { 16 | product: Product; 17 | onSave: (product: Product) => void; 18 | } 19 | 20 | export const ProductForm = ({ product, onSave }: ProductFormProps) => { 21 | const classes = useStyles(); 22 | 23 | const validationSchema = yup.object().shape({ 24 | name: yup.string().required(), 25 | department: yup.string().required(), 26 | manufacturer: yup.string().required(), 27 | price: yup.number().required(), 28 | }); 29 | 30 | return ( 31 | 32 | initialValues={product} 33 | validationSchema={validationSchema} 34 | onSubmit={(values, actions) => { 35 | onSave(values); 36 | actions.setSubmitting(false); 37 | }} 38 | > 39 | {() => ( 40 |
41 | 48 | 55 | 62 | 69 | 70 | 71 | 72 | )} 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/features/Examples/ProductEditor/ProductMaster.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Box from '@material-ui/core/Box'; 3 | import { MasterDetailChildProps, MaterialTable } from '@react-force/core'; 4 | import { findProduct, Product, sortProducts } from '@react-force/mock-data'; 5 | import { ColumnDef } from '@react-force/models'; 6 | import { useProductStore } from './ProductContext'; 7 | 8 | export const ProductMaster = ({ 9 | selectionContext, 10 | onEntitySelected, 11 | }: MasterDetailChildProps) => { 12 | const productStore = useProductStore(); 13 | 14 | const columnDefs: Array> = [ 15 | { 16 | field: 'name', 17 | headerName: 'Product', 18 | width: 250, 19 | }, 20 | { 21 | field: 'manufacturer', 22 | headerName: 'Company', 23 | }, 24 | ]; 25 | 26 | const handleEntityClicked = (entity: Product) => { 27 | onEntitySelected(entity.id); 28 | }; 29 | 30 | // Sort a copy of products by manufacturer 31 | const products = sortProducts(productStore.products); 32 | 33 | return ( 34 | 35 | 36 | entityList={products} 37 | columnDefs={columnDefs} 38 | selectedEntity={findProduct( 39 | products, 40 | selectionContext.entityId 41 | )} 42 | onEntityClicked={handleEntityClicked} 43 | /> 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/features/Examples/ProductEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProductEditor'; 2 | -------------------------------------------------------------------------------- /src/features/Examples/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExamplesPage'; 2 | -------------------------------------------------------------------------------- /src/features/Home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from '@material-ui/core/Container'; 3 | import { makeStyles, Theme } from '@material-ui/core/styles'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import { ViewVerticalContainer } from '@react-force/core'; 6 | import { Header } from '../../components'; 7 | import { PersonList } from './PersonList'; 8 | 9 | const useStyles = makeStyles((theme: Theme) => ({ 10 | container: { 11 | paddingTop: theme.spacing(4), 12 | paddingBottom: theme.spacing(4), 13 | }, 14 | section: { 15 | marginTop: theme.spacing(3), 16 | }, 17 | })); 18 | 19 | export const HomePage = () => { 20 | const classes = useStyles(); 21 | 22 | return ( 23 | 24 |
25 | 26 | 27 | Hi people 28 | 29 | 30 | Welcome to React Force 31 | 32 | 33 | 34 | This template is intended to be a starting point for serious 35 | React applications. It offers an opinionated directory 36 | structure and demonstrates best practices in layout, error 37 | handling, hooks and more. To see a quick example, disconnect 38 | from Internet and try to reload this application. 39 | 40 | 41 |
42 | 43 | Sample Data (from JSONPlaceholder) 44 | 45 | 46 |
47 |
48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/features/Home/PersonList/PersonList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '@material-ui/core/Table'; 3 | import TableBody from '@material-ui/core/TableBody'; 4 | import TableCell from '@material-ui/core/TableCell'; 5 | import TableHead from '@material-ui/core/TableHead'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import { usePeople } from './usePeople'; 8 | 9 | export const PersonList = () => { 10 | const { isLoading, isError, data: people, error } = usePeople(); 11 | 12 | // Allow ErrorBoundary to handle errors 13 | if (isError) { 14 | throw error; 15 | } 16 | 17 | if (isLoading || people === undefined) { 18 | return null; 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | Name 26 | Company 27 | Email 28 | City 29 | 30 | 31 | 32 | {people.map((person) => ( 33 | 34 | {person.name} 35 | {person.company} 36 | {person.email} 37 | {person.city} 38 | 39 | ))} 40 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/features/Home/PersonList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PersonList'; 2 | -------------------------------------------------------------------------------- /src/features/Home/PersonList/usePeople.ts: -------------------------------------------------------------------------------- 1 | import { useEnv } from '@react-force/core'; 2 | import axios from 'axios'; 3 | import { useQuery } from 'react-query'; 4 | import { Person } from '../../../models'; 5 | import { EnvVar } from '../../../utils'; 6 | 7 | /** 8 | * Maps person received from server to domain 9 | * @param person 10 | */ 11 | const mapPersonToDomain = (person: any): Person => { 12 | const { id, name, company, email, address } = person; 13 | 14 | return { 15 | id, 16 | name, 17 | company: company.name, 18 | email, 19 | city: address.city, 20 | }; 21 | }; 22 | 23 | /** 24 | * Fetches people from server 25 | * @param url 26 | */ 27 | async function fetchPeople(url: string): Promise> { 28 | const resp = await axios.get(url); 29 | const data = resp.data; 30 | 31 | // Convert to application domain and return 32 | const people: Array = data.map((person: any) => 33 | mapPersonToDomain(person) 34 | ); 35 | 36 | // Sort by name 37 | people.sort((a, b) => { 38 | if (a.name < b.name) return -1; 39 | if (a.name > b.name) return 1; 40 | return 0; 41 | }); 42 | 43 | return people; 44 | } 45 | 46 | /** 47 | * Hook to fetch people from server 48 | */ 49 | export const usePeople = () => { 50 | const env = useEnv(); 51 | const apiUrl = env.get(EnvVar.API_URL); 52 | 53 | return useQuery, 'people'>( 54 | 'people', 55 | async () => { 56 | return fetchPeople(`${apiUrl}/users`); 57 | }, 58 | { 59 | refetchOnWindowFocus: false, 60 | staleTime: Infinity, 61 | } 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/features/Home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HomePage'; 2 | -------------------------------------------------------------------------------- /src/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Examples'; 2 | export * from './Home'; 3 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { EnvProvider } from '@react-force/core'; 4 | import { App } from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'mobx'; 2 | import { browserHistory, HistoryAdapter } from 'mobx-state-router'; 3 | import { RootStore } from './stores'; 4 | 5 | function initMobX() { 6 | // Enable strict mode for MobX. 7 | // This disallows state changes outside of an action. 8 | configure({ enforceActions: 'observed' }); 9 | } 10 | 11 | function initStores() { 12 | // Create the rootStore 13 | const rootStore = new RootStore(); 14 | const { prefStore, routerStore } = rootStore; 15 | 16 | // Load preferences 17 | prefStore.loadFromStorage(); 18 | 19 | // Observe history changes 20 | const historyAdapter = new HistoryAdapter(routerStore, browserHistory); 21 | historyAdapter.observeRouterStateChanges(); 22 | 23 | return rootStore; 24 | } 25 | 26 | export function initApp() { 27 | initMobX(); 28 | return initStores(); 29 | } 30 | -------------------------------------------------------------------------------- /src/models/Person.ts: -------------------------------------------------------------------------------- 1 | export interface Person { 2 | id: number; 3 | name: string; 4 | company: string; 5 | email: string; 6 | city: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Person'; 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/stores/LsKeys.ts: -------------------------------------------------------------------------------- 1 | // Local storage keys 2 | export const LsKeys = { 3 | paletteType: 'paletteType', 4 | }; 5 | -------------------------------------------------------------------------------- /src/stores/PrefStore.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, PaletteType } from '@material-ui/core'; 2 | import red from '@material-ui/core/colors/red'; 3 | 4 | import { Storage } from '@react-force/web-utils'; 5 | import { action, computed, decorate, observable } from 'mobx'; 6 | import { LsKeys } from './LsKeys'; 7 | import { RootStore } from './RootStore'; 8 | 9 | export class PrefStore { 10 | rootStore: RootStore; 11 | paletteType: PaletteType = 'light'; 12 | 13 | constructor(rootStore: RootStore) { 14 | this.rootStore = rootStore; 15 | } 16 | 17 | // ----- Load from storage ----- 18 | loadFromStorage() { 19 | this.paletteType = Storage.get(LsKeys.paletteType, 'light'); 20 | } 21 | 22 | // ----- Actions ----- 23 | toggleTheme = () => { 24 | this.paletteType = this.paletteType === 'light' ? 'dark' : 'light'; 25 | Storage.set(LsKeys.paletteType, this.paletteType); 26 | }; 27 | 28 | // ----- Computed ----- 29 | get theme() { 30 | const palette = { 31 | primary: { 32 | main: '#556CD6', 33 | }, 34 | secondary: { 35 | main: '#FBC02D', 36 | }, 37 | error: { 38 | main: red.A400, 39 | }, 40 | type: this.paletteType, 41 | // Initialize background to white (default is #fafafa) 42 | // This allows pictures with white background to blend in. 43 | background: { 44 | default: this.paletteType === 'light' ? '#ffffff' : '#303030', 45 | }, 46 | }; 47 | 48 | return createMuiTheme({ palette }); 49 | } 50 | } 51 | 52 | decorate(PrefStore, { 53 | paletteType: observable, 54 | theme: computed, 55 | loadFromStorage: action, 56 | toggleTheme: action, 57 | }); 58 | -------------------------------------------------------------------------------- /src/stores/RootStore.ts: -------------------------------------------------------------------------------- 1 | import { createRouterState, RouterStore } from 'mobx-state-router'; 2 | import { PrefStore } from './PrefStore'; 3 | import { routes } from './routes'; 4 | 5 | const notFound = createRouterState('notFound'); 6 | 7 | export class RootStore { 8 | prefStore = new PrefStore(this); 9 | 10 | // Pass rootStore as an option to RouterStore 11 | routerStore = new RouterStore(routes, notFound, { 12 | rootStore: this, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { RootStore } from './RootStore'; 2 | -------------------------------------------------------------------------------- /src/stores/routes.ts: -------------------------------------------------------------------------------- 1 | // Routes are matched from top to bottom. Make sure they are sequenced 2 | // in the order of priority. It is generally best to sort them by pattern, 3 | // prioritizing specific patterns over generic patterns (patterns with 4 | // one or more parameters). For example: 5 | // /items 6 | // /items/:id 7 | export const routes = [ 8 | { name: 'home', pattern: '/' }, 9 | { 10 | name: 'examples', 11 | pattern: '/examples/:navItemId', 12 | }, 13 | { name: 'notFound', pattern: '/not-found' }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment variables 3 | */ 4 | 5 | export const EnvVar = { 6 | API_URL: 'API_URL', 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './math-utils'; 3 | -------------------------------------------------------------------------------- /src/utils/math-utils.ts: -------------------------------------------------------------------------------- 1 | export const add = (a: number, b: number) => a + b; 2 | -------------------------------------------------------------------------------- /src/viewMap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NotFound } from '@react-force/core'; 3 | import { ExamplesPage, HomePage } from './features'; 4 | 5 | export const viewMap = { 6 | examples: , 7 | home: , 8 | notFound: , 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------