├── .all-contributorsrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── README_SOURCE.md ├── configs ├── jest.config.json └── jest.stubs.js ├── docs ├── build │ ├── 0.6e57cfb5.js │ └── bundle.a71e23e0.js └── index.html ├── generate-readme.js ├── generate-readme.sh ├── generate-styleguide.sh ├── is-git-status-clean.sh ├── package-lock.json ├── package.json └── playground ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .vscode └── settings.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src-old ├── App.css ├── App.test.tsx ├── App.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts └── setupTests.ts ├── src ├── api │ ├── agent.ts │ ├── fixtures │ │ └── todos.json │ ├── index.ts │ ├── models.ts │ ├── todos.ts │ └── utils.ts ├── app.test.tsx ├── app.tsx ├── components │ ├── __snapshots__ │ │ ├── class-counter-with-default-props.stories.storyshot │ │ ├── class-counter.stories.storyshot │ │ ├── fc-counter-with-default-props.stories.storyshot │ │ ├── fc-counter.stories.storyshot │ │ ├── fc-spread-attributes.stories.storyshot │ │ ├── generic-list.stories.storyshot │ │ └── mouse-provider.stories.storyshot │ ├── class-counter-with-default-props.md │ ├── class-counter-with-default-props.stories.tsx │ ├── class-counter-with-default-props.tsx │ ├── class-counter-with-default-props.usage.tsx │ ├── class-counter.md │ ├── class-counter.stories.tsx │ ├── class-counter.tsx │ ├── class-counter.usage.tsx │ ├── error-message.tsx │ ├── fc-counter-with-default-props.md │ ├── fc-counter-with-default-props.stories.tsx │ ├── fc-counter-with-default-props.tsx │ ├── fc-counter-with-default-props.usage.tsx │ ├── fc-counter.md │ ├── fc-counter.stories.tsx │ ├── fc-counter.tsx │ ├── fc-counter.usage.tsx │ ├── fc-spread-attributes.md │ ├── fc-spread-attributes.stories.tsx │ ├── fc-spread-attributes.tsx │ ├── fc-spread-attributes.usage.tsx │ ├── generic-list.md │ ├── generic-list.stories.tsx │ ├── generic-list.tsx │ ├── generic-list.usage.tsx │ ├── index.ts │ ├── mouse-provider.md │ ├── mouse-provider.stories.tsx │ ├── mouse-provider.tsx │ ├── mouse-provider.usage.tsx │ ├── name-provider.md │ ├── name-provider.tsx │ └── name-provider.usage.tsx ├── connected │ ├── fc-counter-connected-bind-action-creators.tsx │ ├── fc-counter-connected-bind-action-creators.usage.tsx │ ├── fc-counter-connected-own-props.spec.tsx │ ├── fc-counter-connected-own-props.tsx │ ├── fc-counter-connected-own-props.usage.tsx │ ├── fc-counter-connected.tsx │ ├── fc-counter-connected.usage.tsx │ └── index.ts ├── context │ ├── theme-consumer-class.tsx │ ├── theme-consumer.tsx │ ├── theme-context.ts │ └── theme-provider.tsx ├── features │ ├── app │ │ └── epics.ts │ ├── counters │ │ ├── actions.ts │ │ ├── actions.usage.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ └── selectors.ts │ ├── todos-typesafe │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── models.ts │ │ ├── reducer.ts │ │ └── selectors.ts │ └── todos │ │ ├── __snapshots__ │ │ └── reducer.spec.ts.snap │ │ ├── actions.ts │ │ ├── constants.ts │ │ ├── epics.spec.ts │ │ ├── epics.ts │ │ ├── index.ts │ │ ├── models.ts │ │ ├── reducer-ta.ts │ │ ├── reducer.spec.ts │ │ ├── reducer.ts │ │ └── selectors.ts ├── hoc │ ├── index.ts │ ├── with-connected-count.tsx │ ├── with-connected-count.usage.tsx │ ├── with-error-boundary.tsx │ ├── with-error-boundary.usage.tsx │ ├── with-state.tsx │ └── with-state.usage.tsx ├── hooks │ ├── react-redux-hooks.tsx │ ├── use-reducer.tsx │ ├── use-state.tsx │ └── use-theme-context.tsx ├── index.css ├── index.tsx ├── layout │ ├── layout-footer.tsx │ ├── layout-header.tsx │ └── layout.tsx ├── models │ ├── index.ts │ ├── nominal-types.ts │ └── user.ts ├── react-app-env.d.ts ├── routes │ ├── home.tsx │ └── not-found.tsx ├── serviceWorker.ts ├── services │ ├── index.ts │ ├── local-storage-service.ts │ ├── logger-service.ts │ └── types.d.ts ├── store │ ├── hooks.ts │ ├── index.ts │ ├── redux-router.ts │ ├── root-action.ts │ ├── root-epic.ts │ ├── root-reducer.ts │ ├── store.ts │ ├── types.d.ts │ └── utils.ts └── storyshots.disabled-test.ts ├── styleguide ├── docs │ └── intro.md ├── package.json └── styleguide.config.js ├── tsconfig.json ├── tsconfig.test.json └── typings ├── augmentations.d.ts ├── globals.d.ts ├── modules.d.ts ├── redux-thunk └── index.d.ts └── redux └── index.d.ts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-redux-typescript-guide", 3 | "projectOwner": "piotrwitek", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "./CONTRIBUTORS.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "contributors": [ 12 | { 13 | "login": "piotrwitek", 14 | "name": "Piotrek Witek", 15 | "avatar_url": "https://avatars0.githubusercontent.com/u/739075?v=4", 16 | "profile": "https://github.com/piotrwitek", 17 | "contributions": [ 18 | "code", 19 | "doc", 20 | "ideas", 21 | "review", 22 | "question" 23 | ] 24 | }, 25 | { 26 | "login": "kazup01", 27 | "name": "Kazz Yokomizo", 28 | "avatar_url": "https://avatars3.githubusercontent.com/u/8602615?v=4", 29 | "profile": "https://github.com/kazup01", 30 | "contributions": [ 31 | "financial", 32 | "fundingFinding" 33 | ] 34 | }, 35 | { 36 | "login": "jakeboone02", 37 | "name": "Jake Boone", 38 | "avatar_url": "https://avatars1.githubusercontent.com/u/366438?v=4", 39 | "profile": "https://github.com/jakeboone02", 40 | "contributions": [ 41 | "doc" 42 | ] 43 | }, 44 | { 45 | "login": "amitdahan", 46 | "name": "Amit Dahan", 47 | "avatar_url": "https://avatars1.githubusercontent.com/u/9748762?v=4", 48 | "profile": "https://github.com/amitdahan", 49 | "contributions": [ 50 | "doc" 51 | ] 52 | }, 53 | { 54 | "login": "gulderov", 55 | "name": "gulderov", 56 | "avatar_url": "https://avatars1.githubusercontent.com/u/98167?v=4", 57 | "profile": "https://github.com/gulderov", 58 | "contributions": [ 59 | "doc" 60 | ] 61 | }, 62 | { 63 | "login": "emp823", 64 | "name": "Erik Pearson", 65 | "avatar_url": "https://avatars1.githubusercontent.com/u/1964212?v=4", 66 | "profile": "https://github.com/emp823", 67 | "contributions": [ 68 | "doc" 69 | ] 70 | }, 71 | { 72 | "login": "flymason", 73 | "name": "Bryan Mason", 74 | "avatar_url": "https://avatars1.githubusercontent.com/u/5342677?v=4", 75 | "profile": "https://github.com/flymason", 76 | "contributions": [ 77 | "doc" 78 | ] 79 | }, 80 | { 81 | "login": "chodorowicz", 82 | "name": "Jakub Chodorowicz", 83 | "avatar_url": "https://avatars1.githubusercontent.com/u/119451?v=4", 84 | "profile": "http://www.jakub.chodorowicz.pl/", 85 | "contributions": [ 86 | "code" 87 | ] 88 | }, 89 | { 90 | "login": "mleg", 91 | "name": "Oleg Maslov", 92 | "avatar_url": "https://avatars1.githubusercontent.com/u/7266431?v=4", 93 | "profile": "https://github.com/mleg", 94 | "contributions": [ 95 | "bug" 96 | ] 97 | }, 98 | { 99 | "login": "awestbro", 100 | "name": "Aaron Westbrook", 101 | "avatar_url": "https://avatars0.githubusercontent.com/u/3393293?v=4", 102 | "profile": "https://github.com/awestbro", 103 | "contributions": [ 104 | "bug" 105 | ] 106 | }, 107 | { 108 | "login": "peterblazejewicz", 109 | "name": "Peter Blazejewicz", 110 | "avatar_url": "https://avatars3.githubusercontent.com/u/14539?v=4", 111 | "profile": "http://www.linkedin.com/in/peterblazejewicz", 112 | "contributions": [ 113 | "doc" 114 | ] 115 | }, 116 | { 117 | "login": "rubysolo", 118 | "name": "Solomon White", 119 | "avatar_url": "https://avatars3.githubusercontent.com/u/1642?v=4", 120 | "profile": "https://github.com/rubysolo", 121 | "contributions": [ 122 | "doc" 123 | ] 124 | }, 125 | { 126 | "login": "pino", 127 | "name": "Levi Rocha", 128 | "avatar_url": "https://avatars2.githubusercontent.com/u/8838006?v=4", 129 | "profile": "https://github.com/pino", 130 | "contributions": [ 131 | "doc" 132 | ] 133 | }, 134 | { 135 | "login": "loadbalance-sudachi-kun", 136 | "name": "Sudachi-kun", 137 | "avatar_url": "https://avatars1.githubusercontent.com/u/41281835?v=4", 138 | "profile": "http://cloudnative.co.jp", 139 | "contributions": [ 140 | "financial" 141 | ] 142 | }, 143 | { 144 | "login": "sosukesuzuki", 145 | "name": "Sosuke Suzuki", 146 | "avatar_url": "https://avatars1.githubusercontent.com/u/14838850?v=4", 147 | "profile": "http://sosukesuzuki.github.io", 148 | "contributions": [ 149 | "code" 150 | ] 151 | }, 152 | { 153 | "login": "chillitom", 154 | "name": "Tom Rathbone", 155 | "avatar_url": "https://avatars0.githubusercontent.com/u/74433?v=4", 156 | "profile": "https://github.com/chillitom", 157 | "contributions": [ 158 | "doc" 159 | ] 160 | }, 161 | { 162 | "login": "arshadkazmi42", 163 | "name": "Arshad Kazmi", 164 | "avatar_url": "https://avatars3.githubusercontent.com/u/4654382?v=4", 165 | "profile": "https://arshadkazmi42.github.io/", 166 | "contributions": [ 167 | "doc" 168 | ] 169 | }, 170 | { 171 | "login": "JeongUkJae", 172 | "name": "JeongUkJae", 173 | "avatar_url": "https://avatars1.githubusercontent.com/u/8815362?v=4", 174 | "profile": "https://jeongukjae.github.io", 175 | "contributions": [ 176 | "doc" 177 | ] 178 | } 179 | ] 180 | } 181 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: piotrekwitek # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: piotrwitek # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://www.buymeacoffee.com/piotrekwitek"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | ## Description 7 | 8 | 9 | ## Steps to Reproduce 10 | 17 | 18 | ## Expected behavior 19 | 20 | 21 | ## Suggested solution(s) 22 | 23 | 24 | ## Project Dependencies 25 | - TypeScript Version: X.X.X 26 | - tsconfig.json: 27 | 28 | 29 | ## Environment (optional) 30 | 31 | - Browser and Version: XXX 32 | - OS: XXX 33 | - Node Version: XXX 34 | - Package Manager and Version: XXX 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Have a question? Please check our spectrum community chat. 4 | --- 5 | 6 | First of all please check our spectrum community chat and we recommend to ask your question there for a quickest response and the indexing in search engines: 7 | - https://spectrum.chat/react-redux-ts 8 | 9 | The only good reason to use issue tracker for your questions would be for "special requests" that doesn't fit into bug reports and feature requests categories. 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## Is your feature request related to a real problem or use-case? 7 | 8 | 9 | ## Describe a solution including usage in code example 10 | 11 | 12 | ## Who does this impact? Who is this for? 13 | 14 | 15 | ## Describe alternatives you've considered (optional) 16 | 17 | 18 | ## Additional context (optional) 19 | 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Description 5 | 6 | 7 | ## Related issues: 8 | - Resolved #XXX 9 | 10 | ## Checklist 11 | 12 | * [ ] I have read [CONTRIBUTING.md](https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/CONTRIBUTING.md) 13 | * [ ] I have edited `README_SOURCE.md` (NOT `README.md`) 14 | * [ ] I have run CI script locally `npm run ci-check` to generate an updated `README.md` 15 | * [ ] I have linked all related issues above 16 | * [ ] I have rebased my branch 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | temp/ 40 | playground/dist 41 | playground/storybook-static 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3080", 12 | "webRoot": "${workspaceFolder}/playground/src", 13 | "sourceMapPathOverrides": { 14 | "webpack:///src/*": "${webRoot}/*" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at piotrek.witek@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## General 4 | 1. Make sure you have read and understand the **Goals** section to be aligned with project goals. 5 | 2. Before submitting a PR please comment in the relevant issue (or create a new one if it doesn't exist yet) to discuss all the requirements (this will prevent rejecting the PR and wasting your work). 6 | 3. All workflow scripts (prettier, linter, tests) must pass successfully (it is run automatically on CI and will fail on github checks). 7 | 8 | ## Edit `README_SOURCE.md` to generate an updated `README.md` 9 | Don't edit `README.md` directly - it is generated automatically from `README_SOURCE.md` using an automated script. 10 | - Use `sh ./generate-readme.sh` script to generate updated `README.md` (this will inject code examples using type-checked source files from the `/playground` folder) 11 | - So to make changes in code examples edit source files in `/playground` folder 12 | 13 | **Source code inject directives:** 14 | ``` 15 | # Inject code block with highlighter 16 | ::codeblock='playground/src/components/fc-counter.tsx':: 17 | 18 | # Inject code block with highlighter and expander 19 | ::expander='playground/src/components/fc-counter.usage.tsx':: 20 | ``` 21 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 2 | 3 | 4 | 5 | | [
Piotrek Witek](https://github.com/piotrwitek)
[💻](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=piotrwitek "Code") [📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=piotrwitek "Documentation") [🤔](#ideas-piotrwitek "Ideas, Planning, & Feedback") [👀](#review-piotrwitek "Reviewed Pull Requests") [💬](#question-piotrwitek "Answering Questions") | [
Kazz Yokomizo](https://github.com/kazup01)
[💵](#financial-kazup01 "Financial") [🔍](#fundingFinding-kazup01 "Funding Finding") | [
Jake Boone](https://github.com/jakeboone02)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=jakeboone02 "Documentation") | [
Amit Dahan](https://github.com/amitdahan)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=amitdahan "Documentation") | [
gulderov](https://github.com/gulderov)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=gulderov "Documentation") | [
Erik Pearson](https://github.com/emp823)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=emp823 "Documentation") | [
Bryan Mason](https://github.com/flymason)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=flymason "Documentation") | 6 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 7 | | [
Jakub Chodorowicz](http://www.jakub.chodorowicz.pl/)
[💻](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=chodorowicz "Code") | [
Oleg Maslov](https://github.com/mleg)
[🐛](https://github.com/piotrwitek/react-redux-typescript-guide/issues?q=author%3Amleg "Bug reports") | [
Aaron Westbrook](https://github.com/awestbro)
[🐛](https://github.com/piotrwitek/react-redux-typescript-guide/issues?q=author%3Aawestbro "Bug reports") | [
Peter Blazejewicz](http://www.linkedin.com/in/peterblazejewicz)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=peterblazejewicz "Documentation") | [
Solomon White](https://github.com/rubysolo)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=rubysolo "Documentation") | [
Levi Rocha](https://github.com/pino)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=pino "Documentation") | [
Sudachi-kun](http://cloudnative.co.jp)
[💵](#financial-loadbalance-sudachi-kun "Financial") | 8 | | [
Sosuke Suzuki](http://sosukesuzuki.github.io)
[💻](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=sosukesuzuki "Code") | [
Tom Rathbone](https://github.com/chillitom)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=chillitom "Documentation") | [
Arshad Kazmi](https://arshadkazmi42.github.io/)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=arshadkazmi42 "Documentation") | [
JeongUkJae](https://jeongukjae.github.io)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=JeongUkJae "Documentation") | 9 | 10 | 11 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io) 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_SOURCE.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # React & Redux in TypeScript - Complete Guide 4 | 5 | _"This guide is a **living compendium** documenting the most important patterns and recipes on how to use **React** (and its Ecosystem) in a **functional style** using **TypeScript**. It will help you make your code **completely type-safe** while focusing on **inferring the types from implementation** so there is less noise coming from excessive type annotations and it's easier to write and maintain correct types in the long run."_ 6 | 7 | [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/react-redux-ts) 8 | [![Join the chat at https://gitter.im/react-redux-typescript-guide/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/react-redux-typescript-guide/Lobby) 9 | 10 | _Found it useful? Want more updates?_ 11 | 12 | [**Show your support by giving a :star:**](https://github.com/piotrwitek/react-redux-typescript-guide/stargazers) 13 | 14 | 15 | Buy Me a Coffee 16 | 17 | 18 | Become a Patron 19 | 20 | 21 |

22 | 23 | ## **What's new?** 24 | 25 | :tada: _Now updated to support **TypeScript v4.6**_ :tada: 26 | :rocket: _Updated to `typesafe-actions@5.x` :rocket: 27 | 28 |

29 | 30 |
31 | 32 | ### **Goals** 33 | 34 | - Complete type safety (with [`--strict`](https://www.typescriptlang.org/docs/handbook/compiler-options.html) flag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking with `any` type) 35 | - Make type annotations concise by eliminating redundancy in types using advanced TypeScript Language features like **Type Inference** and **Control flow analysis** 36 | - Reduce repetition and complexity of types with TypeScript focused [complementary libraries](#react-redux-typescript-ecosystem) 37 | 38 | ### **React, Redux, Typescript Ecosystem** 39 | 40 | - [typesafe-actions](https://github.com/piotrwitek/typesafe-actions) - Typesafe utilities for "action-creators" in Redux / Flux Architecture 41 | - [utility-types](https://github.com/piotrwitek/utility-types) - Collection of generic types for TypeScript, complementing built-in mapped types and aliases - think lodash for reusable types. 42 | - [react-redux-typescript-scripts](https://github.com/piotrwitek/react-redux-typescript-scripts) - dev-tools configuration files shared between projects based on this guide 43 | 44 | ### **Examples** 45 | 46 | - Todo-App playground: [Codesandbox](https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox) 47 | - React, Redux, TypeScript - RealWorld App: [Github](https://github.com/piotrwitek/react-redux-typescript-realworld-app) | [Demo](https://react-redux-typescript-realworld-app.netlify.com/) 48 | 49 | ### **Playground Project** 50 | 51 | [![Build Status](https://semaphoreci.com/api/v1/piotrekwitek/react-redux-typescript-guide/branches/master/shields_badge.svg)](https://semaphoreci.com/piotrekwitek/react-redux-typescript-guide) 52 | 53 | Check out our Playground Project located in the `/playground` folder. It contains all source files of the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type-definitions (like `@types/react` or `@types/react-redux`) to ensure the examples are up-to-date and not broken with updated definitions (It's based on `create-react-app --typescript`). 54 | > Playground project was created so that you can simply clone the repository locally and immediately play around with all the component patterns found in the guide. It will help you to learn all the examples from this guide in a real project environment without the need to create complicated environment setup by yourself. 55 | 56 | ## Contributing Guide 57 | 58 | You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide: [CONTRIBUTING.md](/CONTRIBUTING.md) 59 | 60 | ## Funding 61 | 62 | You can also help by funding issues. 63 | Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform. 64 | 65 | I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it. 66 | 67 | [![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/76996763) 68 | 69 | --- 70 | 71 | 🌟 - _New or updated section_ 72 | 73 | ## Table of Contents 74 | 75 | 76 | 77 | 78 | 79 | - [React Types Cheatsheet](#react-types-cheatsheet) 80 | - [`React.FC` | `React.FunctionComponent`](#reactfcprops--reactfunctioncomponentprops) 81 | - [`React.Component`](#reactcomponentprops-state) 82 | - [`React.ComponentType`](#reactcomponenttypeprops) 83 | - [`React.ComponentProps`](#reactcomponentpropstypeof-xxx) 84 | - [`React.ReactElement` | `JSX.Element`](#reactreactelement--jsxelement) 85 | - [`React.ReactNode`](#reactreactnode) 86 | - [`React.CSSProperties`](#reactcssproperties) 87 | - [`React.XXXHTMLAttributes`](#reactxxxhtmlattributeshtmlxxxelement) 88 | - [`React.ReactEventHandler`](#reactreacteventhandlerhtmlxxxelement) 89 | - [`React.XXXEvent`](#reactxxxeventhtmlxxxelement) 90 | - [React](#react) 91 | - [Function Components - FC](#function-components---fc) 92 | - [- Counter Component](#--counter-component) 93 | - [- Counter Component with default props](#--counter-component-with-default-props) 94 | - [- Spreading attributes in Component](#--spreading-attributes-in-component) 95 | - [Class Components](#class-components) 96 | - [- Class Counter Component](#--class-counter-component) 97 | - [- Class Component with default props](#--class-component-with-default-props) 98 | - [Generic Components](#generic-components) 99 | - [- Generic List Component](#--generic-list-component) 100 | - [Hooks](#hooks) 101 | - [- useState](#--usestate) 102 | - [- useContext](#--usecontext) 103 | - [- useReducer](#--usereducer) 104 | - [Render Props](#render-props) 105 | - [- Name Provider Component](#--name-provider-component) 106 | - [- Mouse Provider Component](#--mouse-provider-component) 107 | - [Higher-Order Components](#higher-order-components) 108 | - [- HOC wrapping a component](#--hoc-wrapping-a-component) 109 | - [- HOC wrapping a component and injecting props](#--hoc-wrapping-a-component-and-injecting-props) 110 | - [- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟](#--nested-hoc---wrapping-a-component-injecting-props-and-connecting-to-redux-) 111 | - [Redux Connected Components](#redux-connected-components) 112 | - [- Redux connected counter](#--redux-connected-counter) 113 | - [- Redux connected counter with own props](#--redux-connected-counter-with-own-props) 114 | - [- Redux connected counter via hooks](#--redux-connected-counter-via-hooks) 115 | - [- Redux connected counter with `redux-thunk` integration](#--redux-connected-counter-with-redux-thunk-integration) 116 | - [Context](#context) 117 | - [ThemeContext](#themecontext) 118 | - [ThemeProvider](#themeprovider) 119 | - [ThemeConsumer](#themeconsumer) 120 | - [ThemeConsumer in class component](#themeconsumer-in-class-component) 121 | - [Redux](#redux) 122 | - [Store Configuration](#store-configuration) 123 | - [Create Global Store Types](#create-global-store-types) 124 | - [Create Store](#create-store) 125 | - [Action Creators 🌟](#action-creators-) 126 | - [Reducers](#reducers) 127 | - [State with Type-level Immutability](#state-with-type-level-immutability) 128 | - [Typing reducer](#typing-reducer) 129 | - [Typing reducer with `typesafe-actions`](#typing-reducer-with-typesafe-actions) 130 | - [Testing reducer](#testing-reducer) 131 | - [Async Flow with `redux-observable`](#async-flow-with-redux-observable) 132 | - [Typing epics](#typing-epics) 133 | - [Testing epics](#testing-epics) 134 | - [Selectors with `reselect`](#selectors-with-reselect) 135 | - [Connect with `react-redux`](#connect-with-react-redux) 136 | - [Typing connected component](#typing-connected-component) 137 | - [Typing `useSelector` and `useDispatch`](#typing-useselector-and-usedispatch) 138 | - [Typing connected component with `redux-thunk` integration](#typing-connected-component-with-redux-thunk-integration) 139 | - [Configuration & Dev Tools](#configuration--dev-tools) 140 | - [Common Npm Scripts](#common-npm-scripts) 141 | - [tsconfig.json](#tsconfigjson) 142 | - [TSLib](#tslib) 143 | - [ESLint](#eslint) 144 | - [.eslintrc.js](#eslintrcjs) 145 | - [Jest](#jest) 146 | - [jest.config.json](#jestconfigjson) 147 | - [jest.stubs.js](#jeststubsjs) 148 | - [Style Guides](#style-guides) 149 | - [react-styleguidist](#react-styleguidist) 150 | - [FAQ](#faq) 151 | - [Ambient Modules](#ambient-modules) 152 | - [Imports in ambient modules](#imports-in-ambient-modules) 153 | - [Type-Definitions](#type-definitions) 154 | - [Missing type-definitions error](#missing-type-definitions-error) 155 | - [Using custom `d.ts` files for npm modules](#using-custom-dts-files-for-npm-modules) 156 | - [Type Augmentation](#type-augmentation) 157 | - [Augmenting library internal declarations - using relative import](#augmenting-library-internal-declarations---using-relative-import) 158 | - [Augmenting library public declarations - using node_modules import](#augmenting-library-public-declarations---using-node_modules-import) 159 | - [Misc](#misc) 160 | - [- should I still use React.PropTypes in TS?](#--should-i-still-use-reactproptypes-in-ts) 161 | - [- when to use `interface` declarations and when `type` aliases?](#--when-to-use-interface-declarations-and-when-type-aliases) 162 | - [- what's better default or named exports?](#--whats-better-default-or-named-exports) 163 | - [- how to best initialize class instance or static properties?](#--how-to-best-initialize-class-instance-or-static-properties) 164 | - [- how to best declare component handler functions?](#--how-to-best-declare-component-handler-functions) 165 | - [Tutorials & Articles](#tutorials--articles) 166 | - [Contributors](#contributors) 167 | 168 | 169 | 170 | --- 171 | 172 | # Installation 173 | 174 | ## Types for React & Redux 175 | 176 | ``` 177 | npm i -D @types/react @types/react-dom @types/react-redux 178 | ``` 179 | 180 | "react" - `@types/react` 181 | "react-dom" - `@types/react-dom` 182 | "redux" - (types included with npm package)* 183 | "react-redux" - `@types/react-redux` 184 | 185 | > *NB: Guide is based on types for Redux >= v4.x.x. 186 | 187 | [⇧ back to top](#table-of-contents) 188 | 189 | --- 190 | 191 | ## React Types Cheatsheet 192 | 193 | ### `React.FC` | `React.FunctionComponent` 194 | 195 | Type representing a functional component 196 | 197 | ```tsx 198 | const MyComponent: React.FC = ... 199 | ``` 200 | 201 | ### `React.Component` 202 | 203 | Type representing a class component 204 | 205 | ```tsx 206 | class MyComponent extends React.Component { ... 207 | ``` 208 | 209 | ### `React.ComponentType` 210 | 211 | Type representing union of (`React.FC | React.Component`) - used in HOC 212 | 213 | ```tsx 214 | const withState =

( 215 | WrappedComponent: React.ComponentType

, 216 | ) => { ... 217 | ``` 218 | 219 | ### `React.ComponentProps` 220 | 221 | Gets Props type of a specified component XXX (WARNING: does not work with statically declared default props and generic props) 222 | 223 | ```tsx 224 | type MyComponentProps = React.ComponentProps; 225 | ``` 226 | 227 | ### `React.ReactElement` | `JSX.Element` 228 | 229 | Type representing a concept of React Element - representation of a native DOM component (e.g. `

`), or a user-defined composite component (e.g. ``) 230 | 231 | ```tsx 232 | const elementOnly: React.ReactElement =
|| ; 233 | ``` 234 | 235 | ### `React.ReactNode` 236 | 237 | Type representing any possible type of React node (basically ReactElement (including Fragments and Portals) + primitive JS types) 238 | 239 | ```tsx 240 | const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined ||
|| ; 241 | const Component = ({ children: React.ReactNode }) => ... 242 | ``` 243 | 244 | ### `React.CSSProperties` 245 | 246 | Type representing style object in JSX - for css-in-js styles 247 | 248 | ```tsx 249 | const styles: React.CSSProperties = { flexDirection: 'row', ... 250 | const element =
` 254 | 255 | Type representing HTML attributes of specified HTML Element - for extending HTML Elements 256 | 257 | ```tsx 258 | const Input: React.FC> = props => { ... } 259 | 260 | 261 | ``` 262 | 263 | ### `React.ReactEventHandler` 264 | 265 | Type representing generic event handler - for declaring event handlers 266 | 267 | ```tsx 268 | const handleChange: React.ReactEventHandler = (ev) => { ... } 269 | 270 | 271 | ``` 272 | 273 | ### `React.XXXEvent` 274 | 275 | Type representing more specific event. Some common event examples: `ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent`. 276 | 277 | ```tsx 278 | const handleChange = (ev: React.MouseEvent) => { ... } 279 | 280 |
281 | ``` 282 | 283 | In code above `React.MouseEvent` is type of mouse event, and this event happened on `HTMLDivElement` 284 | 285 | [⇧ back to top](#table-of-contents) 286 | 287 | --- 288 | 289 | # React 290 | 291 | ## Function Components - FC 292 | 293 | ### - Counter Component 294 | 295 | ::codeblock='playground/src/components/fc-counter.tsx':: 296 | 297 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fccounter) 298 | 299 | [⇧ back to top](#table-of-contents) 300 | 301 | ### - Counter Component with default props 302 | 303 | ::codeblock='playground/src/components/fc-counter-with-default-props.tsx':: 304 | 305 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fccounterwithdefaultprops) 306 | 307 | [⇧ back to top](#table-of-contents) 308 | 309 | ### - [Spreading attributes](https://facebook.github.io/react/docs/jsx-in-depth.html#spread-attributes) in Component 310 | 311 | ::codeblock='playground/src/components/fc-spread-attributes.tsx':: 312 | 313 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fcspreadattributes) 314 | 315 | [⇧ back to top](#table-of-contents) 316 | 317 | --- 318 | 319 | ## Class Components 320 | 321 | ### - Class Counter Component 322 | 323 | ::codeblock='playground/src/components/class-counter.tsx':: 324 | 325 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#classcounter) 326 | 327 | [⇧ back to top](#table-of-contents) 328 | 329 | ### - Class Component with default props 330 | 331 | ::codeblock='playground/src/components/class-counter-with-default-props.tsx':: 332 | 333 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#classcounterwithdefaultprops) 334 | 335 | [⇧ back to top](#table-of-contents) 336 | 337 | --- 338 | 339 | ## Generic Components 340 | 341 | - easily create typed component variations and reuse common logic 342 | - common use case is a generic list components 343 | 344 | ### - Generic List Component 345 | 346 | ::codeblock='playground/src/components/generic-list.tsx':: 347 | 348 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#genericlist) 349 | 350 | [⇧ back to top](#table-of-contents) 351 | 352 | --- 353 | 354 | ## Hooks 355 | 356 | > 357 | 358 | ### - useState 359 | 360 | > 361 | 362 | ::codeblock='playground/src/hooks/use-state.tsx':: 363 | 364 | [⇧ back to top](#table-of-contents) 365 | 366 | ### - useContext 367 | 368 | > 369 | 370 | ::codeblock='playground/src/hooks/use-theme-context.tsx':: 371 | 372 | [⇧ back to top](#table-of-contents) 373 | 374 | ### - useReducer 375 | 376 | > 377 | 378 | ::codeblock='playground/src/hooks/use-reducer.tsx':: 379 | 380 | [⇧ back to top](#table-of-contents) 381 | 382 | --- 383 | 384 | ## Render Props 385 | 386 | > 387 | 388 | ### - Name Provider Component 389 | 390 | Simple component using children as a render prop 391 | 392 | ::codeblock='playground/src/components/name-provider.tsx':: 393 | 394 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#nameprovider) 395 | 396 | [⇧ back to top](#table-of-contents) 397 | 398 | ### - Mouse Provider Component 399 | 400 | `Mouse` component found in [Render Props React Docs](https://reactjs.org/docs/render-props.html#use-render-props-for-cross-cutting-concerns) 401 | 402 | ::codeblock='playground/src/components/mouse-provider.tsx':: 403 | 404 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#mouseprovider) 405 | 406 | [⇧ back to top](#table-of-contents) 407 | 408 | --- 409 | 410 | ## Higher-Order Components 411 | 412 | > 413 | 414 | ### - HOC wrapping a component 415 | 416 | Adds state to a stateless counter 417 | 418 | ::codeblock='playground/src/hoc/with-state.tsx':: 419 | ::expander='playground/src/hoc/with-state.usage.tsx':: 420 | 421 | [⇧ back to top](#table-of-contents) 422 | 423 | ### - HOC wrapping a component and injecting props 424 | 425 | Adds error handling using componentDidCatch to any component 426 | 427 | ::codeblock='playground/src/hoc/with-error-boundary.tsx':: 428 | ::expander='playground/src/hoc/with-error-boundary.usage.tsx':: 429 | 430 | [⇧ back to top](#table-of-contents) 431 | 432 | ### - Nested HOC - wrapping a component, injecting props and connecting to redux 🌟 433 | 434 | Adds error handling using componentDidCatch to any component 435 | 436 | ::codeblock='playground/src/hoc/with-connected-count.tsx':: 437 | ::expander='playground/src/hoc/with-connected-count.usage.tsx':: 438 | 439 | [⇧ back to top](#table-of-contents) 440 | 441 | --- 442 | 443 | ## Redux Connected Components 444 | 445 | ### - Redux connected counter 446 | 447 | ::codeblock='playground/src/connected/fc-counter-connected.tsx':: 448 | ::expander='playground/src/connected/fc-counter-connected.usage.tsx':: 449 | 450 | [⇧ back to top](#table-of-contents) 451 | 452 | ### - Redux connected counter with own props 453 | 454 | ::codeblock='playground/src/connected/fc-counter-connected-own-props.tsx':: 455 | ::expander='playground/src/connected/fc-counter-connected-own-props.usage.tsx':: 456 | 457 | [⇧ back to top](#table-of-contents) 458 | 459 | ### - Redux connected counter via hooks 460 | 461 | ::codeblock='playground/src/hooks/react-redux-hooks.tsx':: 462 | 463 | [⇧ back to top](#table-of-contents) 464 | 465 | ### - Redux connected counter with `redux-thunk` integration 466 | 467 | ::codeblock='playground/src/connected/fc-counter-connected-bind-action-creators.tsx':: 468 | ::expander='playground/src/connected/fc-counter-connected-bind-action-creators.usage.tsx':: 469 | 470 | [⇧ back to top](#table-of-contents) 471 | 472 | ## Context 473 | 474 | > 475 | 476 | ### ThemeContext 477 | 478 | ::codeblock='playground/src/context/theme-context.ts':: 479 | 480 | [⇧ back to top](#table-of-contents) 481 | 482 | ### ThemeProvider 483 | 484 | ::codeblock='playground/src/context/theme-provider.tsx':: 485 | 486 | [⇧ back to top](#table-of-contents) 487 | 488 | ### ThemeConsumer 489 | 490 | ::codeblock='playground/src/context/theme-consumer.tsx':: 491 | 492 | ### ThemeConsumer in class component 493 | 494 | ::codeblock='playground/src/context/theme-consumer-class.tsx':: 495 | 496 | [Implementation with Hooks](#--usecontext) 497 | 498 | [⇧ back to top](#table-of-contents) 499 | 500 | 501 | --- 502 | 503 | # Redux 504 | 505 | ## Store Configuration 506 | 507 | ### Create Global Store Types 508 | 509 | #### `RootState` - type representing root state-tree 510 | 511 | Can be imported in connected components to provide type-safety to Redux `connect` function 512 | 513 | #### `RootAction` - type representing union type of all action objects 514 | 515 | Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics 516 | 517 | ::codeblock='playground/src/store/types.d.ts':: 518 | 519 | [⇧ back to top](#table-of-contents) 520 | 521 | ### Create Store 522 | 523 | When creating a store instance we don't need to provide any additional types. It will set-up a **type-safe Store instance** using type inference. 524 | > The resulting store instance methods like `getState` or `dispatch` will be type checked and will expose all type errors 525 | 526 | ::codeblock='playground/src/store/store.ts':: 527 | 528 | --- 529 | 530 | ## Action Creators 🌟 531 | 532 | > We'll be using a battle-tested helper library [`typesafe-actions`](https://github.com/piotrwitek/typesafe-actions#typesafe-actions) [![Latest Stable Version](https://img.shields.io/npm/v/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions) [![NPM Downloads](https://img.shields.io/npm/dt/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions) that's designed to make it easy and fun working with **Redux** in **TypeScript**. 533 | 534 | > To learn more please check this in-depth tutorial: [Typesafe-Actions - Tutorial](https://github.com/piotrwitek/typesafe-actions#tutorial)! 535 | 536 | A solution below is using a simple factory function to automate the creation of type-safe action creators. The goal is to decrease maintenance effort and reduce code repetition of type annotations for actions and creators. The result is completely typesafe action-creators and their actions. 537 | 538 | ::codeblock='playground/src/features/counters/actions.ts':: 539 | ::expander='playground/src/features/counters/actions.usage.ts':: 540 | 541 | [⇧ back to top](#table-of-contents) 542 | 543 | --- 544 | 545 | ## Reducers 546 | 547 | ### State with Type-level Immutability 548 | 549 | Declare reducer `State` type with `readonly` modifier to get compile time immutability 550 | 551 | ```ts 552 | export type State = { 553 | readonly counter: number; 554 | readonly todos: ReadonlyArray; 555 | }; 556 | ``` 557 | 558 | Readonly modifier allow initialization, but will not allow reassignment by highlighting compiler errors 559 | 560 | ```ts 561 | export const initialState: State = { 562 | counter: 0, 563 | }; // OK 564 | 565 | initialState.counter = 3; // TS Error: cannot be mutated 566 | ``` 567 | 568 | It's great for **Arrays in JS** because it will error when using mutator methods like (`push`, `pop`, `splice`, ...), but it'll still allow immutable methods like (`concat`, `map`, `slice`,...). 569 | 570 | ```ts 571 | state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray' 572 | const newTodos = state.todos.concat('Learn about tagged union types') // OK 573 | ``` 574 | 575 | #### Caveat - `Readonly` is not recursive 576 | 577 | This means that the `readonly` modifier doesn't propagate immutability down the nested structure of objects. You'll need to mark each property on each level explicitly. 578 | 579 | > **TIP:** use `Readonly` or `ReadonlyArray` [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) 580 | 581 | ```ts 582 | export type State = Readonly<{ 583 | counterPairs: ReadonlyArray>, 587 | }>; 588 | 589 | state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated 590 | state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated 591 | state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated 592 | ``` 593 | 594 | #### Solution - recursive `Readonly` is called `DeepReadonly` 595 | 596 | To fix this we can use [`DeepReadonly`](https://github.com/piotrwitek/utility-types#deepreadonlyt) type (available from `utility-types`). 597 | 598 | ```ts 599 | import { DeepReadonly } from 'utility-types'; 600 | 601 | export type State = DeepReadonly<{ 602 | containerObject: { 603 | innerValue: number, 604 | numbers: number[], 605 | } 606 | }>; 607 | 608 | state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated 609 | state.containerObject.innerValue = 1; // TS Error: cannot be mutated 610 | state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods 611 | ``` 612 | 613 | [⇧ back to top](#table-of-contents) 614 | 615 | ### Typing reducer 616 | 617 | > to understand following section make sure to learn about [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html), [Control flow analysis](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#control-flow-based-type-analysis) and [Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types) 618 | 619 | ::codeblock='playground/src/features/todos/reducer.ts':: 620 | 621 | [⇧ back to top](#table-of-contents) 622 | 623 | ### Typing reducer with `typesafe-actions` 624 | 625 | > Notice we are not required to use any generic type parameter in the API. Try to compare it with regular reducer as they are equivalent. 626 | 627 | ::codeblock='playground/src/features/todos/reducer-ta.ts':: 628 | 629 | [⇧ back to top](#table-of-contents) 630 | 631 | ### Testing reducer 632 | 633 | ::codeblock='playground/src/features/todos/reducer.spec.ts':: 634 | 635 | [⇧ back to top](#table-of-contents) 636 | 637 | --- 638 | 639 | ## Async Flow with `redux-observable` 640 | 641 | ### Typing epics 642 | 643 | ::codeblock='playground/src/features/todos/epics.ts':: 644 | 645 | [⇧ back to top](#table-of-contents) 646 | 647 | ### Testing epics 648 | 649 | ::codeblock='playground/src/features/todos/epics.spec.ts':: 650 | 651 | [⇧ back to top](#table-of-contents) 652 | 653 | --- 654 | 655 | ## Selectors with `reselect` 656 | 657 | ::codeblock='playground/src/features/todos/selectors.ts':: 658 | 659 | [⇧ back to top](#table-of-contents) 660 | 661 | --- 662 | 663 | ## Connect with `react-redux` 664 | 665 | ### Typing connected component 666 | 667 | _**NOTE**: Below you'll find a short explanation of concepts behind using `connect` with TypeScript. For more detailed examples please check [Redux Connected Components](#redux-connected-components) section._ 668 | 669 | ```tsx 670 | import MyTypes from 'MyTypes'; 671 | 672 | import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux'; 673 | import { connect } from 'react-redux'; 674 | 675 | import { countersActions } from '../features/counters'; 676 | import { FCCounter } from '../components'; 677 | 678 | // Type annotation for "state" argument is mandatory to check 679 | // the correct shape of state object and injected props you can also 680 | // extend connected component Props interface by annotating `ownProps` argument 681 | const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({ 682 | count: state.counters.reduxCounter, 683 | }); 684 | 685 | // "dispatch" argument needs an annotation to check the correct shape 686 | // of an action object when using dispatch function 687 | const mapDispatchToProps = (dispatch: Dispatch) => 688 | bindActionCreators({ 689 | onIncrement: countersActions.increment, 690 | }, dispatch); 691 | 692 | // shorter alternative is to use an object instead of mapDispatchToProps function 693 | const dispatchToProps = { 694 | onIncrement: countersActions.increment, 695 | }; 696 | 697 | // Notice we don't need to pass any generic type parameters to neither 698 | // the connect function below nor map functions declared above 699 | // because type inference will infer types from arguments annotations automatically 700 | // This is much cleaner and idiomatic approach 701 | export const FCCounterConnected = 702 | connect(mapStateToProps, mapDispatchToProps)(FCCounter); 703 | 704 | // You can add extra layer of validation of your action creators 705 | // by using bindActionCreators generic type parameter and RootAction type 706 | const mapDispatchToProps = (dispatch: Dispatch) => 707 | bindActionCreators>({ 708 | invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... } 709 | }, dispatch); 710 | 711 | ``` 712 | 713 | [⇧ back to top](#table-of-contents) 714 | 715 | ### Typing `useSelector` and `useDispatch` 716 | 717 | ::codeblock='playground/src/store/hooks.ts':: 718 | 719 | [⇧ back to top](#table-of-contents) 720 | 721 | ### Typing connected component with `redux-thunk` integration 722 | 723 | _**NOTE**: When using thunk action creators you need to use `bindActionCreators`. Only this way you can get corrected dispatch props type signature like below.*_ 724 | 725 | _**WARNING**: As of now (Apr 2019) `bindActionCreators` signature of the latest `redux-thunk` release will not work as below, you need to use our modified type definitions that you can find here [`/playground/typings/redux-thunk/index.d.ts`](./playground/typings/redux-thunk/index.d.ts) and then add `paths` overload in your tsconfig like this: [`"paths":{"redux-thunk":["typings/redux-thunk"]}`](./playground/tsconfig.json)._ 726 | 727 | ```tsx 728 | const thunkAsyncAction = () => async (dispatch: Dispatch): Promise => { 729 | // dispatch actions, return Promise, etc. 730 | } 731 | 732 | const mapDispatchToProps = (dispatch: Dispatch) => 733 | bindActionCreators( 734 | { 735 | thunkAsyncAction, 736 | }, 737 | dispatch 738 | ); 739 | 740 | type DispatchProps = ReturnType; 741 | // { thunkAsyncAction: () => Promise; } 742 | 743 | /* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */ 744 | // { thunkAsyncAction: () => (dispatch: Dispatch) => Promise; } 745 | ``` 746 | 747 | [⇧ back to top](#table-of-contents) 748 | 749 | --- 750 | 751 | # Configuration & Dev Tools 752 | 753 | ## Common Npm Scripts 754 | 755 | > Common TS-related npm scripts shared across projects 756 | 757 | ```json 758 | "prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease fix code formatting by running:\nnpm run prettier:fix\n'; exit 1)", 759 | "prettier:fix": "prettier --write 'src/**/*.ts'", 760 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 761 | "tsc": "tsc -p ./ --noEmit", 762 | "tsc:watch": "tsc -p ./ --noEmit -w", 763 | "test": "jest --config jest.config.json", 764 | "test:watch": "jest --config jest.config.json --watch", 765 | "test:update": "jest --config jest.config.json -u" 766 | "ci-check": "npm run prettier && npm run lint && npm run tsc && npm run test", 767 | ``` 768 | 769 | [⇧ back to top](#table-of-contents) 770 | 771 | ## tsconfig.json 772 | 773 | We have recommended `tsconfig.json` that you can easily add to your project thanks to [`react-redux-typescript-scripts`](https://github.com/piotrwitek/react-redux-typescript-scripts) package. 774 | 775 | ::expander='playground/tsconfig.json':: 776 | 777 | [⇧ back to top](#table-of-contents) 778 | 779 | ## TSLib 780 | 781 | This library will cut down on your bundle size, thanks to using external runtime helpers instead of adding them per each file. 782 | 783 | > 784 | 785 | > Installation 786 | `npm i tslib` 787 | 788 | 789 | Then add this to your `tsconfig.json`: 790 | 791 | ```ts 792 | "compilerOptions": { 793 | "importHelpers": true 794 | } 795 | ``` 796 | 797 | [⇧ back to top](#table-of-contents) 798 | 799 | ## ESLint 800 | 801 | We have recommended config that will automatically add a parser & plugin for TypeScript thanks to [`react-redux-typescript-scripts`](https://github.com/piotrwitek/react-redux-typescript-scripts) package. 802 | 803 | > 804 | 805 | > Installation 806 | `npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin` 807 | 808 | 809 | ### .eslintrc.js 810 | 811 | ::expander='playground/.eslintrc.js':: 812 | 813 | [⇧ back to top](#table-of-contents) 814 | 815 | ## Jest 816 | 817 | > 818 | 819 | > Installation 820 | `npm i -D jest ts-jest @types/jest` 821 | 822 | ### jest.config.json 823 | 824 | ::expander='configs/jest.config.json':: 825 | 826 | ### jest.stubs.js 827 | 828 | ::expander='configs/jest.stubs.js':: 829 | 830 | [⇧ back to top](#table-of-contents) 831 | 832 | ## Style Guides 833 | 834 | ### [react-styleguidist](https://github.com/styleguidist/react-styleguidist) 835 | 836 | [⟩⟩⟩ styleguide.config.js](/playground/styleguide.config.js) 837 | 838 | [⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/) 839 | 840 | [⇧ back to top](#table-of-contents) 841 | 842 | --- 843 | 844 | # FAQ 845 | 846 | 847 | ## Ambient Modules 848 | 849 | ### Imports in ambient modules 850 | 851 | For type augmentation imports should stay outside of module declaration. 852 | 853 | ```ts 854 | import { Operator } from 'rxjs/Operator'; 855 | import { Observable } from 'rxjs/Observable'; 856 | 857 | declare module 'rxjs/Subject' { 858 | interface Subject { 859 | lift(operator: Operator): Observable; 860 | } 861 | } 862 | ``` 863 | 864 | When creating 3rd party type-definitions all the imports should be kept inside the module declaration, otherwise it will be treated as augmentation and show error 865 | 866 | ```ts 867 | declare module "react-custom-scrollbars" { 868 | import * as React from "react"; 869 | export interface positionValues { 870 | ... 871 | ``` 872 | 873 | [⇧ back to top](#table-of-contents) 874 | 875 | ## Type-Definitions 876 | 877 | ### Missing type-definitions error 878 | 879 | if you cannot find types for a third-party module you can provide your own types or disable type-checking for this module using [Shorthand Ambient Modules](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Modules.md#shorthand-ambient-modules) 880 | 881 | ::codeblock='playground/typings/modules.d.ts':: 882 | 883 | ### Using custom `d.ts` files for npm modules 884 | 885 | If you want to use an alternative (customized) type-definitions for some npm module (that usually comes with it's own type-definitions), you can do it by adding an override in `paths` compiler option. 886 | 887 | ```ts 888 | { 889 | "compilerOptions": { 890 | "baseUrl": ".", 891 | "paths": { 892 | "redux": ["typings/redux"], // use an alternative type-definitions instead of the included one 893 | ... 894 | }, 895 | ..., 896 | } 897 | } 898 | ``` 899 | 900 | [⇧ back to top](#table-of-contents) 901 | 902 | ## Type Augmentation 903 | 904 | Strategies to fix issues coming from external type-definitions files (*.d.ts) 905 | 906 | ### Augmenting library internal declarations - using relative import 907 | 908 | ```ts 909 | // added missing autoFocus Prop on Input component in "antd@2.10.0" npm package 910 | declare module '../node_modules/antd/lib/input/Input' { 911 | export interface InputProps { 912 | autoFocus?: boolean; 913 | } 914 | } 915 | ``` 916 | 917 | ### Augmenting library public declarations - using node_modules import 918 | 919 | ```ts 920 | // fixed broken public type-definitions in "rxjs@5.4.1" npm package 921 | import { Operator } from 'rxjs/Operator'; 922 | import { Observable } from 'rxjs/Observable'; 923 | 924 | declare module 'rxjs/Subject' { 925 | interface Subject { 926 | lift(operator: Operator): Observable; 927 | } 928 | } 929 | ``` 930 | 931 | > More advanced scenarios for working with vendor type-definitions can be found here [Official TypeScript Docs](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Modules.md#working-with-other-javascript-libraries) 932 | 933 | [⇧ back to top](#table-of-contents) 934 | 935 | ## Misc 936 | 937 | ### - should I still use React.PropTypes in TS? 938 | 939 | No. With TypeScript, using PropTypes is an unnecessary overhead. When declaring Props and State interfaces, you will get complete intellisense and design-time safety with static type checking. This way you'll be safe from runtime errors and you will save a lot of time on debugging. Additional benefit is an elegant and standardized method of documenting your component public API in the source code. 940 | 941 | [⇧ back to top](#table-of-contents) 942 | 943 | ### - when to use `interface` declarations and when `type` aliases? 944 | 945 | From practical side, using `interface` declaration will create an identity (interface name) in compiler errors, on the contrary `type` aliases doesn't create an identity and will be unwinded to show all the properties and nested types it consists of. 946 | Although I prefer to use `type` most of the time there are some places this can become too noisy when reading compiler errors and that's why I like to leverage this distinction to hide some of not so important type details in errors using interfaces identity. 947 | Related `ts-lint` rule: 948 | 949 | [⇧ back to top](#table-of-contents) 950 | 951 | ### - what's better default or named exports? 952 | 953 | A common flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit. 954 | With this solution you'll achieve better encapsulation and be able to safely refactor internal naming and folders structure without breaking your consumer code: 955 | 956 | ```ts 957 | // 1. create your component files (`select.tsx`) using default export in some folder: 958 | 959 | // components/select.tsx 960 | const Select: React.FC = (props) => { 961 | ... 962 | export default Select; 963 | 964 | // 2. in this folder create an `index.ts` file that will re-export components with named exports: 965 | 966 | // components/index.ts 967 | export { default as Select } from './select'; 968 | ... 969 | 970 | // 3. now you can import your components in both ways, with named export (better encapsulation) or using default export (internal access): 971 | 972 | // containers/container.tsx 973 | import { Select } from '@src/components'; 974 | or 975 | import Select from '@src/components/select'; 976 | ... 977 | ``` 978 | 979 | [⇧ back to top](#table-of-contents) 980 | 981 | ### - how to best initialize class instance or static properties? 982 | 983 | Prefered modern syntax is to use class Property Initializers 984 | 985 | ```tsx 986 | class ClassCounterWithInitialCount extends React.Component { 987 | // default props using Property Initializers 988 | static defaultProps: DefaultProps = { 989 | className: 'default-class', 990 | initialCount: 0, 991 | }; 992 | 993 | // initial state using Property Initializers 994 | state: State = { 995 | count: this.props.initialCount, 996 | }; 997 | ... 998 | } 999 | ``` 1000 | 1001 | [⇧ back to top](#table-of-contents) 1002 | 1003 | ### - how to best declare component handler functions? 1004 | 1005 | Prefered modern syntax is to use Class Fields with arrow functions 1006 | 1007 | ```tsx 1008 | class ClassCounter extends React.Component { 1009 | // handlers using Class Fields with arrow functions 1010 | handleIncrement = () => { 1011 | this.setState({ count: this.state.count + 1 }); 1012 | }; 1013 | ... 1014 | } 1015 | ``` 1016 | 1017 | [⇧ back to top](#table-of-contents) 1018 | 1019 | --- 1020 | 1021 | # Tutorials & Articles 1022 | 1023 | > Curated list of relevant in-depth tutorials 1024 | 1025 | Higher-Order Components: 1026 | 1027 | - 1028 | 1029 | [⇧ back to top](#table-of-contents) 1030 | 1031 | --- 1032 | 1033 | # Contributors 1034 | 1035 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 1036 | 1037 | 1038 | 1039 | | [
Piotrek Witek](https://github.com/piotrwitek)
[💻](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=piotrwitek "Code") [📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=piotrwitek "Documentation") [🤔](#ideas-piotrwitek "Ideas, Planning, & Feedback") [👀](#review-piotrwitek "Reviewed Pull Requests") [💬](#question-piotrwitek "Answering Questions") | [
Kazz Yokomizo](https://github.com/kazup01)
[💵](#financial-kazup01 "Financial") [🔍](#fundingFinding-kazup01 "Funding Finding") | [
Jake Boone](https://github.com/jakeboone02)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=jakeboone02 "Documentation") | [
Amit Dahan](https://github.com/amitdahan)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=amitdahan "Documentation") | [
gulderov](https://github.com/gulderov)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=gulderov "Documentation") | [
Erik Pearson](https://github.com/emp823)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=emp823 "Documentation") | [
Bryan Mason](https://github.com/flymason)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=flymason "Documentation") | 1040 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 1041 | | [
Jakub Chodorowicz](http://www.jakub.chodorowicz.pl/)
[💻](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=chodorowicz "Code") | [
Oleg Maslov](https://github.com/mleg)
[🐛](https://github.com/piotrwitek/react-redux-typescript-guide/issues?q=author%3Amleg "Bug reports") | [
Aaron Westbrook](https://github.com/awestbro)
[🐛](https://github.com/piotrwitek/react-redux-typescript-guide/issues?q=author%3Aawestbro "Bug reports") | [
Peter Blazejewicz](http://www.linkedin.com/in/peterblazejewicz)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=peterblazejewicz "Documentation") | [
Solomon White](https://github.com/rubysolo)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=rubysolo "Documentation") | [
Levi Rocha](https://github.com/pino)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=pino "Documentation") | [
Sudachi-kun](http://cloudnative.co.jp)
[💵](#financial-loadbalance-sudachi-kun "Financial") | 1042 | | [
Sosuke Suzuki](http://sosukesuzuki.github.io)
[💻](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=sosukesuzuki "Code") | [
Tom Rathbone](https://github.com/chillitom)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=chillitom "Documentation") | [
Arshad Kazmi](https://arshadkazmi42.github.io/)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=arshadkazmi42 "Documentation") | [
JeongUkJae](https://jeongukjae.github.io)
[📖](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=JeongUkJae "Documentation") | 1043 | 1044 | 1045 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 1046 | 1047 | --- 1048 | 1049 | MIT License 1050 | 1051 | Copyright (c) 2017 Piotr Witek () 1052 | -------------------------------------------------------------------------------- /configs/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "transform": { 4 | ".(ts|tsx)": "ts-jest" 5 | }, 6 | "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$", 7 | "moduleFileExtensions": ["ts", "tsx", "js"], 8 | "moduleNameMapper": { 9 | "^Components/(.*)": "./src/components/$1" 10 | }, 11 | "globals": { 12 | "window": {}, 13 | "ts-jest": { 14 | "tsConfig": "./tsconfig.json" 15 | } 16 | }, 17 | "setupFiles": ["./jest.stubs.js"], 18 | "testURL": "http://localhost/" 19 | } 20 | -------------------------------------------------------------------------------- /configs/jest.stubs.js: -------------------------------------------------------------------------------- 1 | // Global/Window object Stubs for Jest 2 | window.matchMedia = window.matchMedia || function () { 3 | return { 4 | matches: false, 5 | addListener: function () { }, 6 | removeListener: function () { }, 7 | }; 8 | }; 9 | 10 | window.requestAnimationFrame = function (callback) { 11 | setTimeout(callback); 12 | }; 13 | 14 | window.localStorage = { 15 | getItem: function () { }, 16 | setItem: function () { }, 17 | }; 18 | 19 | Object.values = () => []; 20 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React & Redux in TypeScript - Component Typing Patterns 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /generate-readme.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const ROOT_PATH = `${__dirname}/`; 4 | const inputFiles = [ROOT_PATH + 'README_SOURCE.md']; 5 | const outputFile = ROOT_PATH + 'README.md'; 6 | 7 | const result = inputFiles 8 | .map(filePath => fs.readFileSync(filePath, 'utf8')) 9 | .map(injectCodeBlocks) 10 | .map(injectExpanders) 11 | .toString(); 12 | 13 | fs.writeFileSync(outputFile, result, 'utf8'); 14 | 15 | function injectCodeBlocks(text) { 16 | const regex = /::codeblock='(.+?)'::/g; 17 | return text.replace(regex, createMatchReplacer(withSourceWrapper)); 18 | } 19 | 20 | function injectExpanders(text) { 21 | const regex = /::expander='(.+?)'::/g; 22 | return text.replace(regex, createMatchReplacer(withDetailsWrapper)); 23 | } 24 | 25 | function createMatchReplacer(wrapper) { 26 | return (match, filePath) => { 27 | console.log(ROOT_PATH + filePath); 28 | const text = fs.readFileSync(ROOT_PATH + filePath, 'utf8'); 29 | return wrapper(text); 30 | }; 31 | } 32 | 33 | function withSourceWrapper(text) { 34 | return ` 35 | ${'```tsx'} 36 | ${text} 37 | ${'```'} 38 | `.trim(); 39 | } 40 | 41 | function withDetailsWrapper(text) { 42 | return ` 43 |
Click to expand

44 | 45 | ${'```tsx'} 46 | ${text} 47 | ${'```'} 48 |

49 | `.trim(); 50 | } 51 | -------------------------------------------------------------------------------- /generate-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | node generate-readme.js 3 | -------------------------------------------------------------------------------- /generate-styleguide.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd playground && npm run styleguide:build 3 | -------------------------------------------------------------------------------- /is-git-status-clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if output=$(git status --porcelain) && [ -z "$output" ]; then echo "Success!"; else (echo ">>> Please check CONTRIBUTING.md to learn how to properly amend README.md <<<\n" && false); fi 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "all-contributors-cli": "6.9.3", 4 | "doctoc": "1.4.0", 5 | "husky": "3.0.9" 6 | }, 7 | "scripts": { 8 | "ci-check": "npm run doctoc && npm run readme:generate", 9 | "doctoc": "doctoc --maxlevel=3 README_SOURCE.md", 10 | "readme:generate": "node generate-readme.js", 11 | "contributors:check": "all-contributors check", 12 | "contributors:add": "all-contributors add", 13 | "contributors:generate": "all-contributors generate", 14 | "is-git-status-clean": "sh ./is-git-status-clean.sh" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-push": "npm run ci-check && npm run is-git-status-clean " 19 | } 20 | }, 21 | "dependencies": { 22 | "react": "18.1.0", 23 | "react-dom": "18.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['react-app', 'react-app/jest', 'prettier'], 6 | rules: { 'import/no-anonymous-default-export': 0 }, 7 | }; 8 | -------------------------------------------------------------------------------- /playground/.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 | -------------------------------------------------------------------------------- /playground/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /playground/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /playground/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | import requireContext from 'require-context.macro'; 3 | 4 | // We load every file in src directory ending with .stories.tsx 5 | 6 | const req = requireContext('../src', true, /.stories.tsx$/); 7 | function loadStories() { 8 | req.keys().forEach(filename => req(filename)); 9 | } 10 | configure(loadStories, module); 11 | -------------------------------------------------------------------------------- /playground/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config, mode }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | loader: require.resolve('babel-loader'), 5 | options: { 6 | presets: [['react-app', { flow: false, typescript: true }]], 7 | }, 8 | }); 9 | config.resolve.extensions.push('.ts', '.tsx'); 10 | return config; 11 | }; 12 | -------------------------------------------------------------------------------- /playground/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | This folder is a playground project for testing the code examples that can be found in the guide. They are all tested with the most recent supported version of TypeScript and third-party type-definitions (like `@types/react` or `@types/react-redux`) to ensure the examples are still working when new third-party type-definitions are released. 2 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React, Redux, Typescript Guide 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "description": "Playground Project for https://github.com/piotrwitek/react-redux-typescript-guide", 4 | "version": "1.0.0", 5 | "private": true, 6 | "author": "Piotr Witek (http://piotrwitek.github.io/)", 7 | "repository": "https://github.com/piotrwitek/react-redux-typescript-guide.git", 8 | "license": "MIT", 9 | "main": "src/index.tsx", 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject", 15 | "reinstall": "rm -rf node_modules && npm install", 16 | "ci-check": "npm run lint && npm run tsc && npm run test", 17 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 18 | "tsc": "tsc -p ./ --noEmit", 19 | "tsc:watch": "tsc -p ./ --noEmit -w", 20 | "storybook": "start-storybook -p 9009 -s public", 21 | "build-storybook": "build-storybook -s public" 22 | }, 23 | "dependencies": { 24 | "@lagunovsky/redux-react-router": "2.2.0", 25 | "@testing-library/jest-dom": "5.16.4", 26 | "@testing-library/react": "13.1.1", 27 | "@testing-library/user-event": "13.5.0", 28 | "@types/jest": "27.5.0", 29 | "@types/node": "16.11.33", 30 | "@types/react": "18.0.8", 31 | "@types/react-dom": "18.0.3", 32 | "@types/react-redux": "7.1.24", 33 | "@types/react-router-dom": "5.3.3", 34 | "axios": "0.26.1", 35 | "cuid": "2.1.8", 36 | "react": "18.1.0", 37 | "react-dom": "18.1.0", 38 | "react-redux": "7.2.8", 39 | "react-router-dom": "6.3.0", 40 | "react-scripts": "5.0.1", 41 | "redux": "4.1.2", 42 | "redux-observable": "1.2.0", 43 | "redux-thunk": "2.4.1", 44 | "reselect": "4.0.0", 45 | "rxjs": "6.5.3", 46 | "tslib": "2.4.0", 47 | "typesafe-actions": "5.1.0", 48 | "utility-types": "3.10.0" 49 | }, 50 | "devDependencies": { 51 | "@storybook/addon-actions": "5.2.5", 52 | "@storybook/addon-links": "5.2.5", 53 | "@storybook/addon-storyshots": "5.2.5", 54 | "@storybook/addons": "5.2.5", 55 | "@storybook/react": "5.2.5", 56 | "@typescript-eslint/eslint-plugin": "5.22.0", 57 | "@typescript-eslint/parser": "5.22.0", 58 | "eslint": "8.14.0", 59 | "eslint-config-prettier": "8.5.0", 60 | "require-context.macro": "1.2.2", 61 | "typescript": "4.6.4", 62 | "web-vitals": "2.1.4" 63 | }, 64 | "browserslist": { 65 | "production": [ 66 | ">0.2%", 67 | "not dead", 68 | "not op_mini all" 69 | ], 70 | "development": [ 71 | "last 1 chrome version", 72 | "last 1 firefox version", 73 | "last 1 safari version" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appledesire/react-redux-guide/47ca335db14a8bbc30f99d2474446b38f945cb24/playground/public/favicon.ico -------------------------------------------------------------------------------- /playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React, Redux, TypeScript Guide - Playground 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /playground/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "RRTS Guide - Playground", 3 | "name": "React, Redux, TypeScript Guide - Playground", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /playground/src-old/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-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/src-old/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /playground/src-old/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

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

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /playground/src-old/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /playground/src-old/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /playground/src-old/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src-old/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/src-old/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /playground/src-old/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 | -------------------------------------------------------------------------------- /playground/src/api/agent.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const URL = 'http://localhost:3000/api/'; 4 | 5 | const getToken = () => 'some-token'; 6 | 7 | const formatToken = (token: string) => { 8 | return `Token ${token}`; 9 | }; 10 | 11 | // Public 12 | 13 | export const setToken = (token: string) => { 14 | agentInstance.defaults.headers.common.Authorization = formatToken(token); 15 | }; 16 | 17 | const agentInstance = axios.create({ 18 | baseURL: URL, 19 | timeout: 4000, 20 | headers: { 21 | Authorization: formatToken(getToken()), 22 | }, 23 | }); 24 | export default agentInstance; 25 | -------------------------------------------------------------------------------- /playground/src/api/fixtures/todos.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 0, 3 | "text": "Example todo", 4 | "completed": false 5 | } 6 | -------------------------------------------------------------------------------- /playground/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export { default as agent } from './agent'; 2 | export * from './agent'; 3 | export * from './models'; 4 | export * from './todos'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /playground/src/api/models.ts: -------------------------------------------------------------------------------- 1 | export interface ITodoModel { 2 | id: string; 3 | text: string; 4 | completed: false; 5 | } 6 | -------------------------------------------------------------------------------- /playground/src/api/todos.ts: -------------------------------------------------------------------------------- 1 | import { ITodoModel } from './models'; 2 | import { resolveWithDelay } from './utils'; 3 | 4 | const pageSize = 10; 5 | 6 | // Mock API 7 | // tslint:disable-next-line:no-var-requires 8 | const todosResponse: ITodoModel[] = require('../fixtures/todos.json'); 9 | export const Todos = { 10 | getAll: (pageNumber: number = 0) => resolveWithDelay(todosResponse 11 | .slice(pageNumber * pageSize, (pageNumber * pageSize) + pageSize - 1)), 12 | get: (id: string) => resolveWithDelay(todosResponse 13 | .find(t => t.id === id)), 14 | create: (payload: ITodoModel) => resolveWithDelay(todosResponse 15 | .push(payload)), 16 | update: (payload: ITodoModel) => resolveWithDelay(todosResponse 17 | .map(t => t.id === payload.id ? payload : t)), 18 | delete: (id: string) => resolveWithDelay(todosResponse 19 | .filter(t => t.id !== id)), 20 | }; 21 | 22 | // Real API 23 | // const URL = '/todos'; 24 | // export const Todos = { 25 | // getAll: (pageNumber?: number) => 26 | // requests.get(`${URL}?${rangeQueryString(pageSize, pageNumber)}`), 27 | // get: (id: string) => 28 | // requests.get(`${URL}/${id}`), 29 | // create: (payload: ITodoModel) => 30 | // requests.post(`${URL}`, { payload }), 31 | // update: (payload: ITodoModel) => 32 | // requests.put(`${URL}/${payload.id}`, { todo: removeKeys(payload, ['id']) }), 33 | // delete: (id: string) => 34 | // requests.delete(`${URL}/${id}`), 35 | // }; 36 | -------------------------------------------------------------------------------- /playground/src/api/utils.ts: -------------------------------------------------------------------------------- 1 | export const resolveWithDelay = (value: T, time: number = 1000) => new Promise( 2 | (resolve) => setTimeout(() => resolve(value), time) 3 | ); 4 | 5 | export const rangeQueryString = (count: number, pageNumber?: number) => 6 | `limit=${count}&offset=${pageNumber ? pageNumber * count : 0}`; 7 | 8 | export const removeKeys = (payload: T, keys: Array) => { 9 | keys.forEach((key) => { 10 | delete payload[key]; 11 | }); 12 | 13 | return payload; 14 | }; 15 | -------------------------------------------------------------------------------- /playground/src/app.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from './app'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /playground/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Outlet, Route, Routes } from 'react-router-dom'; 4 | import { ReduxRouter } from '@lagunovsky/redux-react-router' 5 | 6 | import { Layout } from './layout/layout'; 7 | import { LayoutFooter } from './layout/layout-footer'; 8 | import { LayoutHeader } from './layout/layout-header'; 9 | import { Home } from './routes/home'; 10 | import { NotFound } from './routes/not-found'; 11 | import { history, store } from './store'; 12 | 13 | export function App() { 14 | return ( 15 | 16 | 17 | 18 | } 23 | renderFooter={() => } 24 | renderContent={() => } 25 | /> 26 | } 27 | > 28 | } /> 29 | } /> 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/class-counter-with-default-props.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots ClassCounterWithDefaultProps with defaut initial count 1`] = ` 4 |
5 | 6 | ClassCounterWithDefaultProps 7 | : 8 | 0 9 | 10 | 16 |
17 | `; 18 | 19 | exports[`Storyshots ClassCounterWithDefaultProps with initial count set 1`] = ` 20 |
21 | 22 | ClassCounterWithDefaultProps 23 | : 24 | 5 25 | 26 | 32 |
33 | `; 34 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/class-counter.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots ClassCounter default 1`] = ` 4 |
5 | 6 | ClassCounter 7 | : 8 | 0 9 | 10 | 16 |
17 | `; 18 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/fc-counter-with-default-props.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots FCCounterWithDefaultProps default 1`] = ` 4 |
5 | 6 | FCCounterWithDefaultProps 7 | : 8 | 5 9 | 10 | 16 |
17 | `; 18 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/fc-counter.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots FCCounter default 1`] = ` 4 |
5 | 6 | FCCounter 7 | : 8 | 0 9 | 10 | 16 |
17 | `; 18 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/fc-spread-attributes.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots FCSpreadAttributes default 1`] = ` 4 |
12 | I'll spread every property you give me! 13 |
14 | `; 15 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/generic-list.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots GenericList default 1`] = ` 4 |
5 |
6 | Rosamonte Especial 7 |
8 |
9 | Aguantadora Despalada 10 |
11 |
12 | Taragui Vitality 13 |
14 |
15 | `; 16 | -------------------------------------------------------------------------------- /playground/src/components/__snapshots__/mouse-provider.stories.storyshot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots MouseProvider default 1`] = ` 4 |
12 |

13 | The mouse position is 14 | 0 15 | , 16 | 0 17 |

18 |
19 | `; 20 | -------------------------------------------------------------------------------- /playground/src/components/class-counter-with-default-props.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./class-counter-with-default-props.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./class-counter-with-default-props.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--with-default-props) 12 | -------------------------------------------------------------------------------- /playground/src/components/class-counter-with-default-props.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { ClassCounterWithDefaultProps } from '../components'; 5 | 6 | storiesOf('ClassCounterWithDefaultProps', module) 7 | .add('with defaut initial count', () => ( 8 | 9 | )) 10 | .add('with initial count set', () => ( 11 | 15 | )); 16 | -------------------------------------------------------------------------------- /playground/src/components/class-counter-with-default-props.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = { 4 | label: string; 5 | initialCount: number; 6 | }; 7 | 8 | type State = { 9 | count: number; 10 | }; 11 | 12 | export class ClassCounterWithDefaultProps extends React.Component< 13 | Props, 14 | State 15 | > { 16 | static defaultProps = { 17 | initialCount: 0, 18 | }; 19 | 20 | readonly state: State = { 21 | count: this.props.initialCount, 22 | }; 23 | 24 | handleIncrement = () => { 25 | this.setState({ count: this.state.count + 1 }); 26 | }; 27 | 28 | render() { 29 | const { handleIncrement } = this; 30 | const { label } = this.props; 31 | const { count } = this.state; 32 | 33 | return ( 34 |
35 | 36 | {label}: {count} 37 | 38 | 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /playground/src/components/class-counter-with-default-props.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ClassCounterWithDefaultProps } from '.'; 4 | 5 | export default () => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /playground/src/components/class-counter.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./class-counter.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./class-counter.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--class-counter) 12 | -------------------------------------------------------------------------------- /playground/src/components/class-counter.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { ClassCounter } from '../components'; 5 | 6 | storiesOf('ClassCounter', module).add('default', () => ( 7 | 8 | )); 9 | -------------------------------------------------------------------------------- /playground/src/components/class-counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = { 4 | label: string; 5 | }; 6 | 7 | type State = { 8 | count: number; 9 | }; 10 | 11 | export class ClassCounter extends React.Component { 12 | readonly state: State = { 13 | count: 0, 14 | }; 15 | 16 | handleIncrement = () => { 17 | this.setState({ count: this.state.count + 1 }); 18 | }; 19 | 20 | render() { 21 | const { handleIncrement } = this; 22 | const { label } = this.props; 23 | const { count } = this.state; 24 | 25 | return ( 26 |
27 | 28 | {label}: {count} 29 | 30 | 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /playground/src/components/class-counter.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ClassCounter } from '.'; 4 | 5 | export default () => ; 6 | -------------------------------------------------------------------------------- /playground/src/components/error-message.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const ErrorMessage: React.FC<{ onReset: () => void }> = ({ 4 | onReset, 5 | }) => { 6 | return ( 7 |
8 |

{`Sorry there was an unexpected error`}

9 | {`To continue: `} 10 | { 13 | onReset(); 14 | }} 15 | > 16 | {`go to home page`} 17 | 18 |
19 | ); 20 | }; 21 | 22 | export function test(props: any) { 23 | const Container = props.componentClass; 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter-with-default-props.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./fc-counter-with-default-props.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./fc-counter-with-default-props.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--fc-counter-with-default-props) 12 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter-with-default-props.stories.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | import { FCCounterWithDefaultProps } from '.'; 7 | 8 | storiesOf('FCCounterWithDefaultProps', module).add('default', () => ( 9 | 13 | )); 14 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter-with-default-props.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = { 4 | label: string; 5 | count: number; 6 | onIncrement: () => void; 7 | }; 8 | 9 | // React.FC is unaplicable here due not working properly with default props 10 | // https://github.com/facebook/create-react-app/pull/8177 11 | export const FCCounterWithDefaultProps = (props: Props): JSX.Element => { 12 | const { label, count, onIncrement } = props; 13 | 14 | const handleIncrement = () => { 15 | onIncrement(); 16 | }; 17 | 18 | return ( 19 |
20 | 21 | {label}: {count} 22 | 23 | 26 |
27 | ); 28 | }; 29 | 30 | FCCounterWithDefaultProps.defaultProps = { count: 5 }; 31 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter-with-default-props.usage.tsx: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import * as React from 'react'; 3 | 4 | import { FCCounterWithDefaultProps } from '.'; 5 | 6 | export default () => ( 7 | 11 | ); 12 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./fc-counter.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./fc-counter.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--fc-counter) 12 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { FCCounter } from '../components'; 6 | 7 | storiesOf('FCCounter', module).add('default', () => ( 8 | 13 | )); 14 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = { 4 | label: string; 5 | count: number; 6 | onIncrement: () => void; 7 | }; 8 | 9 | export const FCCounter: React.FC = props => { 10 | const { label, count, onIncrement } = props; 11 | 12 | const handleIncrement = () => { 13 | onIncrement(); 14 | }; 15 | 16 | return ( 17 |
18 | 19 | {label}: {count} 20 | 21 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /playground/src/components/fc-counter.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FCCounter } from '.'; 4 | 5 | export default class extends React.Component<{}, { count: number }> { 6 | state = { count: 0 }; 7 | 8 | render() { 9 | return ( 10 | { 14 | this.setState({ count: this.state.count + 1 }); 15 | }} 16 | /> 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/src/components/fc-spread-attributes.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./fc-spread-attributes.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./fc-spread-attributes.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--spread-attributes-link) 12 | -------------------------------------------------------------------------------- /playground/src/components/fc-spread-attributes.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { FCSpreadAttributes } from '../components'; 5 | 6 | storiesOf('FCSpreadAttributes', module).add('default', () => ( 7 | 11 | {`I'll spread every property you give me!`} 12 | 13 | )); 14 | -------------------------------------------------------------------------------- /playground/src/components/fc-spread-attributes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = React.PropsWithChildren<{ 4 | className?: string; 5 | style?: React.CSSProperties; 6 | }>; 7 | 8 | export const FCSpreadAttributes: React.FC = (props) => { 9 | const { children, ...restProps } = props; 10 | 11 | return
{children}
; 12 | }; 13 | -------------------------------------------------------------------------------- /playground/src/components/fc-spread-attributes.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FCSpreadAttributes } from '.'; 4 | 5 | export default () => ( 6 | 10 | {`I'll spread every property you give me!`} 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /playground/src/components/generic-list.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./generic-list.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./generic-list.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--generic-list) 12 | -------------------------------------------------------------------------------- /playground/src/components/generic-list.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { IUser, User } from '../models'; 5 | import { GenericList } from '../components'; 6 | 7 | const users = [ 8 | new User('Rosamonte', 'Especial'), 9 | new User('Aguantadora', 'Despalada'), 10 | new User('Taragui', 'Vitality'), 11 | ]; 12 | 13 | export class UserList extends GenericList {} 14 | 15 | storiesOf('GenericList', module).add('default', () => ( 16 |
{item.fullName}
} 19 | /> 20 | )); 21 | -------------------------------------------------------------------------------- /playground/src/components/generic-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface GenericListProps { 4 | items: T[]; 5 | itemRenderer: (item: T) => JSX.Element; 6 | } 7 | 8 | export class GenericList extends React.Component, {}> { 9 | render() { 10 | const { items, itemRenderer } = this.props; 11 | 12 | return ( 13 |
14 | {items.map(itemRenderer)} 15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /playground/src/components/generic-list.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IUser, User } from '../models'; 4 | import { GenericList } from '../components'; 5 | 6 | const users = [ 7 | new User('Rosamonte', 'Especial'), 8 | new User('Aguantadora', 'Despalada'), 9 | new User('Taragui', 'Vitality'), 10 | ]; 11 | 12 | export class UserList extends GenericList {} 13 | 14 | export default () => ( 15 |
{item.fullName}
} 18 | /> 19 | ); 20 | -------------------------------------------------------------------------------- /playground/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error-message'; 2 | export * from './generic-list'; 3 | export * from './fc-counter'; 4 | export * from './fc-counter-with-default-props'; 5 | export * from './fc-spread-attributes'; 6 | export * from './class-counter'; 7 | export * from './class-counter-with-default-props'; 8 | export * from './name-provider'; 9 | export * from './mouse-provider'; 10 | -------------------------------------------------------------------------------- /playground/src/components/mouse-provider.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./mouse-provider.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./mouse-provider.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--mouse-provider) 12 | -------------------------------------------------------------------------------- /playground/src/components/mouse-provider.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import { MouseProvider } from '../components'; 5 | 6 | storiesOf('MouseProvider', module).add('default', () => ( 7 | ( 9 |

10 | The mouse position is {mouse.x}, {mouse.y} 11 |

12 | )} 13 | /> 14 | )); 15 | -------------------------------------------------------------------------------- /playground/src/components/mouse-provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface MouseProviderProps { 4 | render: (state: MouseProviderState) => React.ReactNode; 5 | } 6 | 7 | interface MouseProviderState { 8 | readonly x: number; 9 | readonly y: number; 10 | } 11 | 12 | export class MouseProvider extends React.Component { 13 | readonly state: MouseProviderState = { x: 0, y: 0 }; 14 | 15 | handleMouseMove = (event: React.MouseEvent) => { 16 | this.setState({ 17 | x: event.clientX, 18 | y: event.clientY, 19 | }); 20 | }; 21 | 22 | render() { 23 | return ( 24 |
25 | {/* 26 | Instead of providing a static representation of what renders, 27 | use the `render` prop to dynamically determine what to render. 28 | */} 29 | {this.props.render(this.state)} 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /playground/src/components/mouse-provider.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { MouseProvider } from './mouse-provider'; 4 | 5 | export default () => ( 6 | ( 8 |

The mouse position is {mouse.x}, {mouse.y}

9 | )} 10 | /> 11 | ); 12 | -------------------------------------------------------------------------------- /playground/src/components/name-provider.md: -------------------------------------------------------------------------------- 1 | Usage: 2 | ```jsx { "filePath": "./name-provider.usage.tsx" } 3 | ``` 4 | 5 | Usage Demo: 6 | ```jsx 7 | const Demo = require('./name-provider.usage').default; 8 | 9 | ``` 10 | 11 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#--name-provider) 12 | -------------------------------------------------------------------------------- /playground/src/components/name-provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface NameProviderProps { 4 | children: (state: NameProviderState) => React.ReactNode; 5 | } 6 | 7 | interface NameProviderState { 8 | readonly name: string; 9 | } 10 | 11 | export class NameProvider extends React.Component { 12 | readonly state: NameProviderState = { name: 'Piotr' }; 13 | 14 | render() { 15 | return this.props.children(this.state); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /playground/src/components/name-provider.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NameProvider } from './name-provider'; 4 | 5 | export default () => ( 6 | 7 | {({ name }) => ( 8 |
{name}
9 | )} 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected-bind-action-creators.tsx: -------------------------------------------------------------------------------- 1 | import Types from 'MyTypes'; 2 | import { bindActionCreators, Dispatch } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import * as React from 'react'; 5 | 6 | import { countersActions } from '../features/counters'; 7 | 8 | // Thunk Action 9 | const incrementWithDelay = () => async (dispatch: Dispatch): Promise => { 10 | setTimeout(() => dispatch(countersActions.increment()), 1000); 11 | }; 12 | 13 | const mapStateToProps = (state: Types.RootState) => ({ 14 | count: state.counters.reduxCounter, 15 | }); 16 | 17 | const mapDispatchToProps = (dispatch: Dispatch) => 18 | bindActionCreators( 19 | { 20 | onIncrement: incrementWithDelay, 21 | }, 22 | dispatch 23 | ); 24 | 25 | type Props = ReturnType & 26 | ReturnType & { 27 | label: string; 28 | }; 29 | 30 | export const FCCounter: React.FC = props => { 31 | const { label, count, onIncrement } = props; 32 | 33 | const handleIncrement = () => { 34 | // Thunk action is correctly typed as promise 35 | onIncrement().then(() => { 36 | // ... 37 | }); 38 | }; 39 | 40 | return ( 41 |
42 | 43 | {label}: {count} 44 | 45 | 48 |
49 | ); 50 | }; 51 | 52 | export const FCCounterConnectedBindActionCreators = connect( 53 | mapStateToProps, 54 | mapDispatchToProps 55 | )(FCCounter); 56 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected-bind-action-creators.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FCCounterConnectedBindActionCreators } from '.'; 4 | 5 | export default () => ( 6 | 9 | ); 10 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected-own-props.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore, combineReducers } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import { render, fireEvent, cleanup, screen } from '@testing-library/react'; 5 | 6 | import { FCCounterConnectedOwnProps as ConnectedCounter } from './fc-counter-connected-own-props'; 7 | 8 | const reducer = combineReducers({ 9 | counters: combineReducers({ 10 | reduxCounter: (state: number = 0, action: any) => { 11 | switch (action.type) { 12 | case 'counters/INCREMENT': 13 | return state + 1; // action: { type: "INCREMENT"; } 14 | 15 | default: 16 | return state; 17 | } 18 | }, 19 | }), 20 | }); 21 | 22 | afterEach(cleanup); 23 | 24 | test('can render with redux with defaults', () => { 25 | const label = 'Counter 1'; 26 | renderWithRedux(); 27 | 28 | fireEvent.click(screen.getByText('Increment')); 29 | expect(screen.getByText(RegExp(label)).textContent).toBe(label + ': 1'); 30 | }); 31 | 32 | test('can render with redux with custom initial state', () => { 33 | const label = 'Counter 1'; 34 | renderWithRedux(, { 35 | initialState: { counters: { reduxCounter: 3 } }, 36 | }); 37 | fireEvent.click(screen.getByText('Increment')); 38 | expect(screen.getByText(RegExp(label)).textContent).toBe(label + ': 4'); 39 | }); 40 | 41 | // TODO: move to external utils 42 | // Redux Provider utility 43 | function renderWithRedux( 44 | jsx: JSX.Element, 45 | options: { initialState?: object } = {} 46 | ) { 47 | const store = createStore(reducer, options.initialState); 48 | const view = render({jsx}); 49 | 50 | return { 51 | view, 52 | store, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected-own-props.tsx: -------------------------------------------------------------------------------- 1 | import Types from 'MyTypes'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { countersActions, countersSelectors } from '../features/counters'; 5 | import { FCCounter } from '../components'; 6 | 7 | type OwnProps = { 8 | initialCount?: number; 9 | }; 10 | 11 | const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) => ({ 12 | count: 13 | countersSelectors.getReduxCounter(state.counters) + 14 | (ownProps.initialCount || 0), 15 | }); 16 | 17 | const dispatchProps = { 18 | onIncrement: countersActions.increment, 19 | }; 20 | 21 | export const FCCounterConnectedOwnProps = connect( 22 | mapStateToProps, 23 | dispatchProps 24 | )(FCCounter); 25 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected-own-props.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FCCounterConnectedOwnProps } from '.'; 4 | 5 | export default () => ( 6 | 10 | ); 11 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected.tsx: -------------------------------------------------------------------------------- 1 | import Types from 'MyTypes'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { countersActions, countersSelectors } from '../features/counters'; 5 | import { FCCounter } from '../components'; 6 | 7 | const mapStateToProps = (state: Types.RootState) => ({ 8 | count: countersSelectors.getReduxCounter(state.counters), 9 | }); 10 | 11 | const dispatchProps = { 12 | onIncrement: countersActions.increment, 13 | }; 14 | 15 | export const FCCounterConnected = connect( 16 | mapStateToProps, 17 | dispatchProps 18 | )(FCCounter); 19 | -------------------------------------------------------------------------------- /playground/src/connected/fc-counter-connected.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FCCounterConnected } from '.'; 4 | 5 | export default () => ; 6 | -------------------------------------------------------------------------------- /playground/src/connected/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fc-counter-connected-bind-action-creators'; 2 | export * from './fc-counter-connected-own-props'; 3 | export * from './fc-counter-connected'; 4 | -------------------------------------------------------------------------------- /playground/src/context/theme-consumer-class.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ThemeContext from './theme-context'; 3 | 4 | type Props = {}; 5 | 6 | export class ToggleThemeButtonClass extends React.Component { 7 | static contextType = ThemeContext; 8 | declare context: React.ContextType; 9 | 10 | render() { 11 | const { theme, toggleTheme } = this.context; 12 | return ( 13 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /playground/src/context/theme-consumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ThemeContext from './theme-context'; 3 | 4 | type Props = {}; 5 | 6 | export default function ToggleThemeButton(props: Props) { 7 | return ( 8 | 9 | {({ theme, toggleTheme }) => 30 | ); 31 | }; 32 | 33 | export default () => ( 34 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /playground/src/hoc/with-state.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Diff } from 'utility-types'; 3 | 4 | // These props will be injected into the base component 5 | interface InjectedProps { 6 | count: number; 7 | onIncrement: () => void; 8 | } 9 | 10 | export const withState = ( 11 | BaseComponent: React.ComponentType 12 | ) => { 13 | type HocProps = Diff & { 14 | // here you can extend hoc with new props 15 | initialCount?: number; 16 | }; 17 | type HocState = { 18 | readonly count: number; 19 | }; 20 | 21 | return class Hoc extends React.Component { 22 | // Enhance component name for debugging and React-Dev-Tools 23 | static displayName = `withState(${BaseComponent.name})`; 24 | // reference to original wrapped component 25 | static readonly WrappedComponent = BaseComponent; 26 | 27 | readonly state: HocState = { 28 | count: Number(this.props.initialCount) || 0, 29 | }; 30 | 31 | handleIncrement = () => { 32 | this.setState({ count: this.state.count + 1 }); 33 | }; 34 | 35 | render() { 36 | const { ...restProps } = this.props; 37 | const { count } = this.state; 38 | 39 | return ( 40 | 45 | ); 46 | } 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /playground/src/hoc/with-state.usage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { withState } from '../hoc'; 4 | import { FCCounter } from '../components'; 5 | 6 | const FCCounterWithState = withState(FCCounter); 7 | 8 | export default () => ; 9 | -------------------------------------------------------------------------------- /playground/src/hooks/react-redux-hooks.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { FCCounter } from '../components'; 3 | import { increment } from '../features/counters/actions'; 4 | import { useSelector, useDispatch } from '../store/hooks'; 5 | 6 | const FCCounterConnectedHooksUsage: React.FC = () => { 7 | const counter = useSelector(state => state.counters.reduxCounter); 8 | const dispatch = useDispatch(); 9 | return dispatch(increment())}/>; 10 | }; 11 | 12 | export default FCCounterConnectedHooksUsage; 13 | -------------------------------------------------------------------------------- /playground/src/hooks/use-reducer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface State { 4 | count: number; 5 | } 6 | 7 | type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' }; 8 | 9 | function reducer(state: State, action: Action): State { 10 | switch (action.type) { 11 | case 'increment': 12 | return { count: state.count + 1 }; 13 | case 'decrement': 14 | return { count: state.count - 1 }; 15 | case 'reset': 16 | return { count: 0 }; 17 | default: 18 | throw new Error(); 19 | } 20 | } 21 | 22 | interface CounterProps { 23 | initialCount: number; 24 | } 25 | 26 | function Counter({ initialCount }: CounterProps) { 27 | const [state, dispatch] = React.useReducer(reducer, { 28 | count: initialCount, 29 | }); 30 | 31 | return ( 32 | <> 33 | Count: {state.count} 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default Counter; 42 | -------------------------------------------------------------------------------- /playground/src/hooks/use-state.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = { initialCount: number }; 4 | 5 | export default function Counter({initialCount}: Props) { 6 | const [count, setCount] = React.useState(initialCount); 7 | return ( 8 | <> 9 | Count: {count} 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /playground/src/hooks/use-theme-context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ThemeContext from '../context/theme-context'; 3 | 4 | type Props = {}; 5 | 6 | export default function ThemeToggleButton(props: Props) { 7 | const { theme, toggleTheme } = React.useContext(ThemeContext); 8 | return ( 9 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /playground/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /playground/src/index.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-import-side-effect 2 | import 'tslib'; 3 | // tslint:disable-next-line:no-import-side-effect 4 | import './index.css'; 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | import * as serviceWorker from './serviceWorker'; 10 | import { App } from './app'; 11 | 12 | ReactDOM.render(, document.getElementById('root')); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /playground/src/layout/layout-footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function LayoutFooter() { 4 | return ( 5 |
6 | React & Redux in TypeScript - Complete Guide 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /playground/src/layout/layout-header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export function LayoutHeader() { 5 | return ( 6 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /playground/src/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | renderHeader: () => JSX.Element; 5 | renderContent: () => JSX.Element; 6 | renderFooter?: () => JSX.Element; 7 | }; 8 | 9 | export function Layout({ renderHeader, renderContent, renderFooter }: Props) { 10 | return ( 11 |
12 | {renderHeader()} 13 |
14 | {renderContent()} 15 | {renderFooter && renderFooter()} 16 |
17 | ); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /playground/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; 2 | -------------------------------------------------------------------------------- /playground/src/models/nominal-types.ts: -------------------------------------------------------------------------------- 1 | // Nominal Typing 2 | // Usefull to model domain concepts that are using primitive data type for it's value 3 | 4 | // Method 1: using "interface" 5 | export interface Name extends String { 6 | _brand: 'Name'; 7 | } 8 | const createName = (name: string): Name => { 9 | // validation of business rules 10 | return name as any; 11 | }; 12 | 13 | // Method 2: using "type" 14 | type Surname = string & { _brand: 'Surname' }; 15 | const createSurname = (surname: string): Surname => { 16 | // validation of business rules 17 | return surname as any; 18 | }; 19 | 20 | type Person = { 21 | name: Name; 22 | surname: Surname; 23 | }; 24 | 25 | const person: Person = { 26 | name: createName('Piotr'), 27 | surname: createSurname('Witek'), 28 | }; 29 | 30 | // Type system will ensure that the domain objects can only contain correct data 31 | // person.name = 'Karol'; // error 32 | // person.name = person.surname; // error 33 | person.name = createName('Karol'); // OK! 34 | // person.surname = 'Mate'; // error 35 | // person.surname = person.name; // error 36 | person.surname = createSurname('Mate'); // OK! 37 | 38 | // easy casting to supertype 39 | export let str: string; 40 | str = person.name.toString(); // Method 1 & Method 2 41 | str = person.surname; // Method 2 only 42 | -------------------------------------------------------------------------------- /playground/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid'; 2 | 3 | export interface IUserDTO { 4 | id: string; 5 | first_name: string; 6 | last_name: string; 7 | } 8 | 9 | export interface IUser { 10 | id: string; 11 | firstName: string; 12 | lastName: string; 13 | fullName: string; 14 | 15 | serialize(): IUserDTO; 16 | } 17 | 18 | export class User implements IUser { 19 | id: string = cuid(); 20 | get fullName(): string { 21 | return `${this.firstName} ${this.lastName}`; 22 | } 23 | 24 | constructor(public firstName: string, public lastName: string) {} 25 | 26 | static deserialize(dto: IUserDTO): IUser { 27 | const model = new User(dto.first_name, dto.last_name); 28 | model.id = dto.id; 29 | 30 | return model; 31 | } 32 | 33 | serialize(): IUserDTO { 34 | return { 35 | id: this.id, 36 | first_name: this.firstName, 37 | last_name: this.lastName, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /playground/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/src/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FCCounterUsage from '../components/fc-counter.usage'; 3 | import FCCounterWithDefaultPropsUsage from '../components/fc-counter-with-default-props.usage'; 4 | import FCSpreadAttributesUsage from '../components/fc-spread-attributes.usage'; 5 | import ClassCounterUsage from '../components/class-counter.usage'; 6 | import ClassCounterWithDefaultPropsUsage from '../components/class-counter-with-default-props.usage'; 7 | import UserListUsage from '../components/generic-list.usage'; 8 | import WithErrorBoundaryUsage from '../hoc/with-error-boundary.usage'; 9 | import WithStateUsage from '../hoc/with-state.usage'; 10 | import WithConnectedCountUsage from '../hoc/with-connected-count.usage'; 11 | 12 | export function Home() { 13 | return ( 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /playground/src/routes/not-found.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export function NotFound() { 5 | return ( 6 |
7 |

Not found!

8 |

9 | Go to the home page 10 |

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /playground/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // This optional code is used to register a service worker. 3 | // register() is not called by default. 4 | 5 | // This lets the app load faster on subsequent visits in production, and gives 6 | // it offline capabilities. However, it also means that developers (and users) 7 | // will only see deployed updates on subsequent visits to a page, after all the 8 | // existing tabs open on the page have been closed, since previously cached 9 | // resources are updated in the background. 10 | 11 | // To learn more about the benefits of this model and instructions on how to 12 | // opt-in, read https://bit.ly/CRA-PWA 13 | 14 | const isLocalhost = Boolean( 15 | window.location.hostname === 'localhost' || 16 | // [::1] is the IPv6 localhost address. 17 | window.location.hostname === '[::1]' || 18 | // 127.0.0.1/8 is considered localhost for IPv4. 19 | window.location.hostname.match( 20 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 21 | ) 22 | ); 23 | 24 | type Config = { 25 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 26 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 27 | }; 28 | 29 | export function register(config?: Config) { 30 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 31 | // The URL constructor is available in all browsers that support SW. 32 | const publicUrl = new URL( 33 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 34 | window.location.href 35 | ); 36 | if (publicUrl.origin !== window.location.origin) { 37 | // Our service worker won't work if PUBLIC_URL is on a different origin 38 | // from what our page is served on. This might happen if a CDN is used to 39 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 40 | return; 41 | } 42 | 43 | window.addEventListener('load', () => { 44 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 45 | 46 | if (isLocalhost) { 47 | // This is running on localhost. Let's check if a service worker still exists or not. 48 | checkValidServiceWorker(swUrl, config); 49 | 50 | // Add some additional logging to localhost, pointing developers to the 51 | // service worker/PWA documentation. 52 | navigator.serviceWorker.ready.then(() => { 53 | console.log( 54 | 'This web app is being served cache-first by a service ' + 55 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 56 | ); 57 | }); 58 | } else { 59 | // Is not localhost. Just register service worker 60 | registerValidSW(swUrl, config); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | function registerValidSW(swUrl: string, config?: Config) { 67 | navigator.serviceWorker 68 | .register(swUrl) 69 | .then(registration => { 70 | registration.onupdatefound = () => { 71 | const installingWorker = registration.installing; 72 | if (installingWorker == null) { 73 | return; 74 | } 75 | installingWorker.onstatechange = () => { 76 | if (installingWorker.state === 'installed') { 77 | if (navigator.serviceWorker.controller) { 78 | // At this point, the updated precached content has been fetched, 79 | // but the previous service worker will still serve the older 80 | // content until all client tabs are closed. 81 | console.log( 82 | 'New content is available and will be used when all ' + 83 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 84 | ); 85 | 86 | // Execute callback 87 | if (config && config.onUpdate) { 88 | config.onUpdate(registration); 89 | } 90 | } else { 91 | // At this point, everything has been precached. 92 | // It's the perfect time to display a 93 | // "Content is cached for offline use." message. 94 | console.log('Content is cached for offline use.'); 95 | 96 | // Execute callback 97 | if (config && config.onSuccess) { 98 | config.onSuccess(registration); 99 | } 100 | } 101 | } 102 | }; 103 | }; 104 | }) 105 | .catch(error => { 106 | console.error('Error during service worker registration:', error); 107 | }); 108 | } 109 | 110 | function checkValidServiceWorker(swUrl: string, config?: Config) { 111 | // Check if the service worker can be found. If it can't reload the page. 112 | fetch(swUrl) 113 | .then(response => { 114 | // Ensure service worker exists, and that we really are getting a JS file. 115 | const contentType = response.headers.get('content-type'); 116 | if ( 117 | response.status === 404 || 118 | (contentType != null && contentType.indexOf('javascript') === -1) 119 | ) { 120 | // No service worker found. Probably a different app. Reload the page. 121 | navigator.serviceWorker.ready.then(registration => { 122 | registration.unregister().then(() => { 123 | window.location.reload(); 124 | }); 125 | }); 126 | } else { 127 | // Service worker found. Proceed as normal. 128 | registerValidSW(swUrl, config); 129 | } 130 | }) 131 | .catch(() => { 132 | console.log( 133 | 'No internet connection found. App is running in offline mode.' 134 | ); 135 | }); 136 | } 137 | 138 | export function unregister() { 139 | if ('serviceWorker' in navigator) { 140 | navigator.serviceWorker.ready.then(registration => { 141 | registration.unregister(); 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /playground/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import * as logger from './logger-service'; 2 | import * as localStorage from './local-storage-service'; 3 | 4 | export default { 5 | logger, 6 | localStorage, 7 | }; 8 | -------------------------------------------------------------------------------- /playground/src/services/local-storage-service.ts: -------------------------------------------------------------------------------- 1 | const version = process.env.APP_VERSION; 2 | const STORAGE_KEY = `__SERIALIZED_STATE_TREE_v${version}__`; 3 | 4 | export function saveState(storeState: T): boolean { 5 | if (!localStorage) { 6 | return false; 7 | } 8 | 9 | try { 10 | const serializedState = JSON.stringify(storeState); 11 | localStorage.setItem(STORAGE_KEY, serializedState); 12 | return true; 13 | } catch (error) { 14 | throw new Error('store serialization failed'); 15 | } 16 | } 17 | 18 | export function loadState(): T | undefined { 19 | if (!localStorage) { 20 | return; 21 | } 22 | 23 | try { 24 | const serializedState = localStorage.getItem(STORAGE_KEY); 25 | if (serializedState == null) { 26 | return; 27 | } 28 | return JSON.parse(serializedState); 29 | } catch (error) { 30 | throw new Error('store deserialization failed'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /playground/src/services/logger-service.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-console 2 | export const log = console.log; 3 | -------------------------------------------------------------------------------- /playground/src/services/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'MyTypes' { 2 | export type Services = typeof import('./index').default; 3 | } 4 | -------------------------------------------------------------------------------- /playground/src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { 3 | TypedUseSelectorHook, 4 | useSelector as useGenericSelector, 5 | useDispatch as useGenericDispatch 6 | } from 'react-redux'; 7 | import { RootState, RootAction } from 'MyTypes'; 8 | 9 | export const useSelector: TypedUseSelectorHook = useGenericSelector; 10 | 11 | export const useDispatch: () => Dispatch = useGenericDispatch; 12 | -------------------------------------------------------------------------------- /playground/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { default as store } from './store'; 2 | export * from './redux-router'; 3 | -------------------------------------------------------------------------------- /playground/src/store/redux-router.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | import { createRouterReducer } from '@lagunovsky/redux-react-router'; 3 | import { createRouterMiddleware } from '@lagunovsky/redux-react-router' 4 | 5 | export const history = createBrowserHistory(); 6 | export const routerReducer = createRouterReducer(history); 7 | export const routerMiddleware = createRouterMiddleware(history) 8 | -------------------------------------------------------------------------------- /playground/src/store/root-action.ts: -------------------------------------------------------------------------------- 1 | import * as todosActions from '../features/todos/actions'; 2 | import * as countersActions from '../features/counters/actions'; 3 | import { routerActions } from '@lagunovsky/redux-react-router' 4 | 5 | export default { 6 | router: routerActions, 7 | todos: todosActions, 8 | counters: countersActions, 9 | }; 10 | -------------------------------------------------------------------------------- /playground/src/store/root-epic.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics } from 'redux-observable'; 2 | 3 | import * as todosEpics from '../features/todos/epics'; 4 | 5 | export default combineEpics(...Object.values(todosEpics)); 6 | -------------------------------------------------------------------------------- /playground/src/store/root-reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import countersReducer from '../features/counters/reducer'; 4 | import todosReducer from '../features/todos/reducer'; 5 | import { routerReducer } from './redux-router'; 6 | 7 | const rootReducer = combineReducers({ 8 | router: routerReducer, 9 | todos: todosReducer, 10 | counters: countersReducer, 11 | }); 12 | 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /playground/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { RootAction, RootState, Services } from 'MyTypes'; 2 | import { applyMiddleware, createStore } from 'redux'; 3 | import { createEpicMiddleware } from 'redux-observable'; 4 | 5 | import services from '../services'; 6 | import { routerMiddleware } from './redux-router'; 7 | import rootEpic from './root-epic'; 8 | import rootReducer from './root-reducer'; 9 | import { composeEnhancers } from './utils'; 10 | 11 | const epicMiddleware = createEpicMiddleware< 12 | RootAction, 13 | RootAction, 14 | RootState, 15 | Services 16 | >({ 17 | dependencies: services, 18 | }); 19 | 20 | // configure middlewares 21 | const middlewares = [epicMiddleware, routerMiddleware]; 22 | // compose enhancers 23 | const enhancer = composeEnhancers(applyMiddleware(...middlewares)); 24 | 25 | // rehydrate state on app start 26 | const initialState = {}; 27 | 28 | // create store 29 | const store = createStore( 30 | rootReducer, 31 | initialState, 32 | enhancer 33 | ); 34 | 35 | epicMiddleware.run(rootEpic); 36 | 37 | // export store singleton instance 38 | export default store; 39 | -------------------------------------------------------------------------------- /playground/src/store/types.d.ts: -------------------------------------------------------------------------------- 1 | import { StateType, ActionType } from 'typesafe-actions'; 2 | 3 | declare module 'MyTypes' { 4 | export type Store = StateType; 5 | export type RootAction = ActionType; 6 | export type RootState = StateType>; 7 | } 8 | 9 | declare module 'typesafe-actions' { 10 | interface Types { 11 | RootAction: ActionType; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground/src/store/utils.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'redux'; 2 | 3 | export const composeEnhancers = 4 | (process.env.NODE_ENV === 'development' && 5 | window && 6 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || 7 | compose; 8 | -------------------------------------------------------------------------------- /playground/src/storyshots.disabled-test.ts: -------------------------------------------------------------------------------- 1 | // import initStoryshots, { 2 | // multiSnapshotWithOptions, 3 | // } from '@storybook/addon-storyshots'; 4 | 5 | // initStoryshots({ 6 | // integrityOptions: { cwd: __dirname }, 7 | // test: multiSnapshotWithOptions({}), 8 | // }); 9 | 10 | export {} 11 | -------------------------------------------------------------------------------- /playground/styleguide/docs/intro.md: -------------------------------------------------------------------------------- 1 | ### Styleguide 2 | 3 | [⇦ back to guide](https://github.com/piotrwitek/react-redux-typescript-guide#table-of-contents) 4 | -------------------------------------------------------------------------------- /playground/styleguide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styleguide", 3 | "description": "Styleguide for https://github.com/piotrwitek/react-redux-typescript-guide", 4 | "version": "1.0.0", 5 | "private": true, 6 | "author": "Piotr Witek (http://piotrwitek.github.io/)", 7 | "repository": "https://github.com/piotrwitek/react-redux-typescript-guide.git", 8 | "license": "MIT", 9 | "main": "src/index.tsx", 10 | "scripts": { 11 | "styleguide": "styleguidist server", 12 | "styleguide:build": "styleguidist build" 13 | }, 14 | "dependencies": { 15 | "react-docgen-typescript": "1.12.3", 16 | "react-styleguidist": "8.0.6", 17 | "typescript": "3.1.6", 18 | "webpack-blocks": "1.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /playground/styleguide/styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const { createConfig } = require('webpack-blocks'); 4 | const typescript = require('@webpack-blocks/typescript'); 5 | const webpackConfig = createConfig([typescript()]); 6 | webpackConfig.resolve.alias = { '@src': path.join(__dirname, 'src') }; 7 | 8 | module.exports = { 9 | showUsage: false, 10 | styleguideDir: '../docs/', 11 | title: 'React & Redux in TypeScript - Component Typing Patterns', 12 | ignore: ['**/*.usage.tsx'], 13 | sections: [ 14 | { 15 | name: 'Introduction', 16 | content: './docs/intro.md', 17 | }, 18 | { 19 | name: 'Function Components', 20 | components: () => [ 21 | './src/components/fc-counter.tsx', 22 | './src/components/fc-spread-attributes.tsx', 23 | ], 24 | }, 25 | { 26 | name: 'Class Components', 27 | components: () => [ 28 | './src/components/class-counter.tsx', 29 | './src/components/class-counter-with-default-props.tsx', 30 | ], 31 | }, 32 | { 33 | name: 'Generic Components', 34 | components: () => ['./src/components/generic-list.tsx'], 35 | }, 36 | { 37 | name: 'Render Props', 38 | components: () => [ 39 | './src/components/name-provider.tsx', 40 | './src/components/mouse-provider.tsx', 41 | ], 42 | }, 43 | ], 44 | theme: { 45 | sidebarWidth: 300, 46 | }, 47 | propsParser: require('react-docgen-typescript').parse, 48 | webpackConfig: webpackConfig, 49 | updateExample: function(props, exampleFilePath) { 50 | if (typeof props.settings.filePath === 'string') { 51 | const { 52 | settings: { filePath }, 53 | } = props; 54 | delete props.settings.filePath; 55 | 56 | props.content = fs.readFileSync( 57 | path.resolve(exampleFilePath, '..', filePath), 58 | { encoding: 'utf-8' } 59 | ); 60 | props.settings.static = true; 61 | } 62 | 63 | return props; 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src", 25 | "typings" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /playground/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /playground/typings/augmentations.d.ts: -------------------------------------------------------------------------------- 1 | // typings/augmentations.ts 2 | 3 | // import { Operator } from 'rxjs/Operator'; 4 | // import { Observable } from 'rxjs/Observable'; 5 | // declare module 'rxjs/Subject' { 6 | // // tslint:disable-next-line:interface-name 7 | // interface Subject { 8 | // lift(operator: Operator): Observable; 9 | // } 10 | // } 11 | -------------------------------------------------------------------------------- /playground/typings/globals.d.ts: -------------------------------------------------------------------------------- 1 | // typings/globals.d.ts 2 | 3 | declare interface Window { 4 | __REDUX_DEVTOOLS_EXTENSION__: any; 5 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 6 | } 7 | 8 | declare interface NodeModule { 9 | hot?: { accept: (path: string, callback: () => void) => void }; 10 | } 11 | 12 | declare interface System { 13 | import(module: string): Promise; 14 | } 15 | declare var System: System; 16 | 17 | // declare const process: any; 18 | // declare const require: any; 19 | -------------------------------------------------------------------------------- /playground/typings/modules.d.ts: -------------------------------------------------------------------------------- 1 | // typings/modules.d.ts 2 | declare module 'MyTypes'; 3 | declare module 'react-test-renderer'; 4 | declare module '@storybook/addon-storyshots' 5 | -------------------------------------------------------------------------------- /playground/typings/redux-thunk/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | ActionCreatorsMapObject, 4 | AnyAction, 5 | Dispatch, 6 | Middleware, 7 | } from 'redux'; 8 | 9 | export interface ThunkDispatch { 10 | (thunkAction: ThunkAction): R; 11 | (action: T): T; 12 | } 13 | 14 | export type ThunkAction = ( 15 | dispatch: ThunkDispatch, 16 | getState: () => S, 17 | extraArgument: E 18 | ) => R; 19 | 20 | /** 21 | * Takes a ThunkAction and returns a function signature which matches how it would appear when processed using 22 | * bindActionCreators 23 | * 24 | * @template T ThunkAction to be wrapped 25 | */ 26 | export type ThunkActionDispatch< 27 | T extends (...args: any[]) => ThunkAction 28 | > = (...args: Parameters) => ReturnType>; 29 | 30 | export type ThunkMiddleware< 31 | S = {}, 32 | A extends Action = AnyAction, 33 | E = undefined 34 | > = Middleware, S, ThunkDispatch>; 35 | 36 | declare const thunk: ThunkMiddleware & { 37 | withExtraArgument(extraArgument: E): ThunkMiddleware<{}, AnyAction, E>; 38 | }; 39 | 40 | export default thunk; 41 | 42 | /** 43 | * Redux behaviour changed by middleware, so overloads here 44 | */ 45 | declare module 'redux' { 46 | /** 47 | * Overload for bindActionCreators redux function, returns expects responses 48 | * from thunk actions 49 | */ 50 | function bindActionCreators>( 51 | actionCreators: M, 52 | dispatch: Dispatch 53 | ): { 54 | [N in keyof M]: ReturnType extends ThunkAction 55 | ? (...args: Parameters) => ReturnType> 56 | : M[N] 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /playground/typings/redux/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * An *action* is a plain object that represents an intention to change the 4 | * state. Actions are the only way to get data into the store. Any data, 5 | * whether from UI events, network callbacks, or other sources such as 6 | * WebSockets needs to eventually be dispatched as actions. 7 | * 8 | * Actions must have a `type` field that indicates the type of action being 9 | * performed. Types can be defined as constants and imported from another 10 | * module. It's better to use strings for `type` than Symbols because strings 11 | * are serializable. 12 | * 13 | * Other than `type`, the structure of an action object is really up to you. 14 | * If you're interested, check out Flux Standard Action for recommendations on 15 | * how actions should be constructed. 16 | * 17 | * @template T the type of the action's `type` tag. 18 | */ 19 | export interface Action { 20 | type: T; 21 | } 22 | 23 | /* reducers */ 24 | 25 | /** 26 | * A *reducer* (also called a *reducing function*) is a function that accepts 27 | * an accumulation and a value and returns a new accumulation. They are used 28 | * to reduce a collection of values down to a single value 29 | * 30 | * Reducers are not unique to Redux—they are a fundamental concept in 31 | * functional programming. Even most non-functional languages, like 32 | * JavaScript, have a built-in API for reducing. In JavaScript, it's 33 | * `Array.prototype.reduce()`. 34 | * 35 | * In Redux, the accumulated value is the state object, and the values being 36 | * accumulated are actions. Reducers calculate a new state given the previous 37 | * state and an action. They must be *pure functions*—functions that return 38 | * the exact same output for given inputs. They should also be free of 39 | * side-effects. This is what enables exciting features like hot reloading and 40 | * time travel. 41 | * 42 | * Reducers are the most important concept in Redux. 43 | * 44 | * *Do not put API calls into reducers.* 45 | * 46 | * @template S The type of state consumed and produced by this reducer. 47 | * @template A The type of actions the reducer can potentially respond to. 48 | */ 49 | export type Reducer = (state: S | undefined, action: A) => S; 50 | 51 | /** 52 | * Object whose values correspond to different reducer functions. 53 | * 54 | * @template A The type of actions the reducers can potentially respond to. 55 | */ 56 | export type ReducersMapObject = { 57 | [K in keyof S]: Reducer; 58 | } 59 | 60 | /** 61 | * Turns an object whose values are different reducer functions, into a single 62 | * reducer function. It will call every child reducer, and gather their results 63 | * into a single state object, whose keys correspond to the keys of the passed 64 | * reducer functions. 65 | * 66 | * @template S Combined state object type. 67 | * 68 | * @param reducers An object whose values correspond to different reducer 69 | * functions that need to be combined into one. One handy way to obtain it 70 | * is to use ES6 `import * as reducers` syntax. The reducers may never 71 | * return undefined for any action. Instead, they should return their 72 | * initial state if the state passed to them was undefined, and the current 73 | * state for any unrecognized action. 74 | * 75 | * @returns A reducer function that invokes every reducer inside the passed 76 | * object, and builds a state object with the same shape. 77 | */ 78 | export function combineReducers(reducers: ReducersMapObject): Reducer; 79 | 80 | 81 | /* store */ 82 | 83 | /** 84 | * A *dispatching function* (or simply *dispatch function*) is a function that 85 | * accepts an action or an async action; it then may or may not dispatch one 86 | * or more actions to the store. 87 | * 88 | * We must distinguish between dispatching functions in general and the base 89 | * `dispatch` function provided by the store instance without any middleware. 90 | * 91 | * The base dispatch function *always* synchronously sends an action to the 92 | * store's reducer, along with the previous state returned by the store, to 93 | * calculate a new state. It expects actions to be plain objects ready to be 94 | * consumed by the reducer. 95 | * 96 | * Middleware wraps the base dispatch function. It allows the dispatch 97 | * function to handle async actions in addition to actions. Middleware may 98 | * transform, delay, ignore, or otherwise interpret actions or async actions 99 | * before passing them to the next middleware. 100 | * 101 | * @template D the type of things (actions or otherwise) which may be dispatched. 102 | */ 103 | export interface Dispatch { 104 | (action: A): A; 105 | } 106 | 107 | /** 108 | * Function to remove listener added by `Store.subscribe()`. 109 | */ 110 | export interface Unsubscribe { 111 | (): void; 112 | } 113 | 114 | /** 115 | * A store is an object that holds the application's state tree. 116 | * There should only be a single store in a Redux app, as the composition 117 | * happens on the reducer level. 118 | * 119 | * @template S The type of state held by this store. 120 | * @template A the type of actions which may be dispatched by this store. 121 | * @template N The type of non-actions which may be dispatched by this store. 122 | */ 123 | export interface Store { 124 | /** 125 | * Dispatches an action. It is the only way to trigger a state change. 126 | * 127 | * The `reducer` function, used to create the store, will be called with the 128 | * current state tree and the given `action`. Its return value will be 129 | * considered the **next** state of the tree, and the change listeners will 130 | * be notified. 131 | * 132 | * The base implementation only supports plain object actions. If you want 133 | * to dispatch a Promise, an Observable, a thunk, or something else, you 134 | * need to wrap your store creating function into the corresponding 135 | * middleware. For example, see the documentation for the `redux-thunk` 136 | * package. Even the middleware will eventually dispatch plain object 137 | * actions using this method. 138 | * 139 | * @param action A plain object representing “what changed”. It is a good 140 | * idea to keep actions serializable so you can record and replay user 141 | * sessions, or use the time travelling `redux-devtools`. An action must 142 | * have a `type` property which may not be `undefined`. It is a good idea 143 | * to use string constants for action types. 144 | * 145 | * @returns For convenience, the same action object you dispatched. 146 | * 147 | * Note that, if you use a custom middleware, it may wrap `dispatch()` to 148 | * return something else (for example, a Promise you can await). 149 | */ 150 | dispatch: Dispatch; 151 | 152 | /** 153 | * Reads the state tree managed by the store. 154 | * 155 | * @returns The current state tree of your application. 156 | */ 157 | getState(): S; 158 | 159 | /** 160 | * Adds a change listener. It will be called any time an action is 161 | * dispatched, and some part of the state tree may potentially have changed. 162 | * You may then call `getState()` to read the current state tree inside the 163 | * callback. 164 | * 165 | * You may call `dispatch()` from a change listener, with the following 166 | * caveats: 167 | * 168 | * 1. The subscriptions are snapshotted just before every `dispatch()` call. 169 | * If you subscribe or unsubscribe while the listeners are being invoked, 170 | * this will not have any effect on the `dispatch()` that is currently in 171 | * progress. However, the next `dispatch()` call, whether nested or not, 172 | * will use a more recent snapshot of the subscription list. 173 | * 174 | * 2. The listener should not expect to see all states changes, as the state 175 | * might have been updated multiple times during a nested `dispatch()` before 176 | * the listener is called. It is, however, guaranteed that all subscribers 177 | * registered before the `dispatch()` started will be called with the latest 178 | * state by the time it exits. 179 | * 180 | * @param listener A callback to be invoked on every dispatch. 181 | * @returns A function to remove this change listener. 182 | */ 183 | subscribe(listener: () => void): Unsubscribe; 184 | 185 | /** 186 | * Replaces the reducer currently used by the store to calculate the state. 187 | * 188 | * You might need this if your app implements code splitting and you want to 189 | * load some of the reducers dynamically. You might also need this if you 190 | * implement a hot reloading mechanism for Redux. 191 | * 192 | * @param nextReducer The reducer for the store to use instead. 193 | */ 194 | replaceReducer(nextReducer: Reducer): void; 195 | } 196 | 197 | export type DeepPartial = {[K in keyof T]?: DeepPartial }; 198 | 199 | /** 200 | * A store creator is a function that creates a Redux store. Like with 201 | * dispatching function, we must distinguish the base store creator, 202 | * `createStore(reducer, preloadedState)` exported from the Redux package, from 203 | * store creators that are returned from the store enhancers. 204 | * 205 | * @template S The type of state to be held by the store. 206 | * @template A The type of actions which may be dispatched. 207 | * @template D The type of all things which may be dispatched. 208 | */ 209 | export interface StoreCreator { 210 | (reducer: Reducer, enhancer?: StoreEnhancer): Store; 211 | (reducer: Reducer, preloadedState: DeepPartial, enhancer?: StoreEnhancer): Store; 212 | } 213 | 214 | /** 215 | * A store enhancer is a higher-order function that composes a store creator 216 | * to return a new, enhanced store creator. This is similar to middleware in 217 | * that it allows you to alter the store interface in a composable way. 218 | * 219 | * Store enhancers are much the same concept as higher-order components in 220 | * React, which are also occasionally called “component enhancers”. 221 | * 222 | * Because a store is not an instance, but rather a plain-object collection of 223 | * functions, copies can be easily created and modified without mutating the 224 | * original store. There is an example in `compose` documentation 225 | * demonstrating that. 226 | * 227 | * Most likely you'll never write a store enhancer, but you may use the one 228 | * provided by the developer tools. It is what makes time travel possible 229 | * without the app being aware it is happening. Amusingly, the Redux 230 | * middleware implementation is itself a store enhancer. 231 | * 232 | */ 233 | export type StoreEnhancer = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; 234 | export type GenericStoreEnhancer = StoreEnhancer; 235 | export type StoreEnhancerStoreCreator = (reducer: Reducer, preloadedState?: DeepPartial) => Store; 236 | 237 | /** 238 | * Creates a Redux store that holds the state tree. 239 | * The only way to change the data in the store is to call `dispatch()` on it. 240 | * 241 | * There should only be a single store in your app. To specify how different 242 | * parts of the state tree respond to actions, you may combine several 243 | * reducers 244 | * into a single reducer function by using `combineReducers`. 245 | * 246 | * @template S State object type. 247 | * 248 | * @param reducer A function that returns the next state tree, given the 249 | * current state tree and the action to handle. 250 | * 251 | * @param [preloadedState] The initial state. You may optionally specify it to 252 | * hydrate the state from the server in universal apps, or to restore a 253 | * previously serialized user session. If you use `combineReducers` to 254 | * produce the root reducer function, this must be an object with the same 255 | * shape as `combineReducers` keys. 256 | * 257 | * @param [enhancer] The store enhancer. You may optionally specify it to 258 | * enhance the store with third-party capabilities such as middleware, time 259 | * travel, persistence, etc. The only store enhancer that ships with Redux 260 | * is `applyMiddleware()`. 261 | * 262 | * @returns A Redux store that lets you read the state, dispatch actions and 263 | * subscribe to changes. 264 | */ 265 | export const createStore: StoreCreator; 266 | 267 | 268 | /* middleware */ 269 | 270 | export interface MiddlewareAPI { 271 | dispatch: Dispatch; 272 | getState(): S; 273 | } 274 | 275 | /** 276 | * A middleware is a higher-order function that composes a dispatch function 277 | * to return a new dispatch function. It often turns async actions into 278 | * actions. 279 | * 280 | * Middleware is composable using function composition. It is useful for 281 | * logging actions, performing side effects like routing, or turning an 282 | * asynchronous API call into a series of synchronous actions. 283 | */ 284 | export interface Middleware { 285 | (api: MiddlewareAPI): (next: Dispatch) => Dispatch; 286 | } 287 | 288 | /** 289 | * Creates a store enhancer that applies middleware to the dispatch method 290 | * of the Redux store. This is handy for a variety of tasks, such as 291 | * expressing asynchronous actions in a concise manner, or logging every 292 | * action payload. 293 | * 294 | * See `redux-thunk` package as an example of the Redux middleware. 295 | * 296 | * Because middleware is potentially asynchronous, this should be the first 297 | * store enhancer in the composition chain. 298 | * 299 | * Note that each middleware will be given the `dispatch` and `getState` 300 | * functions as named arguments. 301 | * 302 | * @param middlewares The middleware chain to be applied. 303 | * @returns A store enhancer applying the middleware. 304 | */ 305 | export function applyMiddleware(...middlewares: Middleware[]): GenericStoreEnhancer; 306 | 307 | 308 | /* action creators */ 309 | 310 | /** 311 | * An *action creator* is, quite simply, a function that creates an action. Do 312 | * not confuse the two terms—again, an action is a payload of information, and 313 | * an action creator is a factory that creates an action. 314 | * 315 | * Calling an action creator only produces an action, but does not dispatch 316 | * it. You need to call the store's `dispatch` function to actually cause the 317 | * mutation. Sometimes we say *bound action creators* to mean functions that 318 | * call an action creator and immediately dispatch its result to a specific 319 | * store instance. 320 | * 321 | * If an action creator needs to read the current state, perform an API call, 322 | * or cause a side effect, like a routing transition, it should return an 323 | * async action instead of an action. 324 | * 325 | * @template A Returned action type. 326 | */ 327 | export interface ActionCreator { 328 | (...args: any[]): A; 329 | } 330 | 331 | /** 332 | * Object whose values are action creator functions. 333 | */ 334 | export interface ActionCreatorsMapObject { 335 | [key: string]: ActionCreator; 336 | } 337 | 338 | /** 339 | * Turns an object whose values are action creators, into an object with the 340 | * same keys, but with every function wrapped into a `dispatch` call so they 341 | * may be invoked directly. This is just a convenience method, as you can call 342 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. 343 | * 344 | * For convenience, you can also pass a single function as the first argument, 345 | * and get a function in return. 346 | * 347 | * @param actionCreator An object whose values are action creator functions. 348 | * One handy way to obtain it is to use ES6 `import * as` syntax. You may 349 | * also pass a single function. 350 | * 351 | * @param dispatch The `dispatch` function available on your Redux store. 352 | * 353 | * @returns The object mimicking the original object, but with every action 354 | * creator wrapped into the `dispatch` call. If you passed a function as 355 | * `actionCreator`, the return value will also be a single function. 356 | */ 357 | export function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; 358 | 359 | export function bindActionCreators< 360 | A extends ActionCreator, 361 | B extends ActionCreator 362 | >(actionCreator: A, dispatch: Dispatch): B; 363 | 364 | export function bindActionCreators>(actionCreators: M, dispatch: Dispatch): M; 365 | 366 | export function bindActionCreators< 367 | M extends ActionCreatorsMapObject, 368 | N extends ActionCreatorsMapObject 369 | >(actionCreators: M, dispatch: Dispatch): N; 370 | 371 | 372 | /* compose */ 373 | 374 | type Func0 = () => R; 375 | type Func1 = (a1: T1) => R; 376 | type Func2 = (a1: T1, a2: T2) => R; 377 | type Func3 = (a1: T1, a2: T2, a3: T3, ...args: any[]) => R; 378 | 379 | /** 380 | * Composes single-argument functions from right to left. The rightmost 381 | * function can take multiple arguments as it provides the signature for the 382 | * resulting composite function. 383 | * 384 | * @param funcs The functions to compose. 385 | * @returns R function obtained by composing the argument functions from right 386 | * to left. For example, `compose(f, g, h)` is identical to doing 387 | * `(...args) => f(g(h(...args)))`. 388 | */ 389 | export function compose(): (a: R) => R; 390 | 391 | export function compose(f: F): F; 392 | 393 | /* two functions */ 394 | export function compose( 395 | f1: (b: A) => R, f2: Func0 396 | ): Func0; 397 | export function compose( 398 | f1: (b: A) => R, f2: Func1 399 | ): Func1; 400 | export function compose( 401 | f1: (b: A) => R, f2: Func2 402 | ): Func2; 403 | export function compose( 404 | f1: (b: A) => R, f2: Func3 405 | ): Func3; 406 | 407 | /* three functions */ 408 | export function compose( 409 | f1: (b: B) => R, f2: (a: A) => B, f3: Func0 410 | ): Func0; 411 | export function compose( 412 | f1: (b: B) => R, f2: (a: A) => B, f3: Func1 413 | ): Func1; 414 | export function compose( 415 | f1: (b: B) => R, f2: (a: A) => B, f3: Func2 416 | ): Func2; 417 | export function compose( 418 | f1: (b: B) => R, f2: (a: A) => B, f3: Func3 419 | ): Func3; 420 | 421 | /* four functions */ 422 | export function compose( 423 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func0 424 | ): Func0; 425 | export function compose( 426 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func1 427 | ): Func1; 428 | export function compose( 429 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func2 430 | ): Func2; 431 | export function compose( 432 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func3 433 | ): Func3; 434 | 435 | /* rest */ 436 | export function compose( 437 | f1: (b: any) => R, ...funcs: Function[] 438 | ): (...args: any[]) => R; 439 | 440 | export function compose(...funcs: Function[]): (...args: any[]) => R; 441 | --------------------------------------------------------------------------------