├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-4.8.1.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── eslint.config.mts ├── package.json ├── packages ├── cra-template-redux-typescript │ ├── README.md │ ├── package.json │ ├── template.json │ └── template │ │ ├── .prettierrc.json │ │ ├── README.md │ │ ├── eslint.config.mjs │ │ ├── gitignore │ │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── app │ │ │ ├── createAppSlice.ts │ │ │ ├── hooks.ts │ │ │ └── store.ts │ │ ├── features │ │ │ ├── counter │ │ │ │ ├── Counter.module.css │ │ │ │ ├── Counter.tsx │ │ │ │ ├── counterAPI.ts │ │ │ │ ├── counterSlice.test.ts │ │ │ │ └── counterSlice.ts │ │ │ └── quotes │ │ │ │ ├── Quotes.module.css │ │ │ │ ├── Quotes.tsx │ │ │ │ └── quotesApiSlice.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── utils │ │ │ └── test-utils.tsx │ │ └── tsconfig.json ├── cra-template-redux │ ├── README.md │ ├── package.json │ ├── template.json │ └── template │ │ ├── .prettierrc.json │ │ ├── README.md │ │ ├── eslint.config.mjs │ │ ├── gitignore │ │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ │ └── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── App.test.jsx │ │ ├── app │ │ ├── createAppSlice.js │ │ └── store.js │ │ ├── features │ │ ├── counter │ │ │ ├── Counter.jsx │ │ │ ├── Counter.module.css │ │ │ ├── counterAPI.js │ │ │ ├── counterSlice.js │ │ │ └── counterSlice.test.js │ │ └── quotes │ │ │ ├── Quotes.jsx │ │ │ ├── Quotes.module.css │ │ │ └── quotesApiSlice.js │ │ ├── index.css │ │ ├── index.jsx │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ ├── setupTests.js │ │ └── utils │ │ └── test-utils.jsx ├── expo-template-redux-typescript │ ├── .gitignore │ ├── .prettierrc.json │ ├── App.tsx │ ├── README.md │ ├── app.json │ ├── assets │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash-icon.png │ ├── babel.config.js │ ├── eslint.config.mjs │ ├── gitignore │ ├── globals.d.ts │ ├── index.ts │ ├── jest-setup.ts │ ├── jest.config.ts │ ├── metro.base.config.ts │ ├── metro.config.js │ ├── package.json │ ├── src │ │ ├── Main.test.tsx │ │ ├── Main.tsx │ │ ├── app │ │ │ ├── createAppSlice.ts │ │ │ ├── hooks.ts │ │ │ └── store.ts │ │ ├── components │ │ │ ├── AsyncButton.tsx │ │ │ ├── ExternalLink.tsx │ │ │ ├── ExternalLinks.tsx │ │ │ ├── Header.tsx │ │ │ ├── LearnReduxLinks.tsx │ │ │ ├── Section.tsx │ │ │ └── logo.gif │ │ ├── constants │ │ │ └── Colors.ts │ │ ├── features │ │ │ ├── counter │ │ │ │ ├── Counter.tsx │ │ │ │ ├── counterAPI.ts │ │ │ │ ├── counterSlice.test.ts │ │ │ │ └── counterSlice.ts │ │ │ └── quotes │ │ │ │ ├── Quotes.tsx │ │ │ │ └── quotesApiSlice.ts │ │ └── utils │ │ │ └── test-utils.tsx │ └── tsconfig.json ├── react-native-template-redux-typescript │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── template.config.js │ └── template │ │ ├── .bundle │ │ └── config │ │ ├── .gitignore │ │ ├── .prettierrc.json │ │ ├── .watchmanconfig │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── Gemfile │ │ ├── README.md │ │ ├── android │ │ ├── app │ │ │ ├── build.gradle │ │ │ ├── debug.keystore │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── ReactNativeReduxTemplate │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── MainApplication.kt │ │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── eslint.config.mjs │ │ ├── globals.d.ts │ │ ├── index.js │ │ ├── ios │ │ ├── .xcode.env │ │ ├── Podfile │ │ ├── ReactNativeReduxTemplate.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── ReactNativeReduxTemplate.xcscheme │ │ ├── ReactNativeReduxTemplate │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.mm │ │ │ ├── Images.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── PrivacyInfo.xcprivacy │ │ │ └── main.m │ │ └── ReactNativeReduxTemplateTests │ │ │ ├── Info.plist │ │ │ └── ReactNativeReduxTemplateTests.m │ │ ├── jest-setup.ts │ │ ├── jest.config.ts │ │ ├── metro.config.js │ │ ├── package.json │ │ ├── react-native.config.mjs │ │ ├── src │ │ ├── app │ │ │ ├── createAppSlice.ts │ │ │ ├── hooks.ts │ │ │ └── store.ts │ │ ├── components │ │ │ ├── AsyncButton.tsx │ │ │ ├── Header.tsx │ │ │ ├── LearnReduxLinks.tsx │ │ │ ├── Section.tsx │ │ │ └── logo.gif │ │ ├── constants │ │ │ └── TypedColors.ts │ │ ├── features │ │ │ ├── counter │ │ │ │ ├── Counter.tsx │ │ │ │ ├── counterAPI.ts │ │ │ │ ├── counterSlice.test.ts │ │ │ │ └── counterSlice.ts │ │ │ └── quotes │ │ │ │ ├── Quotes.tsx │ │ │ │ └── quotesApiSlice.ts │ │ └── utils │ │ │ └── test-utils.tsx │ │ └── tsconfig.json ├── rtk-app-structure-example │ ├── .gitignore │ ├── .prettierrc.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── app │ │ │ ├── hooks.ts │ │ │ └── store.ts │ │ ├── features │ │ │ └── counter │ │ │ │ ├── Counter.module.css │ │ │ │ ├── Counter.tsx │ │ │ │ ├── counterAPI.ts │ │ │ │ └── counterSlice.ts │ │ ├── index.css │ │ ├── logo.svg │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.mts └── vite-template-redux │ ├── .gitignore │ ├── .prettierrc.json │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── app │ │ ├── createAppSlice.ts │ │ ├── hooks.ts │ │ └── store.ts │ ├── features │ │ ├── counter │ │ │ ├── Counter.module.css │ │ │ ├── Counter.tsx │ │ │ ├── counterAPI.ts │ │ │ ├── counterSlice.test.ts │ │ │ └── counterSlice.ts │ │ └── quotes │ │ │ ├── Quotes.module.css │ │ │ ├── Quotes.tsx │ │ │ └── quotesApiSlice.ts │ ├── index.css │ ├── logo.svg │ ├── main.tsx │ ├── setupTests.ts │ ├── utils │ │ └── test-utils.tsx │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── prettier.config.mjs ├── scripts └── mockTemplates.mts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | name: Test and lint ${{ matrix.packages }} on Node.js ${{ matrix.node }} and ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [22.x] 17 | os: [ubuntu-latest, windows-latest] 18 | packages: 19 | [ 20 | "cra-template-redux", 21 | "cra-template-redux-typescript", 22 | "expo-template-redux-typescript", 23 | "react-native-template-redux-typescript", 24 | "vite-template-redux", 25 | ] 26 | 27 | steps: 28 | - name: Checkout repository for ${{ matrix.packages }} on ${{ matrix.node }} and ${{ matrix.os }} 29 | if: ${{ github.event_name == 'pull_request' }} 30 | uses: actions/checkout@v4 31 | with: 32 | ref: ${{ github.event.pull_request.head.ref }} 33 | repository: ${{ github.event.pull_request.head.repo.full_name }} 34 | 35 | - name: Checkout repository for ${{ matrix.packages }} on ${{ matrix.node }} and ${{ matrix.os }} 36 | if: ${{ github.event_name != 'pull_request' }} 37 | uses: actions/checkout@v4 38 | 39 | - name: Setup Node.js ${{ matrix.node }} for ${{ matrix.packages }} on ${{ matrix.os }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node }} 43 | check-latest: true 44 | 45 | - name: Set up environment variables for ${{ matrix.packages }} on ${{ matrix.node }} and ${{ matrix.os }} 46 | if: ${{ matrix.os == 'windows-latest' }} 47 | run: echo "TEMP=$USERPROFILE\AppData\Local\Temp" >> $GITHUB_ENV 48 | 49 | - name: Set up JDK for React Native build for ${{ matrix.packages }} on ${{ matrix.node }} and ${{ matrix.os }} 50 | if: matrix.packages == 'react-native-template-redux-typescript' || matrix.packages == 'expo-template-redux-typescript' 51 | uses: actions/setup-java@v4 52 | with: 53 | java-version: "21.x" 54 | distribution: "temurin" 55 | cache: "gradle" 56 | 57 | - name: Mock the templates for ${{ matrix.packages }} on ${{ matrix.node }} and ${{ matrix.os }} 58 | run: npx -y tsx@latest scripts/mockTemplates.mts ${{ matrix.packages }} 59 | 60 | - name: Did we fail? 61 | if: failure() 62 | working-directory: packages/${{ matrix.packages }} 63 | run: ls -R 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | dist*/ 5 | lib 6 | es 7 | 8 | .yalc/ 9 | yalc.lock 10 | 11 | .idea/ 12 | .vscode/ 13 | temp/ 14 | .tmp-projections 15 | build/ 16 | .rts2* 17 | coverage/ 18 | 19 | typesversions 20 | .cache 21 | .yarnrc 22 | .yarn/* 23 | !.yarn/patches 24 | !.yarn/releases 25 | !.yarn/plugins 26 | !.yarn/sdks 27 | !.yarn/versions 28 | .pnp.* 29 | *.tgz 30 | 31 | tsconfig.vitest-temp.json 32 | 33 | # Ignore template lock files 34 | /packages/**/package-lock.json 35 | /packages/**/yarn.lock 36 | 37 | .eslintcache 38 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | enableTransparentWorkspaces: false 6 | 7 | nodeLinker: node-modules 8 | 9 | yarnPath: .yarn/releases/yarn-4.8.1.cjs 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Redux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Project Templates 2 | 3 | This monorepo contains the official Redux templates for Vite, Create-React-App, and (eventually) more. 4 | 5 | For installation and setup instructions, see the README file in each project template folder under `./packages/`. 6 | 7 | Currently, this repo contains these templates: 8 | 9 | - `vite-template-redux`: Vite, with TypeScript 10 | - `cra-template-redux-typescript`: Create-React-App, with TypeScript 11 | - `cra-template-redux`: Create-React-App, with JavaScript 12 | - `expo-template-redux`: Expo, with TypeScript 13 | - `rtk-app-structure-example`: A standalone example of a Redux Toolkit 14 | -------------------------------------------------------------------------------- /eslint.config.mts: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import prettierConfig from "eslint-config-prettier/flat" 3 | import type { ConfigArray } from "typescript-eslint" 4 | import { config, configs } from "typescript-eslint" 5 | 6 | const eslintConfig: ConfigArray = config( 7 | { 8 | name: "global-ignores", 9 | ignores: [ 10 | "**/*.snap", 11 | "**/dist/", 12 | "**/.yalc/", 13 | "**/build/", 14 | "**/temp/", 15 | "**/.temp/", 16 | "**/.tmp/", 17 | "**/.yarn/", 18 | "**/coverage/", 19 | "packages/rtk-app-structure-example", 20 | "packages/vite-template-redux", 21 | "packages/react-native-template-redux-typescript/template", 22 | "packages/expo-template-redux-typescript", 23 | "packages/cra-template-redux/template", 24 | "packages/cra-template-redux-typescript/template", 25 | ], 26 | }, 27 | { 28 | name: `${js.meta.name}/recommended`, 29 | ...js.configs.recommended, 30 | }, 31 | configs.strictTypeChecked, 32 | configs.stylisticTypeChecked, 33 | { 34 | name: "main", 35 | linterOptions: { 36 | reportUnusedDisableDirectives: 2, 37 | }, 38 | languageOptions: { 39 | parserOptions: { 40 | projectService: true, 41 | tsconfigRootDir: import.meta.dirname, 42 | }, 43 | }, 44 | rules: { 45 | "no-undef": [0], 46 | "@typescript-eslint/consistent-type-definitions": [2, "type"], 47 | "@typescript-eslint/consistent-type-imports": [ 48 | 2, 49 | { 50 | prefer: "type-imports", 51 | fixStyle: "separate-type-imports", 52 | disallowTypeAnnotations: true, 53 | }, 54 | ], 55 | }, 56 | }, 57 | 58 | prettierConfig, 59 | ) 60 | 61 | export default eslintConfig 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-templates", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "homepage": "https://github.com/reduxjs/redux-templates#readme", 7 | "bugs": { 8 | "url": "https://github.com/reduxjs/redux-templates/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/reduxjs/redux-templates.git", 13 | "directory": "." 14 | }, 15 | "workspaces": [ 16 | "packages/*" 17 | ], 18 | "scripts": { 19 | "format": "prettier --config=$INIT_CWD/prettier.config.mjs --write $INIT_CWD", 20 | "format-check": "prettier --config=$INIT_CWD/prettier.config.mjs --check $INIT_CWD", 21 | "lint": "eslint --config=$INIT_CWD/eslint.config.mts $INIT_CWD", 22 | "lint-fix": "eslint --config=$INIT_CWD/eslint.config.mts $INIT_CWD --fix", 23 | "type-check": "tsc -p $INIT_CWD/tsconfig.json --noEmit" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.23.0", 27 | "@types/node": "^22.14.0", 28 | "eslint": "^9.23.0", 29 | "eslint-config-prettier": "^10.1.1", 30 | "jiti": "^2.4.2", 31 | "prettier": "^3.5.3", 32 | "rimraf": "^6.0.1", 33 | "tsx": "^4.19.3", 34 | "typescript": "^5.8.2", 35 | "typescript-eslint": "^8.29.0" 36 | }, 37 | "packageManager": "yarn@4.8.1" 38 | } 39 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/README.md: -------------------------------------------------------------------------------- 1 | # cra-template-redux-typescript 2 | 3 | ![build status](https://img.shields.io/github/workflow/status/reduxjs/cra-template-redux-typescript/Tests/master?style=flat-square) 4 | 5 | The official Redux+TS template for [Create React App](https://github.com/facebook/create-react-app). 6 | 7 | To use this template within your project, add `--template redux-typescript` when creating a new app. 8 | 9 | For example: 10 | 11 | ```sh 12 | npx create-react-app my-app --template redux-typescript 13 | 14 | # or 15 | 16 | yarn create react-app my-app --template redux-typescript 17 | ``` 18 | 19 | Cloning this repo pulls down the Redux template only; not a bundled and configured Create React App. 20 | 21 | For more information, please refer to: 22 | 23 | - [Getting Started](https://create-react-app.dev/docs/getting-started) – How to create a new app. 24 | - [User Guide](https://create-react-app.dev) – How to develop apps bootstrapped with Create React App. 25 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-template-redux-typescript", 3 | "version": "2.0.0", 4 | "scripts": { 5 | "format": "prettier --write ./template/src/**/*.{ts,tsx,css}", 6 | "lint": "eslint './template/src/**/*.{ts,tsx}'", 7 | "test": "jest", 8 | "type-check": "tsc" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "create-react-app", 13 | "template", 14 | "typescript", 15 | "redux", 16 | "reduxjs", 17 | "react-redux" 18 | ], 19 | "description": "The official Redux+TS template for Create React App", 20 | "main": "template.json", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/reduxjs/cra-template-redux-typescript.git", 24 | "directory": "packages/cra-template-redux-typescript" 25 | }, 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=8.10" 29 | }, 30 | "homepage": "https://github.com/reduxjs/redux-templates/tree/HEAD/packages/cra-template-redux-typescript#readme", 31 | "bugs": { 32 | "url": "https://github.com/reduxjs/redux-templates/issues" 33 | }, 34 | "files": [ 35 | "template", 36 | "template.json" 37 | ], 38 | "devDependencies": { 39 | "@babel/core": "^7.26.10", 40 | "@babel/preset-env": "^7.26.9", 41 | "@babel/preset-react": "^7.26.3", 42 | "@reduxjs/toolkit": "^2.6.1", 43 | "@testing-library/dom": "^10.4.0", 44 | "@testing-library/jest-dom": "^6.6.3", 45 | "@testing-library/react": "^16.3.0", 46 | "@testing-library/user-event": "^14.6.1", 47 | "@types/jest": "^29.5.14", 48 | "@types/node": "^22.14.0", 49 | "@types/react": "^19.1.0", 50 | "@types/react-dom": "^19.1.1", 51 | "babel-eslint": "^10.1.0", 52 | "babel-jest": "^29.7.0", 53 | "eslint": "^9.23.0", 54 | "eslint-config-react-app": "^7.0.1", 55 | "eslint-plugin-import": "^2.31.0", 56 | "eslint-plugin-jsx-a11y": "^6.10.2", 57 | "eslint-plugin-prettier": "^5.2.6", 58 | "eslint-plugin-react": "^7.37.4", 59 | "eslint-plugin-react-hooks": "^5.2.0", 60 | "jest": "^29.7.0", 61 | "prettier": "^3.5.3", 62 | "react": "^19.1.0", 63 | "react-dom": "^19.1.0", 64 | "react-redux": "^9.2.0", 65 | "react-scripts": "^5.0.1", 66 | "react-test-renderer": "^19.1.0", 67 | "typescript": "^5.8.2", 68 | "web-vitals": "^4.2.4" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "dependencies": { 4 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 5 | "@eslint/js": "^9.23.0", 6 | "@reduxjs/toolkit": "^2.6.1", 7 | "@testing-library/jest-dom": "^6.6.3", 8 | "@testing-library/react": "^16.2.0", 9 | "@testing-library/user-event": "^14.6.1", 10 | "@types/jest": "^29.5.14", 11 | "@types/node": "^22.13.14", 12 | "@types/react": "^19.0.12", 13 | "@types/react-dom": "^19.0.4", 14 | "eslint": "^9.23.0", 15 | "eslint-config-prettier": "^10.1.1", 16 | "eslint-plugin-jest": "^28.11.0", 17 | "eslint-plugin-react": "^7.37.4", 18 | "eslint-plugin-react-hooks": "^5.2.0", 19 | "globals": "^16.0.0", 20 | "prettier": "^3.5.3", 21 | "react-redux": "^9.2.0", 22 | "typescript": "^4.9.5", 23 | "typescript-eslint": "^8.28.0", 24 | "web-vitals": "^4.2.4" 25 | }, 26 | "eslintConfig": {}, 27 | "scripts": { 28 | "format": "prettier --write .", 29 | "format:check": "prettier --check .", 30 | "lint": "eslint .", 31 | "lint:fix": "eslint --fix .", 32 | "type-check": "tsc" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) TS template. 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import prettierConfig from "eslint-config-prettier/flat" 3 | import jestPlugin from "eslint-plugin-jest" 4 | import reactPlugin from "eslint-plugin-react" 5 | import reactHooksPlugin from "eslint-plugin-react-hooks" 6 | import globals from "globals" 7 | import { config, configs } from "typescript-eslint" 8 | 9 | const eslintConfig = config( 10 | { 11 | name: "global-ignores", 12 | ignores: [ 13 | "**/*.snap", 14 | "**/dist/", 15 | "**/.yalc/", 16 | "**/build/", 17 | "**/temp/", 18 | "**/.temp/", 19 | "**/.tmp/", 20 | "**/.yarn/", 21 | "**/coverage/", 22 | ], 23 | }, 24 | { 25 | name: `${js.meta.name}/recommended`, 26 | ...js.configs.recommended, 27 | }, 28 | configs.strictTypeChecked, 29 | configs.stylisticTypeChecked, 30 | { 31 | name: `${jestPlugin.meta.name}/recommended`, 32 | ...jestPlugin.configs["flat/recommended"], 33 | }, 34 | { 35 | name: "eslint-plugin-react/jsx-runtime", 36 | ...reactPlugin.configs.flat["jsx-runtime"], 37 | }, 38 | reactHooksPlugin.configs["recommended-latest"], 39 | { 40 | name: "main", 41 | linterOptions: { 42 | reportUnusedDisableDirectives: 2, 43 | }, 44 | languageOptions: { 45 | ecmaVersion: 2020, 46 | globals: globals.browser, 47 | parserOptions: { 48 | projectService: { 49 | allowDefaultProject: ["*.?(c|m)[jt]s?(x)"], 50 | }, 51 | tsconfigRootDir: import.meta.dirname, 52 | }, 53 | }, 54 | rules: { 55 | "no-undef": [0], 56 | "no-restricted-imports": [ 57 | 2, 58 | { 59 | paths: [ 60 | { 61 | name: "react-redux", 62 | importNames: ["useSelector", "useStore", "useDispatch"], 63 | message: 64 | "Please use pre-typed versions from `src/app/hooks.ts` instead.", 65 | }, 66 | ], 67 | }, 68 | ], 69 | "@typescript-eslint/consistent-type-definitions": [2, "type"], 70 | "@typescript-eslint/consistent-type-imports": [ 71 | 2, 72 | { 73 | prefer: "type-imports", 74 | fixStyle: "separate-type-imports", 75 | disallowTypeAnnotations: true, 76 | }, 77 | ], 78 | }, 79 | }, 80 | 81 | prettierConfig, 82 | ) 83 | 84 | export default eslintConfig 85 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/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 | 25 | .yalc/ 26 | yalc.lock 27 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/cra-template-redux-typescript/template/public/favicon.ico -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Redux App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/cra-template-redux-typescript/template/public/logo192.png -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/cra-template-redux-typescript/template/public/logo512.png -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-float infinite 3s ease-in-out; 13 | } 14 | } 15 | 16 | .App-header { 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | font-size: calc(10px + 2vmin); 23 | } 24 | 25 | .App-link { 26 | color: rgb(112, 76, 182); 27 | } 28 | 29 | @keyframes App-logo-float { 30 | 0% { 31 | transform: translateY(0); 32 | } 33 | 50% { 34 | transform: translateY(10px); 35 | } 36 | 100% { 37 | transform: translateY(0px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css" 2 | import { Counter } from "./features/counter/Counter" 3 | import { Quotes } from "./features/quotes/Quotes" 4 | import logo from "./logo.svg" 5 | 6 | export const App = () => ( 7 |
8 |
9 | logo 10 | 11 |

12 | Edit src/App.tsx and save to reload. 13 |

14 | 15 | 16 | Learn 17 | 23 | React 24 | 25 | , 26 | 32 | Redux 33 | 34 | , 35 | 41 | Redux Toolkit 42 | 43 | , 44 | 50 | React Redux 51 | 52 | , and 53 | 59 | Reselect 60 | 61 | 62 |
63 |
64 | ) 65 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/app/createAppSlice.ts: -------------------------------------------------------------------------------- 1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" 2 | 3 | // `buildCreateSlice` allows us to create a slice with async thunks. 4 | export const createAppSlice = buildCreateSlice({ 5 | creators: { asyncThunk: asyncThunkCreator }, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks. 2 | // These imports are restricted elsewhere to ensure consistent 3 | // usage of typed hooks throughout the application. 4 | // We disable the ESLint rule here because this is the designated place 5 | // for importing and re-exporting the typed versions of hooks. 6 | /* eslint-disable no-restricted-imports */ 7 | import { useDispatch, useSelector } from "react-redux" 8 | import type { AppDispatch, RootState } from "./store" 9 | 10 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 11 | export const useAppDispatch = useDispatch.withTypes() 12 | export const useAppSelector = useSelector.withTypes() 13 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ThunkAction } from "@reduxjs/toolkit" 2 | import { combineSlices, configureStore } from "@reduxjs/toolkit" 3 | import { setupListeners } from "@reduxjs/toolkit/query" 4 | import { counterSlice } from "../features/counter/counterSlice" 5 | import { quotesApiSlice } from "../features/quotes/quotesApiSlice" 6 | 7 | // `combineSlices` automatically combines the reducers using 8 | // their `reducerPath`s, therefore we no longer need to call `combineReducers`. 9 | const rootReducer = combineSlices(counterSlice, quotesApiSlice) 10 | // Infer the `RootState` type from the root reducer 11 | export type RootState = ReturnType 12 | 13 | // The store setup is wrapped in `makeStore` to allow reuse 14 | // when setting up tests that need the same store config 15 | export const makeStore = (preloadedState?: Partial) => { 16 | const store = configureStore({ 17 | reducer: rootReducer, 18 | // Adding the api middleware enables caching, invalidation, polling, 19 | // and other useful features of `rtk-query`. 20 | middleware: getDefaultMiddleware => { 21 | return getDefaultMiddleware().concat(quotesApiSlice.middleware) 22 | }, 23 | preloadedState, 24 | }) 25 | // configure listeners using the provided defaults 26 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors 27 | setupListeners(store.dispatch) 28 | return store 29 | } 30 | 31 | export const store = makeStore() 32 | 33 | // Infer the type of `store` 34 | export type AppStore = typeof store 35 | // Infer the `AppDispatch` type from the store itself 36 | export type AppDispatch = AppStore["dispatch"] 37 | export type AppThunk = ThunkAction< 38 | ThunkReturnType, 39 | RootState, 40 | unknown, 41 | Action 42 | > 43 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/counter/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: "Courier New", Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .asyncButton { 58 | composes: button; 59 | position: relative; 60 | } 61 | 62 | .asyncButton:after { 63 | content: ""; 64 | background-color: rgba(112, 76, 182, 0.15); 65 | display: block; 66 | position: absolute; 67 | width: 100%; 68 | height: 100%; 69 | left: 0; 70 | top: 0; 71 | opacity: 0; 72 | transition: 73 | width 1s linear, 74 | opacity 0.5s ease 1s; 75 | } 76 | 77 | .asyncButton:active:after { 78 | width: 0%; 79 | opacity: 1; 80 | transition: 0s; 81 | } 82 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { useState } from "react" 3 | import { useAppDispatch, useAppSelector } from "../../app/hooks" 4 | import styles from "./Counter.module.css" 5 | import { 6 | decrement, 7 | increment, 8 | incrementAsync, 9 | incrementByAmount, 10 | incrementIfOdd, 11 | selectCount, 12 | selectStatus, 13 | } from "./counterSlice" 14 | 15 | export const Counter = (): JSX.Element => { 16 | const dispatch = useAppDispatch() 17 | const count = useAppSelector(selectCount) 18 | const status = useAppSelector(selectStatus) 19 | const [incrementAmount, setIncrementAmount] = useState("2") 20 | 21 | const incrementValue = Number(incrementAmount) || 0 22 | 23 | return ( 24 |
25 |
26 | 33 | 36 | 43 |
44 |
45 | { 51 | setIncrementAmount(e.target.value) 52 | }} 53 | /> 54 | 60 | 69 | 77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export const fetchCount = (amount = 1): Promise<{ data: number }> => 3 | new Promise<{ data: number }>(resolve => 4 | setTimeout(() => { 5 | resolve({ data: amount }) 6 | }, 500), 7 | ) 8 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/counter/counterSlice.test.ts: -------------------------------------------------------------------------------- 1 | import { makeStore } from "../../app/store" 2 | import type { CounterSliceState } from "./counterSlice" 3 | import { 4 | counterSlice, 5 | decrement, 6 | increment, 7 | incrementByAmount, 8 | selectCount, 9 | } from "./counterSlice" 10 | 11 | describe("counter reducer", () => { 12 | const initialState: CounterSliceState = { 13 | value: 3, 14 | status: "idle", 15 | } 16 | 17 | let store = makeStore() 18 | 19 | beforeEach(() => { 20 | store = makeStore({ counter: initialState }) 21 | }) 22 | 23 | it("should handle initial state", () => { 24 | expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ 25 | value: 0, 26 | status: "idle", 27 | }) 28 | }) 29 | 30 | it("should handle increment", () => { 31 | expect(selectCount(store.getState())).toBe(3) 32 | 33 | store.dispatch(increment()) 34 | 35 | expect(selectCount(store.getState())).toBe(4) 36 | }) 37 | 38 | it("should handle decrement", () => { 39 | expect(selectCount(store.getState())).toBe(3) 40 | 41 | store.dispatch(decrement()) 42 | 43 | expect(selectCount(store.getState())).toBe(2) 44 | }) 45 | 46 | it("should handle incrementByAmount", () => { 47 | expect(selectCount(store.getState())).toBe(3) 48 | 49 | store.dispatch(incrementByAmount(2)) 50 | 51 | expect(selectCount(store.getState())).toBe(5) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/quotes/Quotes.module.css: -------------------------------------------------------------------------------- 1 | .select { 2 | font-size: 25px; 3 | padding: 5px; 4 | padding-top: 2px; 5 | padding-bottom: 2px; 6 | size: 50; 7 | outline: none; 8 | border: 2px solid transparent; 9 | color: rgb(112, 76, 182); 10 | cursor: pointer; 11 | background-color: rgba(112, 76, 182, 0.1); 12 | border-radius: 5px; 13 | transition: all 0.15s; 14 | } 15 | 16 | .container { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/quotes/Quotes.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { useState } from "react" 3 | import styles from "./Quotes.module.css" 4 | import { useGetQuotesQuery } from "./quotesApiSlice" 5 | 6 | const options = [5, 10, 20, 30] 7 | 8 | export const Quotes = (): JSX.Element | null => { 9 | const [numberOfQuotes, setNumberOfQuotes] = useState(10) 10 | // Using a query hook automatically fetches data and returns query values 11 | const { data, isError, isLoading, isSuccess } = 12 | useGetQuotesQuery(numberOfQuotes) 13 | 14 | if (isError) { 15 | return ( 16 |
17 |

There was an error!!!

18 |
19 | ) 20 | } 21 | 22 | if (isLoading) { 23 | return ( 24 |
25 |

Loading...

26 |
27 | ) 28 | } 29 | 30 | if (isSuccess) { 31 | return ( 32 |
33 |

Select the Quantity of Quotes to Fetch:

34 | 47 | {data.quotes.map(({ author, quote, id }) => ( 48 |
49 | “{quote}” 50 |
51 | {author} 52 |
53 |
54 | ))} 55 |
56 | ) 57 | } 58 | 59 | return null 60 | } 61 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/features/quotes/quotesApiSlice.ts: -------------------------------------------------------------------------------- 1 | // Need to use the React-specific entry point to import `createApi` 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" 3 | 4 | type Quote = { 5 | id: number 6 | quote: string 7 | author: string 8 | } 9 | 10 | type QuotesApiResponse = { 11 | quotes: Quote[] 12 | total: number 13 | skip: number 14 | limit: number 15 | } 16 | 17 | // Define a service using a base URL and expected endpoints 18 | export const quotesApiSlice = createApi({ 19 | baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), 20 | reducerPath: "quotesApi", 21 | // Tag types are used for caching and invalidation. 22 | tagTypes: ["Quotes"], 23 | endpoints: build => ({ 24 | // Supply generics for the return type (in this case `QuotesApiResponse`) 25 | // and the expected query argument. If there is no argument, use `void` 26 | // for the argument type instead. 27 | getQuotes: build.query({ 28 | query: (limit = 10) => `?limit=${limit.toString()}`, 29 | // `providesTags` determines which 'tag' is attached to the 30 | // cached data returned by the query. 31 | providesTags: (_result, _error, id) => [{ type: "Quotes", id }], 32 | }), 33 | }), 34 | }) 35 | 36 | // Hooks are auto-generated by RTK-Query 37 | // Same as `quotesApiSlice.endpoints.getQuotes.useQuery` 38 | export const { useGetQuotesQuery } = quotesApiSlice 39 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: 12 | source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { Provider } from "react-redux" 4 | import { App } from "./App" 5 | import { store } from "./app/store" 6 | import "./index.css" 7 | import reportWebVitals from "./reportWebVitals" 8 | 9 | const container = document.getElementById("root") 10 | 11 | if (container) { 12 | const root = createRoot(container) 13 | 14 | root.render( 15 | 16 | 17 | 18 | 19 | , 20 | ) 21 | } else { 22 | throw new Error( 23 | "Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.", 24 | ) 25 | } 26 | 27 | // If you want to start measuring performance in your app, pass a function 28 | // to log results (for example: reportWebVitals(console.log)) 29 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 30 | reportWebVitals() 31 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { MetricType } from "web-vitals" 2 | 3 | const reportWebVitals = (onPerfEntry?: (metric: MetricType) => void) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | void import("web-vitals").then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => { 6 | onCLS(onPerfEntry) 7 | onINP(onPerfEntry) 8 | onFCP(onPerfEntry) 9 | onLCP(onPerfEntry) 10 | onTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom" 6 | -------------------------------------------------------------------------------- /packages/cra-template-redux-typescript/template/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 | "noFallthroughCasesInSwitch": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/cra-template-redux/README.md: -------------------------------------------------------------------------------- 1 | # cra-template-redux 2 | 3 | ![build status](https://img.shields.io/github/workflow/status/reduxjs/cra-template-redux/Tests/master?style=flat-square) 4 | [![npm version](https://img.shields.io/npm/v/cra-template-redux.svg?style=flat-square)](https://www.npmjs.com/package/cra-template-redux) 5 | [![npm downloads](https://img.shields.io/npm/dm/cra-template-redux.svg?style=flat-square)](https://www.npmjs.com/package/cra-template-redux) 6 | 7 | The official Redux+JS template for [Create React App](https://github.com/facebook/create-react-app) 8 | 9 | ## Usage 10 | 11 | To use this template within your project, add `--template redux` when creating a new app. 12 | 13 | For example: 14 | 15 | ```sh 16 | npx create-react-app my-app --template redux 17 | 18 | # or 19 | 20 | yarn create react-app my-app --template redux 21 | ``` 22 | 23 | ## TypeScript 24 | 25 | Use [cra-template-redux-typescript](https://github.com/reduxjs/cra-template-redux-typescript), which is based on this template 26 | 27 | ```sh 28 | npx create-react-app my-app --template redux-typescript 29 | ``` 30 | 31 | Cloning this repo pulls down the Redux template only; not a bundled and configured Create React App. 32 | 33 | For more information, please refer to: 34 | 35 | - [Getting Started](https://create-react-app.dev/docs/getting-started) – How to create a new app. 36 | - [User Guide](https://create-react-app.dev) – How to develop apps bootstrapped with Create React App. 37 | -------------------------------------------------------------------------------- /packages/cra-template-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-template-redux", 3 | "version": "2.0.0", 4 | "scripts": { 5 | "format": "prettier --write ./template/src/**/*.{js,jsx,css}", 6 | "lint": "eslint './template/src/**/*.{js,jsx}'", 7 | "test": "jest" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "create-react-app", 12 | "template", 13 | "javascript", 14 | "redux", 15 | "reduxjs", 16 | "react-redux" 17 | ], 18 | "description": "The official Redux+JS template for Create React App.", 19 | "main": "template.json", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/reduxjs/cra-template-redux.git", 23 | "directory": "packages/cra-template-redux" 24 | }, 25 | "license": "MIT", 26 | "engines": { 27 | "node": ">=8" 28 | }, 29 | "homepage": "https://github.com/reduxjs/redux-templates/tree/HEAD/packages/cra-template-redux#readme", 30 | "bugs": { 31 | "url": "https://github.com/reduxjs/redux-templates/issues" 32 | }, 33 | "files": [ 34 | "template", 35 | "template.json" 36 | ], 37 | "devDependencies": { 38 | "@babel/core": "^7.26.10", 39 | "@babel/preset-env": "^7.26.9", 40 | "@babel/preset-react": "^7.26.3", 41 | "@reduxjs/toolkit": "^2.6.1", 42 | "@testing-library/dom": "^10.4.0", 43 | "@testing-library/jest-dom": "^6.6.3", 44 | "@testing-library/react": "^16.3.0", 45 | "@testing-library/user-event": "^14.6.1", 46 | "babel-eslint": "^10.1.0", 47 | "babel-jest": "^29.7.0", 48 | "babel-preset-react-app": "^10.1.0", 49 | "eslint": "^9.23.0", 50 | "eslint-config-react-app": "^7.0.1", 51 | "eslint-plugin-import": "^2.31.0", 52 | "eslint-plugin-jsx-a11y": "^6.10.2", 53 | "eslint-plugin-prettier": "^5.2.6", 54 | "eslint-plugin-react": "^7.37.4", 55 | "eslint-plugin-react-hooks": "^5.2.0", 56 | "jest": "^29.7.0", 57 | "prettier": "^3.5.3", 58 | "react": "^19.1.0", 59 | "react-dom": "^19.1.0", 60 | "react-redux": "^9.2.0", 61 | "react-scripts": "^5.0.1", 62 | "react-test-renderer": "^19.1.0", 63 | "web-vitals": "^4.2.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "dependencies": { 4 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 5 | "@eslint/js": "^9.23.0", 6 | "@reduxjs/toolkit": "^2.6.1", 7 | "@testing-library/jest-dom": "^6.6.3", 8 | "@testing-library/react": "^16.2.0", 9 | "@testing-library/user-event": "^14.6.1", 10 | "eslint": "^9.23.0", 11 | "eslint-config-prettier": "^10.1.1", 12 | "eslint-plugin-jest": "^28.11.0", 13 | "eslint-plugin-react-hooks": "^5.2.0", 14 | "globals": "^16.0.0", 15 | "prettier": "^3.5.3", 16 | "react-redux": "^9.2.0", 17 | "typescript-eslint": "^8.28.0", 18 | "web-vitals": "^4.2.4" 19 | }, 20 | "eslintConfig": {}, 21 | "scripts": { 22 | "format": "prettier --write .", 23 | "format:check": "prettier --check .", 24 | "lint": "eslint .", 25 | "lint:fix": "eslint --fix ." 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App and Redux 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import prettierConfig from "eslint-config-prettier/flat" 3 | import jestPlugin from "eslint-plugin-jest" 4 | import reactPlugin from "eslint-plugin-react" 5 | import reactHooksPlugin from "eslint-plugin-react-hooks" 6 | import globals from "globals" 7 | import { config, configs } from "typescript-eslint" 8 | 9 | const eslintConfig = config( 10 | { 11 | name: "global-ignores", 12 | ignores: [ 13 | "**/*.snap", 14 | "**/dist/", 15 | "**/.yalc/", 16 | "**/build/", 17 | "**/temp/", 18 | "**/.temp/", 19 | "**/.tmp/", 20 | "**/.yarn/", 21 | "**/coverage/", 22 | ], 23 | }, 24 | { 25 | name: `${js.meta.name}/recommended`, 26 | ...js.configs.recommended, 27 | }, 28 | configs.strict, 29 | configs.stylistic, 30 | { 31 | name: `${jestPlugin.meta.name}/recommended`, 32 | ...jestPlugin.configs["flat/recommended"], 33 | }, 34 | { 35 | name: "eslint-plugin-react/jsx-runtime", 36 | ...reactPlugin.configs.flat["jsx-runtime"], 37 | }, 38 | reactHooksPlugin.configs["recommended-latest"], 39 | { 40 | name: "main", 41 | linterOptions: { 42 | reportUnusedDisableDirectives: 2, 43 | }, 44 | files: ["**/*.?(c|m)js?(x)"], 45 | languageOptions: { 46 | ecmaVersion: 2020, 47 | globals: globals.browser, 48 | }, 49 | }, 50 | 51 | prettierConfig, 52 | ) 53 | 54 | export default eslintConfig 55 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/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 | 25 | .yalc/ 26 | yalc.lock 27 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/cra-template-redux/template/public/favicon.ico -------------------------------------------------------------------------------- /packages/cra-template-redux/template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Redux App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/cra-template-redux/template/public/logo192.png -------------------------------------------------------------------------------- /packages/cra-template-redux/template/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/cra-template-redux/template/public/logo512.png -------------------------------------------------------------------------------- /packages/cra-template-redux/template/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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-float infinite 3s ease-in-out; 13 | } 14 | } 15 | 16 | .App-header { 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | font-size: calc(10px + 2vmin); 23 | } 24 | 25 | .App-link { 26 | color: rgb(112, 76, 182); 27 | } 28 | 29 | @keyframes App-logo-float { 30 | 0% { 31 | transform: translateY(0); 32 | } 33 | 50% { 34 | transform: translateY(10px); 35 | } 36 | 100% { 37 | transform: translateY(0px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css" 2 | import { Counter } from "./features/counter/Counter" 3 | import { Quotes } from "./features/quotes/Quotes" 4 | import logo from "./logo.svg" 5 | 6 | export const App = () => ( 7 |
8 |
9 | logo 10 | 11 |

12 | Edit src/App.tsx and save to reload. 13 |

14 | 15 | 16 | Learn 17 | 23 | React 24 | 25 | , 26 | 32 | Redux 33 | 34 | , 35 | 41 | Redux Toolkit 42 | 43 | , 44 | 50 | React Redux 51 | 52 | , and 53 | 59 | Reselect 60 | 61 | 62 |
63 |
64 | ) 65 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/app/createAppSlice.js: -------------------------------------------------------------------------------- 1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" 2 | 3 | // `buildCreateSlice` allows us to create a slice with async thunks. 4 | export const createAppSlice = buildCreateSlice({ 5 | creators: { asyncThunk: asyncThunkCreator }, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/app/store.js: -------------------------------------------------------------------------------- 1 | import { combineSlices, configureStore } from "@reduxjs/toolkit" 2 | import { setupListeners } from "@reduxjs/toolkit/query" 3 | import { counterSlice } from "../features/counter/counterSlice" 4 | import { quotesApiSlice } from "../features/quotes/quotesApiSlice" 5 | 6 | // `combineSlices` automatically combines the reducers using 7 | // their `reducerPath`s, therefore we no longer need to call `combineReducers`. 8 | const rootReducer = combineSlices(counterSlice, quotesApiSlice) 9 | 10 | // The store setup is wrapped in `makeStore` to allow reuse 11 | // when setting up tests that need the same store config 12 | export const makeStore = preloadedState => { 13 | const store = configureStore({ 14 | reducer: rootReducer, 15 | // Adding the api middleware enables caching, invalidation, polling, 16 | // and other useful features of `rtk-query`. 17 | middleware: getDefaultMiddleware => { 18 | return getDefaultMiddleware().concat(quotesApiSlice.middleware) 19 | }, 20 | preloadedState, 21 | }) 22 | // configure listeners using the provided defaults 23 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors 24 | setupListeners(store.dispatch) 25 | return store 26 | } 27 | 28 | export const store = makeStore() 29 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/counter/Counter.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { useDispatch, useSelector } from "react-redux" 3 | import styles from "./Counter.module.css" 4 | import { 5 | decrement, 6 | increment, 7 | incrementAsync, 8 | incrementByAmount, 9 | incrementIfOdd, 10 | selectCount, 11 | selectStatus, 12 | } from "./counterSlice" 13 | 14 | export const Counter = () => { 15 | const dispatch = useDispatch() 16 | const count = useSelector(selectCount) 17 | const status = useSelector(selectStatus) 18 | const [incrementAmount, setIncrementAmount] = useState("2") 19 | 20 | const incrementValue = Number(incrementAmount) || 0 21 | 22 | return ( 23 |
24 |
25 | 32 | 35 | 42 |
43 |
44 | { 50 | setIncrementAmount(e.target.value) 51 | }} 52 | /> 53 | 59 | 68 | 76 |
77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/counter/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: "Courier New", Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .asyncButton { 58 | composes: button; 59 | position: relative; 60 | } 61 | 62 | .asyncButton:after { 63 | content: ""; 64 | background-color: rgba(112, 76, 182, 0.15); 65 | display: block; 66 | position: absolute; 67 | width: 100%; 68 | height: 100%; 69 | left: 0; 70 | top: 0; 71 | opacity: 0; 72 | transition: 73 | width 1s linear, 74 | opacity 0.5s ease 1s; 75 | } 76 | 77 | .asyncButton:active:after { 78 | width: 0%; 79 | opacity: 1; 80 | transition: 0s; 81 | } 82 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/counter/counterAPI.js: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export const fetchCount = (amount = 1) => 3 | new Promise(resolve => 4 | setTimeout(() => { 5 | resolve({ data: amount }) 6 | }, 500), 7 | ) 8 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/counter/counterSlice.test.js: -------------------------------------------------------------------------------- 1 | import { makeStore } from "../../app/store" 2 | import { 3 | counterSlice, 4 | decrement, 5 | increment, 6 | incrementByAmount, 7 | selectCount, 8 | } from "./counterSlice" 9 | 10 | describe("counter reducer", () => { 11 | const initialState = { 12 | value: 3, 13 | status: "idle", 14 | } 15 | 16 | let store = makeStore() 17 | 18 | beforeEach(() => { 19 | store = makeStore({ counter: initialState }) 20 | }) 21 | 22 | it("should handle initial state", () => { 23 | expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ 24 | value: 0, 25 | status: "idle", 26 | }) 27 | }) 28 | 29 | it("should handle increment", () => { 30 | expect(selectCount(store.getState())).toBe(3) 31 | 32 | store.dispatch(increment()) 33 | 34 | expect(selectCount(store.getState())).toBe(4) 35 | }) 36 | 37 | it("should handle decrement", () => { 38 | expect(selectCount(store.getState())).toBe(3) 39 | 40 | store.dispatch(decrement()) 41 | 42 | expect(selectCount(store.getState())).toBe(2) 43 | }) 44 | 45 | it("should handle incrementByAmount", () => { 46 | expect(selectCount(store.getState())).toBe(3) 47 | 48 | store.dispatch(incrementByAmount(2)) 49 | 50 | expect(selectCount(store.getState())).toBe(5) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/quotes/Quotes.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import styles from "./Quotes.module.css" 3 | import { useGetQuotesQuery } from "./quotesApiSlice" 4 | 5 | const options = [5, 10, 20, 30] 6 | 7 | export const Quotes = () => { 8 | const [numberOfQuotes, setNumberOfQuotes] = useState(10) 9 | // Using a query hook automatically fetches data and returns query values 10 | const { data, isError, isLoading, isSuccess } = 11 | useGetQuotesQuery(numberOfQuotes) 12 | 13 | if (isError) { 14 | return ( 15 |
16 |

There was an error!!!

17 |
18 | ) 19 | } 20 | 21 | if (isLoading) { 22 | return ( 23 |
24 |

Loading...

25 |
26 | ) 27 | } 28 | 29 | if (isSuccess) { 30 | return ( 31 |
32 |

Select the Quantity of Quotes to Fetch:

33 | 46 | {data.quotes.map(({ author, quote, id }) => ( 47 |
48 | “{quote}” 49 |
50 | {author} 51 |
52 |
53 | ))} 54 |
55 | ) 56 | } 57 | 58 | return null 59 | } 60 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/quotes/Quotes.module.css: -------------------------------------------------------------------------------- 1 | .select { 2 | font-size: 25px; 3 | padding: 5px; 4 | padding-top: 2px; 5 | padding-bottom: 2px; 6 | size: 50; 7 | outline: none; 8 | border: 2px solid transparent; 9 | color: rgb(112, 76, 182); 10 | cursor: pointer; 11 | background-color: rgba(112, 76, 182, 0.1); 12 | border-radius: 5px; 13 | transition: all 0.15s; 14 | } 15 | 16 | .container { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/features/quotes/quotesApiSlice.js: -------------------------------------------------------------------------------- 1 | // Need to use the React-specific entry point to import `createApi` 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" 3 | 4 | // Define a service using a base URL and expected endpoints 5 | export const quotesApiSlice = createApi({ 6 | baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), 7 | reducerPath: "quotesApi", 8 | // Tag types are used for caching and invalidation. 9 | tagTypes: ["Quotes"], 10 | endpoints: build => ({ 11 | // Supply generics for the return type (in this case `QuotesApiResponse`) 12 | // and the expected query argument. If there is no argument, use `void` 13 | // for the argument type instead. 14 | getQuotes: build.query({ 15 | query: (limit = 10) => `?limit=${limit.toString()}`, 16 | // `providesTags` determines which 'tag' is attached to the 17 | // cached data returned by the query. 18 | providesTags: (_result, _error, id) => [{ type: "Quotes", id }], 19 | }), 20 | }), 21 | }) 22 | 23 | // Hooks are auto-generated by RTK-Query 24 | // Same as `quotesApiSlice.endpoints.getQuotes.useQuery` 25 | export const { useGetQuotesQuery } = quotesApiSlice 26 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: 12 | source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { Provider } from "react-redux" 4 | import { App } from "./App" 5 | import { store } from "./app/store" 6 | import "./index.css" 7 | import reportWebVitals from "./reportWebVitals" 8 | 9 | const container = document.getElementById("root") 10 | 11 | if (container) { 12 | const root = createRoot(container) 13 | 14 | root.render( 15 | 16 | 17 | 18 | 19 | , 20 | ) 21 | } else { 22 | throw new Error( 23 | "Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.", 24 | ) 25 | } 26 | 27 | // If you want to start measuring performance in your app, pass a function 28 | // to log results (for example: reportWebVitals(console.log)) 29 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 30 | reportWebVitals() 31 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | void import("web-vitals").then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => { 4 | onCLS(onPerfEntry) 5 | onINP(onPerfEntry) 6 | onFCP(onPerfEntry) 7 | onLCP(onPerfEntry) 8 | onTTFB(onPerfEntry) 9 | }) 10 | } 11 | } 12 | 13 | export default reportWebVitals 14 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom" 6 | -------------------------------------------------------------------------------- /packages/cra-template-redux/template/src/utils/test-utils.jsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react" 2 | import { userEvent } from "@testing-library/user-event" 3 | import { Provider } from "react-redux" 4 | import { makeStore } from "../app/store" 5 | 6 | /** 7 | * Renders the given React element with Redux Provider and custom store. 8 | * This function is useful for testing components that are connected to the Redux store. 9 | * 10 | * @param ui - The React component or element to render. 11 | * @param extendedRenderOptions - Optional configuration options for rendering. This includes `preloadedState` for initial Redux state and `store` for a specific Redux store instance. Any additional properties are passed to React Testing Library's render function. 12 | * @returns An object containing the Redux store used in the render, User event API for simulating user interactions in tests, and all of React Testing Library's query functions for testing the component. 13 | */ 14 | export const renderWithProviders = (ui, extendedRenderOptions = {}) => { 15 | const { 16 | preloadedState = {}, 17 | // Automatically create a store instance if no store was passed in 18 | store = makeStore(preloadedState), 19 | userEventOptions, 20 | ...renderOptions 21 | } = extendedRenderOptions 22 | 23 | const Wrapper = ({ children }) => ( 24 | {children} 25 | ) 26 | 27 | // Return an object with the store and all of RTL's query functions 28 | return { 29 | store, 30 | user: userEvent.setup(userEventOptions), 31 | ...render(ui, { wrapper: Wrapper, ...renderOptions }), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | .yarn/ 6 | package-lock.json 7 | yarn.lock 8 | 9 | # Expo 10 | .expo/ 11 | dist/ 12 | web-build/ 13 | 14 | # Native 15 | *.orig.* 16 | *.jks 17 | *.p8 18 | *.p12 19 | *.key 20 | *.mobileprovision 21 | 22 | # Metro 23 | .metro-health-check* 24 | 25 | # debug 26 | npm-debug.* 27 | yarn-debug.* 28 | yarn-error.* 29 | 30 | # macOS 31 | .DS_Store 32 | *.pem 33 | 34 | # local env files 35 | .env*.local 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | 40 | # IDE 41 | .vscode 42 | 43 | .yalc/ 44 | yalc.lock 45 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/App.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { Provider } from "react-redux" 3 | import { store } from "./src/app/store" 4 | import { Main } from "./src/Main" 5 | 6 | export const App = (): JSX.Element => ( 7 | 8 |
9 | 10 | ) 11 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Expo Template Redux TypeScript 2 | 3 | The official Redux+TS template for Expo. 4 | 5 | ## :arrow_forward: Usage 6 | 7 | ```sh 8 | npx create-expo my-app --template expo-template-redux-typescript 9 | ``` 10 | 11 | # Getting Started 12 | 13 | > **Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. 14 | 15 | ## Step 1: Start the Metro Server 16 | 17 | First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. 18 | 19 | To start Metro, run the following command from the _root_ of your React Native project: 20 | 21 | ```bash 22 | # using npm 23 | npm start 24 | ``` 25 | 26 | ```bash 27 | # OR using Yarn 28 | yarn start 29 | ``` 30 | 31 | ## Step 2: Start your Application 32 | 33 | Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: 34 | 35 | ### For Android 36 | 37 | ```bash 38 | # using npm 39 | npm run android 40 | ``` 41 | 42 | ```bash 43 | # OR using Yarn 44 | yarn android 45 | ``` 46 | 47 | ### For iOS 48 | 49 | ```bash 50 | # using npm 51 | npm run ios 52 | ``` 53 | 54 | ```bash 55 | # OR using Yarn 56 | yarn ios 57 | ``` 58 | 59 | If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. 60 | 61 | This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. 62 | 63 | ## Step 3: Modifying your App 64 | 65 | Now that you have successfully run the app, let's modify it. 66 | 67 | 1. Open `App.tsx` in your text editor of choice and edit some lines. 68 | 2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes! 69 | 70 | For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes! 71 | 72 | ## Congratulations! :tada: 73 | 74 | You've successfully run and modified your Expo App. :partying_face: 75 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-template-redux-typescript", 4 | "slug": "expo-template-redux-typescript", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | } 23 | }, 24 | "web": { 25 | "favicon": "./assets/favicon.png" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/expo-template-redux-typescript/assets/adaptive-icon.png -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/expo-template-redux-typescript/assets/favicon.png -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/expo-template-redux-typescript/assets/icon.png -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/assets/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/expo-template-redux-typescript/assets/splash-icon.png -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @import { ConfigFunction } from "@babel/core" */ 2 | /** @import { BabelPresetExpoOptions } from "babel-preset-expo" */ 3 | 4 | /** 5 | * @satisfies {ConfigFunction} 6 | */ 7 | const config = api => { 8 | api.cache.forever() 9 | 10 | return { 11 | presets: [ 12 | /** 13 | * @satisfies {['babel-preset-expo', BabelPresetExpoOptions?]} 14 | */ 15 | (["babel-preset-expo"]), 16 | ], 17 | } 18 | } 19 | 20 | module.exports = config 21 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import prettierConfig from "eslint-config-prettier/flat" 3 | import jestPlugin from "eslint-plugin-jest" 4 | import reactPlugin from "eslint-plugin-react" 5 | import reactHooksPlugin from "eslint-plugin-react-hooks" 6 | import globals from "globals" 7 | import { config, configs } from "typescript-eslint" 8 | 9 | const eslintConfig = config( 10 | { 11 | name: "global-ignores", 12 | ignores: [ 13 | "**/*.snap", 14 | "**/dist/", 15 | "**/.yalc/", 16 | "**/build/", 17 | "**/temp/", 18 | "**/.temp/", 19 | "**/.tmp/", 20 | "**/.yarn/", 21 | "**/coverage/", 22 | ], 23 | }, 24 | { 25 | name: `${js.meta.name}/recommended`, 26 | ...js.configs.recommended, 27 | }, 28 | configs.strictTypeChecked, 29 | configs.stylisticTypeChecked, 30 | { 31 | name: `${jestPlugin.meta.name}/recommended`, 32 | ...jestPlugin.configs["flat/recommended"], 33 | }, 34 | { 35 | name: "eslint-plugin-react/jsx-runtime", 36 | ...reactPlugin.configs.flat["jsx-runtime"], 37 | }, 38 | reactHooksPlugin.configs["recommended-latest"], 39 | { 40 | name: "main", 41 | linterOptions: { 42 | reportUnusedDisableDirectives: 2, 43 | }, 44 | languageOptions: { 45 | ecmaVersion: 2020, 46 | globals: globals.node, 47 | parserOptions: { 48 | projectService: true, 49 | tsconfigRootDir: import.meta.dirname, 50 | }, 51 | }, 52 | rules: { 53 | "no-undef": [0], 54 | "no-restricted-imports": [ 55 | 2, 56 | { 57 | paths: [ 58 | { 59 | name: "react-redux", 60 | importNames: ["useSelector", "useStore", "useDispatch"], 61 | message: 62 | "Please use pre-typed versions from `src/app/hooks.ts` instead.", 63 | }, 64 | ], 65 | }, 66 | ], 67 | "@typescript-eslint/consistent-type-definitions": [2, "type"], 68 | "@typescript-eslint/consistent-type-imports": [ 69 | 2, 70 | { 71 | prefer: "type-imports", 72 | fixStyle: "separate-type-imports", 73 | disallowTypeAnnotations: true, 74 | }, 75 | ], 76 | }, 77 | }, 78 | { 79 | name: "commonjs", 80 | files: ["metro.config.js"], 81 | languageOptions: { 82 | sourceType: "commonjs", 83 | }, 84 | rules: { 85 | "@typescript-eslint/no-require-imports": [ 86 | 0, 87 | [{ allow: [], allowAsImport: false }], 88 | ], 89 | }, 90 | }, 91 | 92 | prettierConfig, 93 | ) 94 | 95 | export default eslintConfig 96 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | build/ 12 | 13 | # Native 14 | *.orig.* 15 | *.jks 16 | *.p8 17 | *.p12 18 | *.key 19 | *.mobileprovision 20 | 21 | # Metro 22 | .metro-health-check* 23 | 24 | # debug 25 | npm-debug.* 26 | yarn-debug.* 27 | yarn-error.* 28 | 29 | # macOS 30 | .DS_Store 31 | *.pem 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | # testing 40 | /coverage 41 | 42 | # Yarn 43 | .yarn/* 44 | !.yarn/patches 45 | !.yarn/plugins 46 | !.yarn/releases 47 | !.yarn/sdks 48 | !.yarn/versions 49 | 50 | # IDE 51 | .vscode 52 | 53 | .yalc/ 54 | yalc.lock 55 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.gif" { 2 | const logo: number 3 | export default logo 4 | } 5 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/index.ts: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo" 2 | import { App } from "./App" 3 | 4 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 5 | // It also ensures that whether you load the app in Expo Go or in a native build, 6 | // the environment is set up appropriately 7 | registerRootComponent(App) 8 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/react-native" 2 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest" 2 | 3 | const config: Config = { 4 | preset: "jest-expo", 5 | verbose: true, 6 | /** 7 | * Without this we will get the following error: 8 | * `SyntaxError: Cannot use import statement outside a module` 9 | */ 10 | transformIgnorePatterns: [ 11 | "node_modules/(?!((jest-)?react-native|...|react-redux))", 12 | ], 13 | /** 14 | * React Native's `jest` preset includes a 15 | * [polyfill for `window`](https://github.com/facebook/react-native/blob/acb634bc9662c1103bc7c8ca83cfdc62516d0060/packages/react-native/jest/setup.js#L61-L66). 16 | * This polyfill causes React-Redux to use `useEffect` 17 | * instead of `useLayoutEffect` for the `useIsomorphicLayoutEffect` hook. 18 | * As a result, nested component updates may not be properly batched 19 | * when using the `connect` API, leading to potential issues. 20 | */ 21 | globals: { 22 | window: undefined, 23 | navigator: { 24 | product: "ReactNative", 25 | }, 26 | }, 27 | setupFilesAfterEnv: ["/jest-setup.ts"], 28 | fakeTimers: { 29 | enableGlobally: true, 30 | advanceTimers: true, 31 | }, 32 | } 33 | 34 | export default config 35 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/metro.base.config.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultConfig } from "expo/metro-config" 2 | import type { MetroConfig } from "metro-config" 3 | import { mergeConfig } from "metro-config" 4 | 5 | const config: MetroConfig = mergeConfig(getDefaultConfig(__dirname)) 6 | 7 | export { config } 8 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/metro.config.js: -------------------------------------------------------------------------------- 1 | require("ts-node/register") 2 | 3 | const { config } = require("./metro.base.config.ts") 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-template-redux-typescript", 3 | "version": "1.0.0", 4 | "description": "The official Redux+TS template for Expo", 5 | "main": "index.ts", 6 | "scripts": { 7 | "android": "expo start --android", 8 | "build": "expo prebuild -p android && react-native bundle --entry-file index.ts --bundle-output build/bundle.js --platform android --assets-dest build/assets", 9 | "format:check": "prettier --check .", 10 | "format": "prettier --write .", 11 | "ios": "expo start --ios", 12 | "lint:fix": "eslint --fix .", 13 | "lint": "eslint .", 14 | "start": "expo start", 15 | "test": "jest", 16 | "type-check": "tsc --noEmit", 17 | "web": "expo start --web" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/reduxjs/redux-templates.git", 22 | "directory": "packages/expo-template-redux-typescript" 23 | }, 24 | "dependencies": { 25 | "@reduxjs/toolkit": "^2.6.1", 26 | "expo": "~52.0.42", 27 | "expo-status-bar": "~2.0.1", 28 | "react": "19.0.0", 29 | "react-native": "0.78.2", 30 | "react-redux": "^9.2.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.26.10", 34 | "@eslint/js": "^9.23.0", 35 | "@react-native-community/cli": "^18.0.0", 36 | "@react-native-community/cli-platform-android": "^17.0.0", 37 | "@react-native-community/cli-platform-ios": "^17.0.0", 38 | "@testing-library/react-native": "^13.2.0", 39 | "@types/babel__core": "^7.20.5", 40 | "@types/jest": "^29.5.14", 41 | "@types/node": "^22.14.0", 42 | "@types/react": "^19.1.0", 43 | "@types/react-test-renderer": "^19.1.0", 44 | "eslint": "^9.23.0", 45 | "eslint-config-prettier": "^10.1.1", 46 | "eslint-plugin-jest": "^28.11.0", 47 | "eslint-plugin-react": "^7.37.4", 48 | "eslint-plugin-react-hooks": "^5.2.0", 49 | "globals": "^16.0.0", 50 | "jest": "^29.7.0", 51 | "jest-expo": "^52.0.6", 52 | "prettier": "^3.5.3", 53 | "react-test-renderer": "19.0.0", 54 | "ts-node": "^10.9.2", 55 | "typescript": "^5.8.2", 56 | "typescript-eslint": "^8.29.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/app/createAppSlice.ts: -------------------------------------------------------------------------------- 1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" 2 | 3 | // `buildCreateSlice` allows us to create a slice with async thunks. 4 | export const createAppSlice = buildCreateSlice({ 5 | creators: { asyncThunk: asyncThunkCreator }, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks. 2 | // These imports are restricted elsewhere to ensure consistent 3 | // usage of typed hooks throughout the application. 4 | // We disable the ESLint rule here because this is the designated place 5 | // for importing and re-exporting the typed versions of hooks. 6 | /* eslint-disable no-restricted-imports */ 7 | import { useEffect } from "react" 8 | import { Animated, useAnimatedValue, useWindowDimensions } from "react-native" 9 | import { useDispatch, useSelector } from "react-redux" 10 | import type { AppDispatch, RootState } from "./store" 11 | 12 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 13 | export const useAppDispatch = useDispatch.withTypes() 14 | export const useAppSelector = useSelector.withTypes() 15 | 16 | /** 17 | * Custom React hook for calculating viewport units 18 | * based on the current window dimensions. 19 | * 20 | * @returns An object containing the calculated viewport heigh and width values. 21 | */ 22 | export const useViewportUnits = (): { vh: number; vw: number } => { 23 | const { width, height } = useWindowDimensions() 24 | 25 | const vh = height / 100 26 | const vw = width / 100 27 | 28 | return { vh, vw } 29 | } 30 | 31 | /** 32 | * Custom React hook for creating a bounce animation effect. 33 | * 34 | * @param value - The maximum height to which the object should bounce. Defaults to 10 if not provided. 35 | * @returns The {@linkcode Animated.Value} object that can be used to drive animations. 36 | */ 37 | export const useBounceAnimation = (value = 10): Animated.Value => { 38 | const bounce = useAnimatedValue(0) 39 | 40 | bounce.interpolate({ 41 | inputRange: [-300, -100, 0, 100, 101], 42 | outputRange: [300, 0, 1, 0, 0], 43 | }) 44 | 45 | useEffect(() => { 46 | Animated.loop( 47 | Animated.sequence([ 48 | Animated.timing(bounce, { 49 | toValue: value, 50 | duration: 1500, 51 | useNativeDriver: true, 52 | }), 53 | Animated.timing(bounce, { 54 | toValue: 0, 55 | duration: 1500, 56 | useNativeDriver: true, 57 | }), 58 | ]), 59 | ).start() 60 | }, [bounce, value]) 61 | 62 | return bounce 63 | } 64 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ThunkAction } from "@reduxjs/toolkit" 2 | import { combineSlices, configureStore } from "@reduxjs/toolkit" 3 | import { setupListeners } from "@reduxjs/toolkit/query" 4 | import { counterSlice } from "../features/counter/counterSlice" 5 | import { quotesApiSlice } from "../features/quotes/quotesApiSlice" 6 | 7 | // `combineSlices` automatically combines the reducers using 8 | // their `reducerPath`s, therefore we no longer need to call `combineReducers`. 9 | const rootReducer = combineSlices(counterSlice, quotesApiSlice) 10 | // Infer the `RootState` type from the root reducer 11 | export type RootState = ReturnType 12 | 13 | // The store setup is wrapped in `makeStore` to allow reuse 14 | // when setting up tests that need the same store config 15 | export const makeStore = (preloadedState?: Partial) => { 16 | const store = configureStore({ 17 | reducer: rootReducer, 18 | // Adding the api middleware enables caching, invalidation, polling, 19 | // and other useful features of `rtk-query`. 20 | middleware: getDefaultMiddleware => { 21 | return getDefaultMiddleware().concat(quotesApiSlice.middleware) 22 | }, 23 | preloadedState, 24 | }) 25 | // configure listeners using the provided defaults 26 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors 27 | setupListeners(store.dispatch) 28 | return store 29 | } 30 | 31 | export const store = makeStore() 32 | 33 | // Infer the type of `store` 34 | export type AppStore = typeof store 35 | // Infer the `AppDispatch` type from the store itself 36 | export type AppDispatch = AppStore["dispatch"] 37 | export type AppThunk = ThunkAction< 38 | ThunkReturnType, 39 | RootState, 40 | unknown, 41 | Action 42 | > 43 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/components/AsyncButton.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, PropsWithChildren } from "react" 2 | import type { 3 | GestureResponderEvent, 4 | PressableProps, 5 | ViewStyle, 6 | } from "react-native" 7 | import { 8 | Animated, 9 | Pressable, 10 | StyleSheet, 11 | View, 12 | useAnimatedValue, 13 | } from "react-native" 14 | 15 | type AsyncButtonProps = PressableProps & PropsWithChildren 16 | 17 | export const AsyncButton = ({ 18 | onPress, 19 | style, 20 | children, 21 | ...restProps 22 | }: AsyncButtonProps): JSX.Element => { 23 | const progress = useAnimatedValue(0) 24 | const opacity = useAnimatedValue(1) 25 | 26 | const _onPress = (e: GestureResponderEvent) => { 27 | progress.setValue(0) 28 | opacity.setValue(1) 29 | 30 | onPress?.(e) 31 | 32 | Animated.timing(progress, { 33 | toValue: 1, 34 | duration: 1000, 35 | useNativeDriver: false, 36 | }).start(({ finished }) => { 37 | if (!finished) { 38 | return 39 | } 40 | 41 | Animated.timing(opacity, { 42 | toValue: 0, 43 | duration: 200, 44 | useNativeDriver: false, 45 | }).start() 46 | }) 47 | } 48 | 49 | const progressInterpolate = progress.interpolate({ 50 | inputRange: [0, 1], 51 | outputRange: ["0%", "100%"], 52 | extrapolate: "clamp", 53 | }) 54 | 55 | const progressStyle: Animated.WithAnimatedObject = { 56 | width: progressInterpolate, 57 | opacity, 58 | } 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | {children} 66 | 67 | ) 68 | } 69 | 70 | const styles = StyleSheet.create({ 71 | progress: { 72 | position: "absolute", 73 | top: 0, 74 | bottom: 0, 75 | left: 0, 76 | backgroundColor: "rgba(112,76,182, 0.15)", 77 | }, 78 | }) 79 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { useCallback } from "react" 3 | import type { TouchableOpacityProps } from "react-native" 4 | import { Alert, Linking, TouchableOpacity } from "react-native" 5 | 6 | type ExternalLinkProps = TouchableOpacityProps & { 7 | url: string 8 | } 9 | 10 | export const ExternalLink = ({ 11 | url, 12 | ...touchableOpacityProps 13 | }: ExternalLinkProps): JSX.Element => { 14 | const onPress = useCallback(async () => { 15 | const supported = await Linking.canOpenURL(url) 16 | 17 | if (supported) { 18 | await Linking.openURL(url) 19 | } else { 20 | Alert.alert(`Don't know how to open this URL: ${url}`) 21 | } 22 | }, [url]) 23 | 24 | return ( 25 | { 29 | void onPress() 30 | }} 31 | /> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/components/ExternalLinks.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { Fragment } from "react" 3 | import { StyleSheet, Text, useColorScheme, View } from "react-native" 4 | import { Colors } from "../constants/Colors" 5 | import { ExternalLink } from "./ExternalLink" 6 | 7 | export type Link = { 8 | id: number 9 | title: string 10 | url: string 11 | description: string 12 | } 13 | 14 | type ExternalLinksProps = { 15 | links: Link[] 16 | } 17 | 18 | export const ExternalLinks = ({ links }: ExternalLinksProps): JSX.Element => { 19 | const isDarkMode = useColorScheme() === "dark" 20 | 21 | return ( 22 | 23 | {links.map(({ description, id, title, url }) => ( 24 | 25 | 31 | 32 | {title} 33 | 39 | {description} 40 | 41 | 42 | 43 | ))} 44 | 45 | ) 46 | } 47 | 48 | const styles = StyleSheet.create({ 49 | container: { 50 | marginTop: 32, 51 | paddingHorizontal: 24, 52 | }, 53 | linkContainer: { 54 | flexWrap: "wrap", 55 | flexDirection: "row", 56 | justifyContent: "space-between", 57 | alignItems: "center", 58 | paddingVertical: 8, 59 | }, 60 | link: { 61 | flex: 2, 62 | fontSize: 18, 63 | fontWeight: "400", 64 | color: Colors.primary, 65 | }, 66 | description: { 67 | flex: 3, 68 | paddingVertical: 16, 69 | fontWeight: "400", 70 | fontSize: 18, 71 | }, 72 | separator: { 73 | height: StyleSheet.hairlineWidth, 74 | }, 75 | }) 76 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { Animated, StyleSheet, View, useColorScheme } from "react-native" 3 | import { useBounceAnimation, useViewportUnits } from "../app/hooks" 4 | import { Colors } from "../constants/Colors" 5 | import logo from "./logo.gif" 6 | 7 | export const Header = (): JSX.Element => { 8 | const isDarkMode = useColorScheme() === "dark" 9 | const { vh } = useViewportUnits() 10 | const bounce = useBounceAnimation() 11 | const height = 40 * vh 12 | 13 | return ( 14 | 20 | 25 | 26 | ) 27 | } 28 | 29 | const styles = StyleSheet.create({ 30 | container: { 31 | flexDirection: "row", 32 | justifyContent: "center", 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/components/Section.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, PropsWithChildren } from "react" 2 | import { StyleSheet, Text, View, useColorScheme } from "react-native" 3 | import { Colors } from "../constants/Colors" 4 | 5 | type SectionProps = PropsWithChildren<{ 6 | title: string 7 | }> 8 | 9 | export const Section = ({ children, title }: SectionProps): JSX.Element => { 10 | const isDarkMode = useColorScheme() === "dark" 11 | 12 | return ( 13 | 14 | 20 | {title} 21 | 22 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | sectionContainer: { 36 | marginTop: 32, 37 | paddingHorizontal: 24, 38 | }, 39 | sectionTitle: { 40 | fontSize: 24, 41 | fontWeight: "600", 42 | }, 43 | sectionDescription: { 44 | marginTop: 8, 45 | fontSize: 18, 46 | fontWeight: "400", 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/components/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/expo-template-redux-typescript/src/components/logo.gif -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | type AllColors = { 2 | primary: string 3 | white: string 4 | lighter: string 5 | light: string 6 | dark: string 7 | darker: string 8 | black: string 9 | } 10 | 11 | export const Colors: AllColors = { 12 | light: "#DAE1E7", 13 | lighter: "#F3F3F3", 14 | white: "#FFF", 15 | dark: "#444", 16 | darker: "#222", 17 | black: "#000", 18 | primary: "#1292B4", 19 | } 20 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export const fetchCount = (amount = 1): Promise<{ data: number }> => 3 | new Promise<{ data: number }>(resolve => 4 | setTimeout(() => { 5 | resolve({ data: amount }) 6 | }, 500), 7 | ) 8 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/features/counter/counterSlice.test.ts: -------------------------------------------------------------------------------- 1 | import { makeStore } from "../../app/store" 2 | import type { CounterSliceState } from "./counterSlice" 3 | import { 4 | counterSlice, 5 | decrement, 6 | increment, 7 | incrementByAmount, 8 | selectCount, 9 | } from "./counterSlice" 10 | 11 | describe("counter reducer", () => { 12 | const initialState: CounterSliceState = { 13 | value: 3, 14 | status: "idle", 15 | } 16 | 17 | let store = makeStore() 18 | 19 | beforeEach(() => { 20 | store = makeStore({ counter: initialState }) 21 | }) 22 | 23 | it("should handle initial state", () => { 24 | expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ 25 | value: 0, 26 | status: "idle", 27 | }) 28 | }) 29 | 30 | it("should handle increment", () => { 31 | expect(selectCount(store.getState())).toBe(3) 32 | 33 | store.dispatch(increment()) 34 | 35 | expect(selectCount(store.getState())).toBe(4) 36 | }) 37 | 38 | it("should handle decrement", () => { 39 | expect(selectCount(store.getState())).toBe(3) 40 | 41 | store.dispatch(decrement()) 42 | 43 | expect(selectCount(store.getState())).toBe(2) 44 | }) 45 | 46 | it("should handle incrementByAmount", () => { 47 | expect(selectCount(store.getState())).toBe(3) 48 | 49 | store.dispatch(incrementByAmount(2)) 50 | 51 | expect(selectCount(store.getState())).toBe(5) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/features/quotes/quotesApiSlice.ts: -------------------------------------------------------------------------------- 1 | // Need to use the React-specific entry point to import `createApi` 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" 3 | 4 | type Quote = { 5 | id: number 6 | quote: string 7 | author: string 8 | } 9 | 10 | type QuotesApiResponse = { 11 | quotes: Quote[] 12 | total: number 13 | skip: number 14 | limit: number 15 | } 16 | 17 | // Define a service using a base URL and expected endpoints 18 | export const quotesApiSlice = createApi({ 19 | baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), 20 | reducerPath: "quotesApi", 21 | // Tag types are used for caching and invalidation. 22 | tagTypes: ["Quotes"], 23 | endpoints: build => ({ 24 | // Supply generics for the return type (in this case `QuotesApiResponse`) 25 | // and the expected query argument. If there is no argument, use `void` 26 | // for the argument type instead. 27 | getQuotes: build.query({ 28 | query: (limit = 10) => `?limit=${limit.toString()}`, 29 | // `providesTags` determines which 'tag' is attached to the 30 | // cached data returned by the query. 31 | providesTags: (_result, _error, id) => [{ type: "Quotes", id }], 32 | }), 33 | }), 34 | }) 35 | 36 | // Hooks are auto-generated by RTK-Query 37 | // Same as `quotesApiSlice.endpoints.getQuotes.useQuery` 38 | export const { useGetQuotesQuery } = quotesApiSlice 39 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/src/utils/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from "@testing-library/react-native" 2 | import { render, userEvent } from "@testing-library/react-native" 3 | import type { PropsWithChildren, ReactElement } from "react" 4 | import { Provider } from "react-redux" 5 | import type { AppStore, RootState } from "../app/store" 6 | import { makeStore } from "../app/store" 7 | 8 | /** 9 | * This type extends the default options for 10 | * React Testing Library's render function. It allows for 11 | * additional configuration such as specifying an initial Redux state and 12 | * a custom store instance. 13 | */ 14 | type ExtendedRenderOptions = Omit & { 15 | /** 16 | * Defines a specific portion or the entire initial state for the Redux store. 17 | * This is particularly useful for initializing the state in a 18 | * controlled manner during testing, allowing components to be rendered 19 | * with predetermined state conditions. 20 | */ 21 | preloadedState?: Partial 22 | 23 | /** 24 | * Allows the use of a specific Redux store instance instead of a 25 | * default or global store. This flexibility is beneficial when 26 | * testing components with unique store requirements or when isolating 27 | * tests from a global store state. The custom store should be configured 28 | * to match the structure and middleware of the store used by the application. 29 | * 30 | * @default makeStore(preloadedState) 31 | */ 32 | store?: AppStore 33 | } 34 | 35 | /** 36 | * Renders the given React element with Redux Provider and custom store. 37 | * This function is useful for testing components that are connected to the Redux store. 38 | * 39 | * @param ui - The React component or element to render. 40 | * @param extendedRenderOptions - Optional configuration options for rendering. This includes `preloadedState` for initial Redux state and `store` for a specific Redux store instance. Any additional properties are passed to React Testing Library's render function. 41 | * @returns An object containing the Redux store used in the render, User event API for simulating user interactions in tests, and all of React Testing Library's query functions for testing the component. 42 | */ 43 | export const renderWithProviders = ( 44 | ui: ReactElement, 45 | extendedRenderOptions: ExtendedRenderOptions = {}, 46 | ) => { 47 | const { 48 | preloadedState = {}, 49 | // Automatically create a store instance if no store was passed in 50 | store = makeStore(preloadedState), 51 | ...renderOptions 52 | } = extendedRenderOptions 53 | 54 | const Wrapper = ({ children }: PropsWithChildren) => ( 55 | {children} 56 | ) 57 | 58 | // Return an object with the store and all of RTL's query functions 59 | return { 60 | store, 61 | user: userEvent.setup(), 62 | ...render(ui, { wrapper: Wrapper, ...renderOptions }), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/expo-template-redux-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | ios/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | .yarn/ 43 | template/package-lock.json 44 | template/yarn.lock 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/ 52 | 53 | **/fastlane/report.xml 54 | **/fastlane/Preview.html 55 | **/fastlane/screenshots 56 | **/fastlane/test_output 57 | 58 | # Bundle artifact 59 | *.jsbundle 60 | 61 | # Ruby / CocoaPods 62 | /ios/Pods/ 63 | /vendor/bundle/ 64 | 65 | # Temporary files created by Metro to check the health of the file watcher 66 | .metro-health-check* 67 | 68 | # testing 69 | /coverage 70 | 71 | #IDE 72 | .vscode 73 | 74 | .yalc/ 75 | yalc.lock 76 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-template-redux-typescript", 3 | "version": "0.0.1", 4 | "description": "The official Redux+TS template for React Native", 5 | "files": [ 6 | "template", 7 | "template.config.js" 8 | ], 9 | "homepage": "https://github.com/reduxjs/redux-templates/tree/HEAD/packages/react-native-template-redux-typescript#readme", 10 | "bugs": { 11 | "url": "https://github.com/reduxjs/redux-templates/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/reduxjs/redux-templates.git", 16 | "directory": "packages/react-native-template-redux-typescript" 17 | }, 18 | "scripts": { 19 | "format": "prettier --write \"./**/*.{js,ts,jsx,tsx,mjs,cts,md}\"", 20 | "lint": "eslint './template/src/**/*.{ts,tsx,js,jsx}'", 21 | "test": "jest", 22 | "type-check": "tsc" 23 | }, 24 | "keywords": [ 25 | "react-native", 26 | "typescript", 27 | "jest", 28 | "template", 29 | "boilerplate" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration settings for a React-Native template. 3 | * 4 | * @typedef {Object} TemplateConfig 5 | * @property {string} placeholderName A unique identifier or name for the placeholder within the template. 6 | * @property {string} templateDir The file path to the directory where the template is located. 7 | * @property {string} [postInitScript] The path to a script that should be executed after the template initialization is complete (optional). 8 | * @property {string} [titlePlaceholder] A placeholder for the title within the template (optional). 9 | */ 10 | 11 | /** 12 | * @type TemplateConfig 13 | */ 14 | const templateConfig = { 15 | placeholderName: "ReactNativeReduxTemplate", 16 | templateDir: "./template", 17 | titlePlaceholder: "react-native-template", 18 | } 19 | 20 | module.exports = templateConfig 21 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | dist/ 29 | .idea 30 | .gradle 31 | local.properties 32 | *.iml 33 | *.hprof 34 | .cxx/ 35 | *.keystore 36 | !debug.keystore 37 | .kotlin/ 38 | 39 | # node.js 40 | # 41 | node_modules/ 42 | npm-debug.log 43 | yarn-error.log 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://docs.fastlane.tools/best-practices/source-control/ 51 | 52 | **/fastlane/report.xml 53 | **/fastlane/Preview.html 54 | **/fastlane/screenshots 55 | **/fastlane/test_output 56 | 57 | # Bundle artifact 58 | *.jsbundle 59 | 60 | # Ruby / CocoaPods 61 | **/Pods/ 62 | /vendor/bundle/ 63 | 64 | # Temporary files created by Metro to check the health of the file watcher 65 | .metro-health-check* 66 | 67 | # testing 68 | /coverage 69 | 70 | # Yarn 71 | .yarn/* 72 | !.yarn/patches 73 | !.yarn/plugins 74 | !.yarn/releases 75 | !.yarn/sdks 76 | !.yarn/versions 77 | 78 | #IDE 79 | .vscode 80 | 81 | .yalc/ 82 | yalc.lock 83 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/App.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { 3 | SafeAreaView, 4 | ScrollView, 5 | StatusBar, 6 | StyleSheet, 7 | Text, 8 | View, 9 | useColorScheme, 10 | } from "react-native" 11 | import { 12 | DebugInstructions, 13 | HermesBadge, 14 | LearnMoreLinks, 15 | ReloadInstructions, 16 | } from "react-native/Libraries/NewAppScreen" 17 | import { Header } from "./src/components/Header" 18 | import { LearnReduxLinks } from "./src/components/LearnReduxLinks" 19 | import { Section } from "./src/components/Section" 20 | import { TypedColors } from "./src/constants/TypedColors" 21 | import { Counter } from "./src/features/counter/Counter" 22 | import { Quotes } from "./src/features/quotes/Quotes" 23 | 24 | export const App = (): JSX.Element => { 25 | const isDarkMode = useColorScheme() === "dark" 26 | 27 | const backgroundStyle = { 28 | backgroundColor: isDarkMode ? TypedColors.darker : TypedColors.lighter, 29 | } 30 | 31 | return ( 32 | 33 | 37 | 41 |
42 | 43 | 48 | 49 | 50 |
51 | Edit App.tsx to change this 52 | screen and then come back to see your edits. 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | Discover what to do next with Redux: 62 |
63 | 64 |
65 | Read the docs to discover what to do next: 66 |
67 | 68 |
69 | 70 | 71 | ) 72 | } 73 | 74 | const styles = StyleSheet.create({ 75 | highlight: { 76 | fontWeight: "700", 77 | }, 78 | }) 79 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/debug.keystore -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/java/com/ReactNativeReduxTemplate/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ReactNativeReduxTemplate 2 | 3 | import com.facebook.react.ReactActivity 4 | import com.facebook.react.ReactActivityDelegate 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate 7 | 8 | class MainActivity : ReactActivity() { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | override fun getMainComponentName(): String = "ReactNativeReduxTemplate" 15 | 16 | /** 17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 19 | */ 20 | override fun createReactActivityDelegate(): ReactActivityDelegate = 21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 22 | } 23 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/java/com/ReactNativeReduxTemplate/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.ReactNativeReduxTemplate 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeHost 8 | import com.facebook.react.ReactPackage 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 13 | import com.facebook.soloader.SoLoader 14 | 15 | class MainApplication : Application(), ReactApplication { 16 | 17 | override val reactNativeHost: ReactNativeHost = 18 | object : DefaultReactNativeHost(this) { 19 | override fun getPackages(): List = 20 | PackageList(this).packages.apply { 21 | // Packages that cannot be autolinked yet can be added manually here, for example: 22 | // add(MyReactNativePackage()) 23 | } 24 | 25 | override fun getJSMainModuleName(): String = "index" 26 | 27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 28 | 29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 31 | } 32 | 33 | override val reactHost: ReactHost 34 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 35 | 36 | override fun onCreate() { 37 | super.onCreate() 38 | SoLoader.init(this, OpenSourceMergedSoMapping) 39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 40 | // If you opted-in for the New Architecture, we load the native entry point for this app. 41 | load() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ReactNativeReduxTemplate 3 | 4 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "35.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 35 6 | targetSdkVersion = 34 7 | ndkVersion = "26.1.10909125" 8 | kotlinVersion = "1.9.24" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Use this property to specify which architecture you want to build. 26 | # You can also override it from the CLI using 27 | # ./gradlew -PreactNativeArchitectures=x86_64 28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 29 | 30 | # Use this property to enable support to the new architecture. 31 | # This will allow you to use TurboModules and the Fabric render in 32 | # your application. You should enable this flag either if you want 33 | # to write custom TurboModules/Fabric components OR use libraries that 34 | # are providing them. 35 | newArchEnabled=true 36 | 37 | # Use this property to enable or disable the Hermes JS engine. 38 | # If set to false, you will be using JSC instead. 39 | hermesEnabled=true 40 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | rootProject.name = 'ReactNativeReduxTemplate' 5 | include ':app' 6 | includeBuild('../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeReduxTemplate", 3 | "displayName": "ReactNativeReduxTemplate" 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @import { ConfigFunction } from '@babel/core' */ 2 | 3 | /** 4 | * @satisfies {ConfigFunction} 5 | */ 6 | const config = api => { 7 | api.cache.using(() => process.env.NODE_ENV) 8 | 9 | return { 10 | presets: [["module:@react-native/babel-preset"]], 11 | } 12 | } 13 | 14 | module.exports = config 15 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import prettierConfig from "eslint-config-prettier/flat" 3 | import jestPlugin from "eslint-plugin-jest" 4 | import reactPlugin from "eslint-plugin-react" 5 | import reactHooksPlugin from "eslint-plugin-react-hooks" 6 | import globals from "globals" 7 | import { config, configs } from "typescript-eslint" 8 | 9 | const eslintConfig = config( 10 | { 11 | name: "global-ignores", 12 | ignores: [ 13 | "**/*.snap", 14 | "**/dist/", 15 | "**/.yalc/", 16 | "**/build/", 17 | "**/temp/", 18 | "**/.temp/", 19 | "**/.tmp/", 20 | "**/.yarn/", 21 | "**/coverage/", 22 | ], 23 | }, 24 | { 25 | name: `${js.meta.name}/recommended`, 26 | ...js.configs.recommended, 27 | }, 28 | configs.strictTypeChecked, 29 | configs.stylisticTypeChecked, 30 | { 31 | name: `${jestPlugin.meta.name}/recommended`, 32 | ...jestPlugin.configs["flat/recommended"], 33 | }, 34 | { 35 | name: "eslint-plugin-react/jsx-runtime", 36 | ...reactPlugin.configs.flat["jsx-runtime"], 37 | }, 38 | reactHooksPlugin.configs["recommended-latest"], 39 | { 40 | name: "main", 41 | linterOptions: { 42 | reportUnusedDisableDirectives: 2, 43 | }, 44 | languageOptions: { 45 | ecmaVersion: 2020, 46 | globals: globals.node, 47 | parserOptions: { 48 | projectService: true, 49 | tsconfigRootDir: import.meta.dirname, 50 | }, 51 | }, 52 | rules: { 53 | "no-undef": [0], 54 | "no-restricted-imports": [ 55 | 2, 56 | { 57 | paths: [ 58 | { 59 | name: "react-redux", 60 | importNames: ["useSelector", "useStore", "useDispatch"], 61 | message: 62 | "Please use pre-typed versions from `src/app/hooks.ts` instead.", 63 | }, 64 | ], 65 | }, 66 | ], 67 | "@typescript-eslint/consistent-type-definitions": [2, "type"], 68 | "@typescript-eslint/consistent-type-imports": [ 69 | 2, 70 | { 71 | prefer: "type-imports", 72 | fixStyle: "separate-type-imports", 73 | disallowTypeAnnotations: true, 74 | }, 75 | ], 76 | }, 77 | }, 78 | { 79 | name: "commonjs", 80 | files: ["metro.config.js"], 81 | languageOptions: { 82 | sourceType: "commonjs", 83 | }, 84 | rules: { 85 | "@typescript-eslint/no-require-imports": [ 86 | 0, 87 | [{ allow: [], allowAsImport: false }], 88 | ], 89 | }, 90 | }, 91 | 92 | prettierConfig, 93 | ) 94 | 95 | export default eslintConfig 96 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.gif" { 2 | const logo: number 3 | export default logo 4 | } 5 | 6 | declare module "react-native/Libraries/NewAppScreen" { 7 | import type { FC } from "react" 8 | export const HermesBadge: FC 9 | } 10 | 11 | declare module "react-native/Libraries/Core/Devtools/openURLInBrowser" { 12 | export default function openURLInBrowser(url: string): void 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from "react-native" 2 | import { Provider } from "react-redux" 3 | import { App } from "./App" 4 | import { name as appName } from "./app.json" 5 | import { store } from "./src/app/store" 6 | 7 | AppRegistry.registerComponent(appName, () => () => ( 8 | 9 | 10 | 11 | )) 12 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'ReactNativeReduxTemplate' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | # An absolute path to your application root. 23 | :app_path => "#{Pod::Config.instance.installation_root}/.." 24 | ) 25 | 26 | target 'ReactNativeReduxTemplateTests' do 27 | inherit! :complete 28 | # Pods for testing 29 | end 30 | 31 | post_install do |installer| 32 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 33 | react_native_post_install( 34 | installer, 35 | config[:reactNativePath], 36 | :mac_catalyst_enabled => false, 37 | # :ccache_enabled => true 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"ReactNativeReduxTemplate"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | return [self bundleURL]; 20 | } 21 | 22 | - (NSURL *)bundleURL 23 | { 24 | #if DEBUG 25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 26 | #else 27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 28 | #endif 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "scale": "2x", 6 | "size": "20x20" 7 | }, 8 | { 9 | "idiom": "iphone", 10 | "scale": "3x", 11 | "size": "20x20" 12 | }, 13 | { 14 | "idiom": "iphone", 15 | "scale": "2x", 16 | "size": "29x29" 17 | }, 18 | { 19 | "idiom": "iphone", 20 | "scale": "3x", 21 | "size": "29x29" 22 | }, 23 | { 24 | "idiom": "iphone", 25 | "scale": "2x", 26 | "size": "40x40" 27 | }, 28 | { 29 | "idiom": "iphone", 30 | "scale": "3x", 31 | "size": "40x40" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "scale": "2x", 36 | "size": "60x60" 37 | }, 38 | { 39 | "idiom": "iphone", 40 | "scale": "3x", 41 | "size": "60x60" 42 | }, 43 | { 44 | "idiom": "ios-marketing", 45 | "scale": "1x", 46 | "size": "1024x1024" 47 | } 48 | ], 49 | "info": { 50 | "author": "xcode", 51 | "version": 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ReactNativeReduxTemplate 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | 30 | NSAllowsArbitraryLoads 31 | 32 | NSAllowsLocalNetworking 33 | 34 | 35 | NSLocationWhenInUseUsageDescription 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIRequiredDeviceCapabilities 40 | 41 | arm64 42 | 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UIViewControllerBasedStatusBarAppearance 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplate/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplateTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/ios/ReactNativeReduxTemplateTests/ReactNativeReduxTemplateTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface ReactNativeReduxTemplateTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation ReactNativeReduxTemplateTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/react-native" 2 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest" 2 | 3 | const config: Config = { 4 | preset: "react-native", 5 | verbose: true, 6 | /** 7 | * Without this we will get the following error: 8 | * `SyntaxError: Cannot use import statement outside a module` 9 | */ 10 | transformIgnorePatterns: [ 11 | "node_modules/(?!((jest-)?react-native|...|react-redux))", 12 | ], 13 | /** 14 | * React Native's `jest` preset includes a 15 | * [polyfill for `window`](https://github.com/facebook/react-native/blob/acb634bc9662c1103bc7c8ca83cfdc62516d0060/packages/react-native/jest/setup.js#L61-L66). 16 | * This polyfill causes React-Redux to use `useEffect` 17 | * instead of `useLayoutEffect` for the `useIsomorphicLayoutEffect` hook. 18 | * As a result, nested component updates may not be properly batched 19 | * when using the `connect` API, leading to potential issues. 20 | */ 21 | globals: { 22 | window: undefined, 23 | navigator: { 24 | product: "ReactNative", 25 | }, 26 | }, 27 | setupFilesAfterEnv: ["/jest-setup.ts"], 28 | fakeTimers: { 29 | enableGlobally: true, 30 | advanceTimers: true, 31 | }, 32 | } 33 | 34 | export default config 35 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config") 2 | 3 | /** 4 | * Metro configuration 5 | * @see {@link https://reactnative.dev/docs/metro | React Native docs} 6 | * @see {@link https://facebook.github.io/metro/docs/configuration | Metro docs} 7 | * 8 | * @type {import('@react-native/metro-config').MetroConfig} 9 | */ 10 | const config = mergeConfig(getDefaultConfig(__dirname)) 11 | 12 | module.exports = config 13 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-template", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "build": "react-native bundle --entry-file index.js --bundle-output build/bundle.js --platform android --assets-dest build/assets", 8 | "format:check": "prettier --check .", 9 | "format": "prettier --write .", 10 | "ios": "react-native run-ios", 11 | "lint:fix": "eslint --fix .", 12 | "lint": "eslint .", 13 | "start": "react-native start", 14 | "test": "jest", 15 | "type-check": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@reduxjs/toolkit": "^2.6.1", 19 | "react": "19.0.0", 20 | "react-native": "^0.78.2", 21 | "react-redux": "^9.2.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.26.10", 25 | "@babel/preset-env": "^7.26.9", 26 | "@babel/runtime": "^7.27.0", 27 | "@eslint/js": "^9.23.0", 28 | "@react-native-community/cli": "^18.0.0", 29 | "@react-native-community/cli-platform-android": "^17.0.0", 30 | "@react-native-community/cli-platform-ios": "^17.0.0", 31 | "@react-native/babel-preset": "^0.78.2", 32 | "@react-native/metro-config": "^0.78.2", 33 | "@react-native/typescript-config": "^0.78.2", 34 | "@testing-library/react-native": "^13.2.0", 35 | "@types/babel__core": "^7.20.5", 36 | "@types/jest": "^29.5.14", 37 | "@types/node": "^22.14.0", 38 | "@types/react": "^19.1.0", 39 | "@types/react-test-renderer": "^19.1.0", 40 | "babel-jest": "^29.7.0", 41 | "eslint": "^9.23.0", 42 | "eslint-config-prettier": "^10.1.1", 43 | "eslint-plugin-jest": "^28.11.0", 44 | "eslint-plugin-react": "^7.37.4", 45 | "eslint-plugin-react-hooks": "^5.2.0", 46 | "globals": "^16.0.0", 47 | "jest": "^29.7.0", 48 | "prettier": "^3.5.3", 49 | "react-test-renderer": "19.0.0", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.8.2", 52 | "typescript-eslint": "^8.29.0" 53 | }, 54 | "engines": { 55 | "node": ">=18" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/react-native.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@react-native-community/cli-types').UserConfig} */ 2 | const config = { 3 | project: { 4 | ios: { 5 | automaticPodsInstallation: true, 6 | }, 7 | }, 8 | } 9 | 10 | export default config 11 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/app/createAppSlice.ts: -------------------------------------------------------------------------------- 1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" 2 | 3 | // `buildCreateSlice` allows us to create a slice with async thunks. 4 | export const createAppSlice = buildCreateSlice({ 5 | creators: { asyncThunk: asyncThunkCreator }, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks. 2 | // These imports are restricted elsewhere to ensure consistent 3 | // usage of typed hooks throughout the application. 4 | // We disable the ESLint rule here because this is the designated place 5 | // for importing and re-exporting the typed versions of hooks. 6 | /* eslint-disable no-restricted-imports */ 7 | import { useEffect } from "react" 8 | import { Animated, useAnimatedValue, useWindowDimensions } from "react-native" 9 | import { useDispatch, useSelector } from "react-redux" 10 | import type { AppDispatch, RootState } from "./store" 11 | 12 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 13 | export const useAppDispatch = useDispatch.withTypes() 14 | export const useAppSelector = useSelector.withTypes() 15 | 16 | /** 17 | * Custom React hook for calculating viewport units 18 | * based on the current window dimensions. 19 | * 20 | * @returns An object containing the calculated viewport heigh and width values. 21 | */ 22 | export const useViewportUnits = (): { vh: number; vw: number } => { 23 | const { width, height } = useWindowDimensions() 24 | 25 | const vh = height / 100 26 | const vw = width / 100 27 | 28 | return { vh, vw } 29 | } 30 | 31 | /** 32 | * Custom React hook for creating a bounce animation effect. 33 | * 34 | * @param value - The maximum height to which the object should bounce. Defaults to 10 if not provided. 35 | * @returns The {@linkcode Animated.Value} object that can be used to drive animations. 36 | */ 37 | export const useBounceAnimation = (value = 10): Animated.Value => { 38 | const bounce = useAnimatedValue(0) 39 | 40 | bounce.interpolate({ 41 | inputRange: [-300, -100, 0, 100, 101], 42 | outputRange: [300, 0, 1, 0, 0], 43 | }) 44 | 45 | useEffect(() => { 46 | Animated.loop( 47 | Animated.sequence([ 48 | Animated.timing(bounce, { 49 | toValue: value, 50 | duration: 1500, 51 | useNativeDriver: true, 52 | }), 53 | Animated.timing(bounce, { 54 | toValue: 0, 55 | duration: 1500, 56 | useNativeDriver: true, 57 | }), 58 | ]), 59 | ).start() 60 | }, [bounce, value]) 61 | 62 | return bounce 63 | } 64 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ThunkAction } from "@reduxjs/toolkit" 2 | import { combineSlices, configureStore } from "@reduxjs/toolkit" 3 | import { setupListeners } from "@reduxjs/toolkit/query" 4 | import { counterSlice } from "../features/counter/counterSlice" 5 | import { quotesApiSlice } from "../features/quotes/quotesApiSlice" 6 | 7 | // `combineSlices` automatically combines the reducers using 8 | // their `reducerPath`s, therefore we no longer need to call `combineReducers`. 9 | const rootReducer = combineSlices(counterSlice, quotesApiSlice) 10 | // Infer the `RootState` type from the root reducer 11 | export type RootState = ReturnType 12 | 13 | // The store setup is wrapped in `makeStore` to allow reuse 14 | // when setting up tests that need the same store config 15 | export const makeStore = (preloadedState?: Partial) => { 16 | const store = configureStore({ 17 | reducer: rootReducer, 18 | // Adding the api middleware enables caching, invalidation, polling, 19 | // and other useful features of `rtk-query`. 20 | middleware: getDefaultMiddleware => { 21 | return getDefaultMiddleware().concat(quotesApiSlice.middleware) 22 | }, 23 | preloadedState, 24 | }) 25 | // configure listeners using the provided defaults 26 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors 27 | setupListeners(store.dispatch) 28 | return store 29 | } 30 | 31 | export const store = makeStore() 32 | 33 | // Infer the type of `store` 34 | export type AppStore = typeof store 35 | // Infer the `AppDispatch` type from the store itself 36 | export type AppDispatch = AppStore["dispatch"] 37 | export type AppThunk = ThunkAction< 38 | ThunkReturnType, 39 | RootState, 40 | unknown, 41 | Action 42 | > 43 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/components/AsyncButton.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, PropsWithChildren } from "react" 2 | import type { 3 | GestureResponderEvent, 4 | PressableProps, 5 | ViewStyle, 6 | } from "react-native" 7 | import { 8 | Animated, 9 | Pressable, 10 | StyleSheet, 11 | View, 12 | useAnimatedValue, 13 | } from "react-native" 14 | 15 | type AsyncButtonProps = PressableProps & PropsWithChildren 16 | 17 | export const AsyncButton = ({ 18 | onPress, 19 | style, 20 | children, 21 | ...restProps 22 | }: AsyncButtonProps): JSX.Element => { 23 | const progress = useAnimatedValue(0) 24 | const opacity = useAnimatedValue(1) 25 | 26 | const _onPress = (e: GestureResponderEvent) => { 27 | progress.setValue(0) 28 | opacity.setValue(1) 29 | 30 | onPress?.(e) 31 | 32 | Animated.timing(progress, { 33 | toValue: 1, 34 | duration: 1000, 35 | useNativeDriver: false, 36 | }).start(({ finished }) => { 37 | if (!finished) { 38 | return 39 | } 40 | 41 | Animated.timing(opacity, { 42 | toValue: 0, 43 | duration: 200, 44 | useNativeDriver: false, 45 | }).start() 46 | }) 47 | } 48 | 49 | const progressInterpolate = progress.interpolate({ 50 | inputRange: [0, 1], 51 | outputRange: ["0%", "100%"], 52 | extrapolate: "clamp", 53 | }) 54 | 55 | const progressStyle: Animated.WithAnimatedObject = { 56 | width: progressInterpolate, 57 | opacity, 58 | } 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | {children} 66 | 67 | ) 68 | } 69 | 70 | const styles = StyleSheet.create({ 71 | progress: { 72 | position: "absolute", 73 | top: 0, 74 | bottom: 0, 75 | left: 0, 76 | backgroundColor: "rgba(112,76,182, 0.15)", 77 | }, 78 | }) 79 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { Animated, StyleSheet, View, useColorScheme } from "react-native" 3 | import { useBounceAnimation, useViewportUnits } from "../app/hooks" 4 | import { TypedColors } from "../constants/TypedColors" 5 | import logo from "./logo.gif" 6 | 7 | export const Header = (): JSX.Element => { 8 | const isDarkMode = useColorScheme() === "dark" 9 | const { vh } = useViewportUnits() 10 | const bounce = useBounceAnimation() 11 | const height = 40 * vh 12 | 13 | return ( 14 | 20 | 25 | 26 | ) 27 | } 28 | 29 | const styles = StyleSheet.create({ 30 | container: { 31 | flexDirection: "row", 32 | justifyContent: "center", 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/components/Section.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, PropsWithChildren } from "react" 2 | import { StyleSheet, Text, View, useColorScheme } from "react-native" 3 | import { TypedColors } from "../constants/TypedColors" 4 | 5 | type SectionProps = PropsWithChildren<{ 6 | title: string 7 | }> 8 | 9 | export const Section = ({ children, title }: SectionProps): JSX.Element => { 10 | const isDarkMode = useColorScheme() === "dark" 11 | 12 | return ( 13 | 14 | 20 | {title} 21 | 22 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | sectionContainer: { 36 | marginTop: 32, 37 | paddingHorizontal: 24, 38 | }, 39 | sectionTitle: { 40 | fontSize: 24, 41 | fontWeight: "600", 42 | }, 43 | sectionDescription: { 44 | marginTop: 8, 45 | fontSize: 18, 46 | fontWeight: "400", 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/components/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/redux-templates/3fea92b2b899f457995032a52fc9d92e13a4976f/packages/react-native-template-redux-typescript/template/src/components/logo.gif -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/constants/TypedColors.ts: -------------------------------------------------------------------------------- 1 | import { Colors } from "react-native/Libraries/NewAppScreen" 2 | 3 | type AllColors = { 4 | primary: string 5 | white: string 6 | lighter: string 7 | light: string 8 | dark: string 9 | darker: string 10 | black: string 11 | } 12 | 13 | export const TypedColors: AllColors = Colors satisfies AllColors as AllColors 14 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export const fetchCount = (amount = 1): Promise<{ data: number }> => 3 | new Promise<{ data: number }>(resolve => 4 | setTimeout(() => { 5 | resolve({ data: amount }) 6 | }, 500), 7 | ) 8 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/features/counter/counterSlice.test.ts: -------------------------------------------------------------------------------- 1 | import { makeStore } from "../../app/store" 2 | import type { CounterSliceState } from "./counterSlice" 3 | import { 4 | counterSlice, 5 | decrement, 6 | increment, 7 | incrementByAmount, 8 | selectCount, 9 | } from "./counterSlice" 10 | 11 | describe("counter reducer", () => { 12 | const initialState: CounterSliceState = { 13 | value: 3, 14 | status: "idle", 15 | } 16 | 17 | let store = makeStore() 18 | 19 | beforeEach(() => { 20 | store = makeStore({ counter: initialState }) 21 | }) 22 | 23 | it("should handle initial state", () => { 24 | expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ 25 | value: 0, 26 | status: "idle", 27 | }) 28 | }) 29 | 30 | it("should handle increment", () => { 31 | expect(selectCount(store.getState())).toBe(3) 32 | 33 | store.dispatch(increment()) 34 | 35 | expect(selectCount(store.getState())).toBe(4) 36 | }) 37 | 38 | it("should handle decrement", () => { 39 | expect(selectCount(store.getState())).toBe(3) 40 | 41 | store.dispatch(decrement()) 42 | 43 | expect(selectCount(store.getState())).toBe(2) 44 | }) 45 | 46 | it("should handle incrementByAmount", () => { 47 | expect(selectCount(store.getState())).toBe(3) 48 | 49 | store.dispatch(incrementByAmount(2)) 50 | 51 | expect(selectCount(store.getState())).toBe(5) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/src/features/quotes/quotesApiSlice.ts: -------------------------------------------------------------------------------- 1 | // Need to use the React-specific entry point to import `createApi` 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" 3 | 4 | type Quote = { 5 | id: number 6 | quote: string 7 | author: string 8 | } 9 | 10 | type QuotesApiResponse = { 11 | quotes: Quote[] 12 | total: number 13 | skip: number 14 | limit: number 15 | } 16 | 17 | // Define a service using a base URL and expected endpoints 18 | export const quotesApiSlice = createApi({ 19 | baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), 20 | reducerPath: "quotesApi", 21 | // Tag types are used for caching and invalidation. 22 | tagTypes: ["Quotes"], 23 | endpoints: build => ({ 24 | // Supply generics for the return type (in this case `QuotesApiResponse`) 25 | // and the expected query argument. If there is no argument, use `void` 26 | // for the argument type instead. 27 | getQuotes: build.query({ 28 | query: (limit = 10) => `?limit=${limit.toString()}`, 29 | // `providesTags` determines which 'tag' is attached to the 30 | // cached data returned by the query. 31 | providesTags: (_result, _error, id) => [{ type: "Quotes", id }], 32 | }), 33 | }), 34 | }) 35 | 36 | // Hooks are auto-generated by RTK-Query 37 | // Same as `quotesApiSlice.endpoints.getQuotes.useQuery` 38 | export const { useGetQuotesQuery } = quotesApiSlice 39 | -------------------------------------------------------------------------------- /packages/react-native-template-redux-typescript/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | typesversions 27 | .cache 28 | .yarnrc 29 | .yarn/* 30 | !.yarn/patches 31 | !.yarn/releases 32 | !.yarn/plugins 33 | !.yarn/sdks 34 | !.yarn/versions 35 | .pnp.* 36 | *.tgz -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/README.md: -------------------------------------------------------------------------------- 1 | # Redux Toolkit App Structure Example 2 | 3 | This slimmed-down version of [the Redux Toolkit template for Vite](https://github.com/reduxjs/redux-templates/tree/master/packages/vite-template-redux) shows the standard app structure and store setup for Redux Toolkit, TypeScript, and React. 4 | 5 | It's used in the ["Redux Essentials" tutorial Part 2](https://redux.js.org/tutorials/essentials/part-2-app-structure) as an embeddable CodeSandbox to help teach Redux app structure. 6 | 7 | ## Scripts 8 | 9 | - `dev`/`start` - start dev server and open browser 10 | - `build` - build for production 11 | - `preview` - locally preview production build 12 | 13 | ## Inspiration 14 | 15 | - [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template) 16 | - [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react) 17 | - [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib) 18 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Redux App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtk-app-structure-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "start": "vite", 8 | "build": "tsc && vite build", 9 | "type-check": "tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "@reduxjs/toolkit": "^2.6.1", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0", 15 | "react-redux": "^9.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.14.0", 19 | "@types/react": "^19.1.0", 20 | "@types/react-dom": "^19.1.1", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "prettier": "^3.5.3", 23 | "typescript": "^5.8.2", 24 | "vite": "^6.2.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 20vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-float infinite 3s ease-in-out; 13 | } 14 | } 15 | 16 | .App-header { 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: flex-start; 22 | font-size: calc(10px + 2vmin); 23 | } 24 | 25 | .App-link { 26 | color: rgb(112, 76, 182); 27 | } 28 | 29 | @keyframes App-logo-float { 30 | 0% { 31 | transform: translateY(0); 32 | } 33 | 50% { 34 | transform: translateY(10px); 35 | } 36 | 100% { 37 | transform: translateY(0px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import "./App.css" 3 | import { Counter } from "./features/counter/Counter" 4 | import logo from "./logo.svg" 5 | 6 | export const App = (): JSX.Element => ( 7 |
8 |
9 | logo 10 | 11 |

12 | Edit src/App.tsx and save to reload. 13 |

14 | 15 | Learn 16 | 22 | React 23 | 24 | and 25 | 31 | Redux 32 | 33 | 34 |
35 |
36 | ) 37 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks. 2 | import { useDispatch, useSelector } from "react-redux" 3 | import type { AppDispatch, RootState } from "./store" 4 | 5 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 6 | export const useAppDispatch = useDispatch.withTypes() 7 | export const useAppSelector = useSelector.withTypes() 8 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ThunkAction } from "@reduxjs/toolkit" 2 | import { configureStore } from "@reduxjs/toolkit" 3 | import counterReducer from "@/features/counter/counterSlice" 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | counter: counterReducer, 8 | }, 9 | }) 10 | 11 | // Infer the type of `store` 12 | export type AppStore = typeof store 13 | export type RootState = ReturnType 14 | // Infer the `AppDispatch` type from the store itself 15 | export type AppDispatch = AppStore["dispatch"] 16 | // Define a reusable type describing thunk functions 17 | export type AppThunk = ThunkAction< 18 | ThunkReturnType, 19 | RootState, 20 | unknown, 21 | Action 22 | > 23 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/features/counter/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: "Courier New", Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .oddButton { 58 | composes: button; 59 | font-size: 24px; 60 | } 61 | 62 | .asyncButton { 63 | composes: button; 64 | font-size: 24px; 65 | position: relative; 66 | } 67 | 68 | .asyncButton:after { 69 | content: ""; 70 | background-color: rgba(112, 76, 182, 0.15); 71 | display: block; 72 | position: absolute; 73 | width: 100%; 74 | height: 100%; 75 | left: 0; 76 | top: 0; 77 | opacity: 0; 78 | transition: 79 | width 1s linear, 80 | opacity 0.5s ease 1s; 81 | } 82 | 83 | .asyncButton:active:after { 84 | width: 0%; 85 | opacity: 1; 86 | transition: 0s; 87 | } 88 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/features/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | // Use pre-typed versions of the React-Redux 4 | // `useDispatch` and `useSelector` hooks 5 | import { useAppDispatch, useAppSelector } from "@/app/hooks" 6 | import type { JSX } from "react" 7 | import styles from "./Counter.module.css" 8 | import { 9 | decrement, 10 | increment, 11 | incrementAsync, 12 | incrementByAmount, 13 | incrementIfOdd, 14 | selectCount, 15 | selectStatus, 16 | } from "./counterSlice" 17 | 18 | export const Counter = (): JSX.Element => { 19 | const dispatch = useAppDispatch() 20 | const count = useAppSelector(selectCount) 21 | const status = useAppSelector(selectStatus) 22 | const [incrementAmount, setIncrementAmount] = useState("2") 23 | 24 | const incrementValue = Number(incrementAmount) || 0 25 | 26 | return ( 27 |
28 |
29 | 38 | 39 | {count} 40 | 41 | 50 |
51 |
52 | { 58 | setIncrementAmount(e.target.value) 59 | }} 60 | /> 61 | 69 |
70 |
71 | 80 | 88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export const fetchCount = (amount = 1): Promise<{ data: number }> => 3 | new Promise<{ data: number }>(resolve => 4 | setTimeout(() => { 5 | resolve({ data: amount }) 6 | }, 500), 7 | ) 8 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: 12 | source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { Provider } from "react-redux" 4 | import { App } from "./App" 5 | import { store } from "./app/store" 6 | import "./index.css" 7 | 8 | const container = document.getElementById("root")! 9 | const root = createRoot(container) 10 | 11 | root.render( 12 | 13 | 14 | 15 | 16 | , 17 | ) 18 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "module": "ESNext", 12 | "moduleResolution": "bundler", 13 | "isolatedModules": true, 14 | "resolveJsonModule": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/rtk-app-structure-example/vite.config.mts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { defineConfig } from "vite" 3 | import react from "@vitejs/plugin-react" 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: [{ find: "@", replacement: path.resolve("./src") }], 10 | }, 11 | server: { 12 | open: true, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/vite-template-redux/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .yalc/ 27 | yalc.lock 28 | -------------------------------------------------------------------------------- /packages/vite-template-redux/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/vite-template-redux/README.md: -------------------------------------------------------------------------------- 1 | # vite-template-redux 2 | 3 | Uses [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [React Testing Library](https://github.com/testing-library/react-testing-library) to create a modern [React](https://react.dev/) app compatible with [Create React App](https://create-react-app.dev/) 4 | 5 | ```sh 6 | npx tiged reduxjs/redux-templates/packages/vite-template-redux my-app 7 | ``` 8 | 9 | ## Goals 10 | 11 | - Easy migration from Create React App or Vite 12 | - As beginner friendly as Create React App 13 | - Optimized performance compared to Create React App 14 | - Customizable without ejecting 15 | 16 | ## Scripts 17 | 18 | - `dev`/`start` - start dev server and open browser 19 | - `build` - build for production 20 | - `preview` - locally preview production build 21 | - `test` - launch test runner 22 | 23 | ## Inspiration 24 | 25 | - [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template) 26 | - [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react) 27 | - [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib) 28 | -------------------------------------------------------------------------------- /packages/vite-template-redux/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import vitestPlugin from "@vitest/eslint-plugin" 3 | import prettierConfig from "eslint-config-prettier/flat" 4 | import reactPlugin from "eslint-plugin-react" 5 | import reactHooksPlugin from "eslint-plugin-react-hooks" 6 | import globals from "globals" 7 | import { config, configs } from "typescript-eslint" 8 | 9 | const eslintConfig = config( 10 | { 11 | name: "global-ignores", 12 | ignores: [ 13 | "**/*.snap", 14 | "**/dist/", 15 | "**/.yalc/", 16 | "**/build/", 17 | "**/temp/", 18 | "**/.temp/", 19 | "**/.tmp/", 20 | "**/.yarn/", 21 | "**/coverage/", 22 | ], 23 | }, 24 | { 25 | name: `${js.meta.name}/recommended`, 26 | ...js.configs.recommended, 27 | }, 28 | configs.strictTypeChecked, 29 | configs.stylisticTypeChecked, 30 | vitestPlugin.configs.recommended, 31 | { 32 | name: "eslint-plugin-react/jsx-runtime", 33 | ...reactPlugin.configs.flat["jsx-runtime"], 34 | }, 35 | reactHooksPlugin.configs["recommended-latest"], 36 | { 37 | name: "main", 38 | linterOptions: { 39 | reportUnusedDisableDirectives: 2, 40 | }, 41 | languageOptions: { 42 | ecmaVersion: 2020, 43 | globals: globals.browser, 44 | parserOptions: { 45 | projectService: true, 46 | tsconfigRootDir: import.meta.dirname, 47 | }, 48 | }, 49 | settings: { 50 | vitest: { 51 | typecheck: true, 52 | }, 53 | }, 54 | rules: { 55 | "no-undef": [0], 56 | "@typescript-eslint/consistent-type-definitions": [2, "type"], 57 | "@typescript-eslint/consistent-type-imports": [ 58 | 2, 59 | { 60 | prefer: "type-imports", 61 | fixStyle: "separate-type-imports", 62 | disallowTypeAnnotations: true, 63 | }, 64 | ], 65 | "no-restricted-imports": [ 66 | 2, 67 | { 68 | paths: [ 69 | { 70 | name: "react-redux", 71 | importNames: ["useSelector", "useStore", "useDispatch"], 72 | message: 73 | "Please use pre-typed versions from `src/app/hooks.ts` instead.", 74 | }, 75 | ], 76 | }, 77 | ], 78 | }, 79 | }, 80 | 81 | prettierConfig, 82 | ) 83 | 84 | export default eslintConfig 85 | -------------------------------------------------------------------------------- /packages/vite-template-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Redux App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/vite-template-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-template-redux", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc -b && vite build", 8 | "dev": "vite", 9 | "format:check": "prettier --check .", 10 | "format": "prettier --write .", 11 | "lint:fix": "eslint --fix .", 12 | "lint": "eslint .", 13 | "preview": "vite preview", 14 | "start": "vite", 15 | "test": "vitest --run", 16 | "type-check": "tsc -b --noEmit" 17 | }, 18 | "dependencies": { 19 | "@reduxjs/toolkit": "^2.6.1", 20 | "react": "^19.1.0", 21 | "react-dom": "^19.1.0", 22 | "react-redux": "^9.2.0" 23 | }, 24 | "devDependencies": { 25 | "@eslint/js": "^9.23.0", 26 | "@testing-library/dom": "^10.4.0", 27 | "@testing-library/jest-dom": "^6.6.3", 28 | "@testing-library/react": "^16.3.0", 29 | "@testing-library/user-event": "^14.6.1", 30 | "@types/node": "^22.14.0", 31 | "@types/react": "^19.1.0", 32 | "@types/react-dom": "^19.1.1", 33 | "@vitejs/plugin-react": "^4.3.4", 34 | "@vitest/eslint-plugin": "^1.1.39", 35 | "eslint": "^9.23.0", 36 | "eslint-config-prettier": "^10.1.1", 37 | "eslint-plugin-prettier": "^5.2.6", 38 | "eslint-plugin-react": "^7.37.4", 39 | "eslint-plugin-react-hooks": "^5.2.0", 40 | "globals": "^16.0.0", 41 | "jsdom": "^26.0.0", 42 | "prettier": "^3.5.3", 43 | "typescript": "^5.8.2", 44 | "typescript-eslint": "^8.29.0", 45 | "vite": "^6.2.4", 46 | "vitest": "^3.1.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-float infinite 3s ease-in-out; 13 | } 14 | } 15 | 16 | .App-header { 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | font-size: calc(10px + 2vmin); 23 | } 24 | 25 | .App-link { 26 | color: rgb(112, 76, 182); 27 | } 28 | 29 | @keyframes App-logo-float { 30 | 0% { 31 | transform: translateY(0); 32 | } 33 | 50% { 34 | transform: translateY(10px); 35 | } 36 | 100% { 37 | transform: translateY(0px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css" 2 | import { Counter } from "./features/counter/Counter" 3 | import { Quotes } from "./features/quotes/Quotes" 4 | import logo from "./logo.svg" 5 | 6 | export const App = () => ( 7 |
8 |
9 | logo 10 | 11 |

12 | Edit src/App.tsx and save to reload. 13 |

14 | 15 | 16 | Learn 17 | 23 | React 24 | 25 | , 26 | 32 | Redux 33 | 34 | , 35 | 41 | Redux Toolkit 42 | 43 | , 44 | 50 | React Redux 51 | 52 | , and 53 | 59 | Reselect 60 | 61 | 62 |
63 |
64 | ) 65 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/app/createAppSlice.ts: -------------------------------------------------------------------------------- 1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" 2 | 3 | // `buildCreateSlice` allows us to create a slice with async thunks. 4 | export const createAppSlice = buildCreateSlice({ 5 | creators: { asyncThunk: asyncThunkCreator }, 6 | }) 7 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks. 2 | // These imports are restricted elsewhere to ensure consistent 3 | // usage of typed hooks throughout the application. 4 | // We disable the ESLint rule here because this is the designated place 5 | // for importing and re-exporting the typed versions of hooks. 6 | /* eslint-disable no-restricted-imports */ 7 | import { useDispatch, useSelector } from "react-redux" 8 | import type { AppDispatch, RootState } from "./store" 9 | 10 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 11 | export const useAppDispatch = useDispatch.withTypes() 12 | export const useAppSelector = useSelector.withTypes() 13 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ThunkAction } from "@reduxjs/toolkit" 2 | import { combineSlices, configureStore } from "@reduxjs/toolkit" 3 | import { setupListeners } from "@reduxjs/toolkit/query" 4 | import { counterSlice } from "../features/counter/counterSlice" 5 | import { quotesApiSlice } from "../features/quotes/quotesApiSlice" 6 | 7 | // `combineSlices` automatically combines the reducers using 8 | // their `reducerPath`s, therefore we no longer need to call `combineReducers`. 9 | const rootReducer = combineSlices(counterSlice, quotesApiSlice) 10 | // Infer the `RootState` type from the root reducer 11 | export type RootState = ReturnType 12 | 13 | // The store setup is wrapped in `makeStore` to allow reuse 14 | // when setting up tests that need the same store config 15 | export const makeStore = (preloadedState?: Partial) => { 16 | const store = configureStore({ 17 | reducer: rootReducer, 18 | // Adding the api middleware enables caching, invalidation, polling, 19 | // and other useful features of `rtk-query`. 20 | middleware: getDefaultMiddleware => { 21 | return getDefaultMiddleware().concat(quotesApiSlice.middleware) 22 | }, 23 | preloadedState, 24 | }) 25 | // configure listeners using the provided defaults 26 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors 27 | setupListeners(store.dispatch) 28 | return store 29 | } 30 | 31 | export const store = makeStore() 32 | 33 | // Infer the type of `store` 34 | export type AppStore = typeof store 35 | // Infer the `AppDispatch` type from the store itself 36 | export type AppDispatch = AppStore["dispatch"] 37 | export type AppThunk = ThunkAction< 38 | ThunkReturnType, 39 | RootState, 40 | unknown, 41 | Action 42 | > 43 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/counter/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: "Courier New", Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .asyncButton { 58 | composes: button; 59 | position: relative; 60 | } 61 | 62 | .asyncButton:after { 63 | content: ""; 64 | background-color: rgba(112, 76, 182, 0.15); 65 | display: block; 66 | position: absolute; 67 | width: 100%; 68 | height: 100%; 69 | left: 0; 70 | top: 0; 71 | opacity: 0; 72 | transition: 73 | width 1s linear, 74 | opacity 0.5s ease 1s; 75 | } 76 | 77 | .asyncButton:active:after { 78 | width: 0%; 79 | opacity: 1; 80 | transition: 0s; 81 | } 82 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { useState } from "react" 3 | import { useAppDispatch, useAppSelector } from "../../app/hooks" 4 | import styles from "./Counter.module.css" 5 | import { 6 | decrement, 7 | increment, 8 | incrementAsync, 9 | incrementByAmount, 10 | incrementIfOdd, 11 | selectCount, 12 | selectStatus, 13 | } from "./counterSlice" 14 | 15 | export const Counter = (): JSX.Element => { 16 | const dispatch = useAppDispatch() 17 | const count = useAppSelector(selectCount) 18 | const status = useAppSelector(selectStatus) 19 | const [incrementAmount, setIncrementAmount] = useState("2") 20 | 21 | const incrementValue = Number(incrementAmount) || 0 22 | 23 | return ( 24 |
25 |
26 | 33 | 36 | 43 |
44 |
45 | { 51 | setIncrementAmount(e.target.value) 52 | }} 53 | /> 54 | 60 | 69 | 77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export const fetchCount = (amount = 1): Promise<{ data: number }> => 3 | new Promise<{ data: number }>(resolve => 4 | setTimeout(() => { 5 | resolve({ data: amount }) 6 | }, 500), 7 | ) 8 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/counter/counterSlice.test.ts: -------------------------------------------------------------------------------- 1 | import type { AppStore } from "../../app/store" 2 | import { makeStore } from "../../app/store" 3 | import type { CounterSliceState } from "./counterSlice" 4 | import { 5 | counterSlice, 6 | decrement, 7 | increment, 8 | incrementByAmount, 9 | selectCount, 10 | } from "./counterSlice" 11 | 12 | type LocalTestContext = { 13 | store: AppStore 14 | } 15 | 16 | describe("counter reducer", () => { 17 | beforeEach(context => { 18 | const initialState: CounterSliceState = { 19 | value: 3, 20 | status: "idle", 21 | } 22 | 23 | const store = makeStore({ counter: initialState }) 24 | 25 | context.store = store 26 | }) 27 | 28 | it("should handle initial state", () => { 29 | expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ 30 | value: 0, 31 | status: "idle", 32 | }) 33 | }) 34 | 35 | it("should handle increment", ({ store }) => { 36 | expect(selectCount(store.getState())).toBe(3) 37 | 38 | store.dispatch(increment()) 39 | 40 | expect(selectCount(store.getState())).toBe(4) 41 | }) 42 | 43 | it("should handle decrement", ({ store }) => { 44 | expect(selectCount(store.getState())).toBe(3) 45 | 46 | store.dispatch(decrement()) 47 | 48 | expect(selectCount(store.getState())).toBe(2) 49 | }) 50 | 51 | it("should handle incrementByAmount", ({ store }) => { 52 | expect(selectCount(store.getState())).toBe(3) 53 | 54 | store.dispatch(incrementByAmount(2)) 55 | 56 | expect(selectCount(store.getState())).toBe(5) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/quotes/Quotes.module.css: -------------------------------------------------------------------------------- 1 | .select { 2 | font-size: 25px; 3 | padding: 5px; 4 | padding-top: 2px; 5 | padding-bottom: 2px; 6 | size: 50; 7 | outline: none; 8 | border: 2px solid transparent; 9 | color: rgb(112, 76, 182); 10 | cursor: pointer; 11 | background-color: rgba(112, 76, 182, 0.1); 12 | border-radius: 5px; 13 | transition: all 0.15s; 14 | } 15 | 16 | .container { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/quotes/Quotes.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { useState } from "react" 3 | import styles from "./Quotes.module.css" 4 | import { useGetQuotesQuery } from "./quotesApiSlice" 5 | 6 | const options = [5, 10, 20, 30] 7 | 8 | export const Quotes = (): JSX.Element | null => { 9 | const [numberOfQuotes, setNumberOfQuotes] = useState(10) 10 | // Using a query hook automatically fetches data and returns query values 11 | const { data, isError, isLoading, isSuccess } = 12 | useGetQuotesQuery(numberOfQuotes) 13 | 14 | if (isError) { 15 | return ( 16 |
17 |

There was an error!!!

18 |
19 | ) 20 | } 21 | 22 | if (isLoading) { 23 | return ( 24 |
25 |

Loading...

26 |
27 | ) 28 | } 29 | 30 | if (isSuccess) { 31 | return ( 32 |
33 |

Select the Quantity of Quotes to Fetch:

34 | 47 | {data.quotes.map(({ author, quote, id }) => ( 48 |
49 | “{quote}” 50 |
51 | {author} 52 |
53 |
54 | ))} 55 |
56 | ) 57 | } 58 | 59 | return null 60 | } 61 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/features/quotes/quotesApiSlice.ts: -------------------------------------------------------------------------------- 1 | // Need to use the React-specific entry point to import `createApi` 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" 3 | 4 | type Quote = { 5 | id: number 6 | quote: string 7 | author: string 8 | } 9 | 10 | type QuotesApiResponse = { 11 | quotes: Quote[] 12 | total: number 13 | skip: number 14 | limit: number 15 | } 16 | 17 | // Define a service using a base URL and expected endpoints 18 | export const quotesApiSlice = createApi({ 19 | baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), 20 | reducerPath: "quotesApi", 21 | // Tag types are used for caching and invalidation. 22 | tagTypes: ["Quotes"], 23 | endpoints: build => ({ 24 | // Supply generics for the return type (in this case `QuotesApiResponse`) 25 | // and the expected query argument. If there is no argument, use `void` 26 | // for the argument type instead. 27 | getQuotes: build.query({ 28 | query: (limit = 10) => `?limit=${limit.toString()}`, 29 | // `providesTags` determines which 'tag' is attached to the 30 | // cached data returned by the query. 31 | providesTags: (_result, _error, id) => [{ type: "Quotes", id }], 32 | }), 33 | }), 34 | }) 35 | 36 | // Hooks are auto-generated by RTK-Query 37 | // Same as `quotesApiSlice.endpoints.getQuotes.useQuery` 38 | export const { useGetQuotesQuery } = quotesApiSlice 39 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: 12 | source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { Provider } from "react-redux" 4 | import { App } from "./App" 5 | import { store } from "./app/store" 6 | import "./index.css" 7 | 8 | const container = document.getElementById("root") 9 | 10 | if (container) { 11 | const root = createRoot(container) 12 | 13 | root.render( 14 | 15 | 16 | 17 | 18 | , 19 | ) 20 | } else { 21 | throw new Error( 22 | "Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.", 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest" 2 | -------------------------------------------------------------------------------- /packages/vite-template-redux/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/vite-template-redux/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "resolveJsonModule": true, 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true, 25 | "types": ["vitest/globals"] 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/vite-template-redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vite-template-redux/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "allowJs": true, 9 | "checkJs": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "resolveJsonModule": true, 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["vite.config.ts", "eslint.config.js"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/vite-template-redux/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react" 2 | import * as path from "node:path" 3 | import { defineConfig } from "vitest/config" 4 | import packageJson from "./package.json" with { type: "json" } 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | 10 | server: { 11 | open: true, 12 | }, 13 | 14 | test: { 15 | root: import.meta.dirname, 16 | name: packageJson.name, 17 | environment: "jsdom", 18 | 19 | typecheck: { 20 | enabled: true, 21 | tsconfig: path.join(import.meta.dirname, "tsconfig.json"), 22 | }, 23 | 24 | globals: true, 25 | watch: false, 26 | setupFiles: ["./src/setupTests.ts"], 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @import { Config } from "prettier" */ 2 | 3 | /** 4 | * @satisfies {Config} 5 | */ 6 | const prettierConfig = { 7 | arrowParens: "avoid", 8 | semi: false, 9 | } 10 | 11 | export default prettierConfig 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "checkJs": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "isolatedModules": true, 10 | "jsx": "react-jsx", 11 | "lib": ["DOM", "ESNext"], 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "noEmit": true, 15 | "noErrorTruncation": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitOverride": true, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "outDir": "./dist", 22 | "resolveJsonModule": true, 23 | "rootDir": "./", 24 | "skipLibCheck": true, 25 | "sourceMap": true, 26 | "strict": true, 27 | "target": "ESNext", 28 | "types": ["vitest/globals", "vitest/importMeta"], 29 | "useDefineForClassFields": true, 30 | "useUnknownInCatchVariables": true 31 | }, 32 | "exclude": [ 33 | "dist", 34 | "packages/cra-template-redux/template", 35 | "packages/cra-template-redux-typescript/template", 36 | "packages/expo-template-redux-typescript", 37 | "packages/react-native-template-redux-typescript/template", 38 | "packages/rtk-app-structure-example", 39 | "packages/vite-template-redux" 40 | ] 41 | } 42 | --------------------------------------------------------------------------------