├── .yarnrc ├── jest.config.js ├── .idea ├── misc.xml ├── vcs.xml ├── modules.xml └── staged-components.iml ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── main.test.tsx.snap │ └── main.test.tsx └── index.tsx ├── gulpfile.js ├── tsconfig.json ├── package.json ├── LICENSE ├── README.md └── .gitignore /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org" 2 | 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: true, 4 | coverageDirectory: "coverage", 5 | }; 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/main.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`provider initialize 1`] = ``; 4 | 5 | exports[`provider initialize 2`] = ` 6 | 7 |
8 |

9 | 2 10 |

11 | 14 |
15 |
16 | `; 17 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const clean = require('gulp-clean') 3 | 4 | function cleanLib() { 5 | return gulp.src('lib', {read: false, allowEmpty: true}) 6 | .pipe(clean()) 7 | } 8 | 9 | function copyFiles() { 10 | return gulp 11 | .src(['package.json', 'README.md', 'LICENSE']) 12 | .pipe(gulp.dest('lib/')) 13 | } 14 | 15 | exports.prebuild = gulp.series(cleanLib, copyFiles) 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "declaration": true, 5 | "noImplicitAny": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "esModuleInterop": true, 9 | "strictNullChecks": true, 10 | "jsx": "react", 11 | "lib": ["es6", "dom"], 12 | "outDir": "./lib" 13 | }, 14 | "include": [ 15 | "./src/**/*" 16 | ], 17 | "exclude": [ 18 | "**/__tests__", 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.idea/staged-components.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "staged-components", 3 | "version": "1.1.3", 4 | "scripts": { 5 | "build": "gulp prebuild && tsc", 6 | "test": "jest" 7 | }, 8 | "description": "Make React function component staged.", 9 | "keywords": [ 10 | "react" 11 | ], 12 | "main": "index.js", 13 | "repository": "https://github.com/awmleer/staged-components", 14 | "author": "awmleer ", 15 | "license": "MIT", 16 | "private": false, 17 | "devDependencies": { 18 | "@testing-library/jest-dom": "^4.0.0", 19 | "@testing-library/react": "^8.0.1", 20 | "@types/jest": "^24.0.15", 21 | "@types/react": "^16.8.6", 22 | "gulp": "^4.0.2", 23 | "gulp-clean": "^0.4.0", 24 | "jest": "^24.8.0", 25 | "react": "^16.8.6", 26 | "react-dom": "^16.8.6", 27 | "ts-jest": "^24.0.2", 28 | "typescript": "^3.5.2", 29 | "use-debounce": "^2.1.0" 30 | }, 31 | "peerDependencies": { 32 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 33 | }, 34 | "publishConfig": { 35 | "registry": "https://registry.npmjs.org" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 awmleer 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 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC, PropsWithChildren, ReactElement, Ref, RefForwardingComponent} from 'react' 2 | 3 | type StageRender = () => StageRender | ReactElement | null 4 | type StageRenderRoot

= (props: PropsWithChildren

) => StageRender | ReactElement | null 5 | type StageRenderRootWithRef = (props: PropsWithChildren

, ref: Ref) => StageRender | ReactElement | null 6 | 7 | function processNext(next: StageRender | ReactElement | null) { 8 | if (typeof next === 'function') { 9 | return ( 10 | 11 | ) 12 | } else { 13 | return next 14 | } 15 | } 16 | 17 | function Stage

(props: { 18 | stage: StageRender 19 | }) { 20 | const next = props.stage() 21 | return processNext(next) 22 | } 23 | 24 | export function staged

( 25 | stage: StageRenderRoot

26 | ): FC

27 | export function staged

( 28 | stage: StageRenderRootWithRef, 29 | ): RefForwardingComponent 30 | export function staged

( 31 | stage: StageRenderRootWithRef, 32 | ) { 33 | return function Staged(props, ref) { 34 | const next = stage(props, ref) 35 | return processNext(next) 36 | } as FC

37 | } 38 | -------------------------------------------------------------------------------- /src/__tests__/main.test.tsx: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react' 2 | import {staged} from '..' 3 | import * as React from 'react' 4 | import {forwardRef, useEffect, useImperativeHandle, useState} from 'react' 5 | import * as testing from '@testing-library/react' 6 | 7 | export const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time)) 8 | 9 | test('provider initialize', async function () { 10 | const App = staged(() => { 11 | const [waiting, setWaiting] = useState(true) 12 | useEffect(() => { 13 | setTimeout(() => { 14 | act(() => { 15 | setWaiting(false) 16 | }) 17 | }, 300) 18 | }, []) 19 | if (waiting) return null 20 | return () => { 21 | const [count, setCount] = useState(1) 22 | return ( 23 |

24 |

{count}

25 | 26 |
27 | ) 28 | } 29 | }) 30 | const renderer = testing.render( 31 | 32 | ) 33 | expect(renderer.asFragment()).toMatchSnapshot() 34 | await sleep(500) 35 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change')) 36 | expect(renderer.asFragment()).toMatchSnapshot() 37 | }) 38 | 39 | test('usage with forwardRef', async function() { 40 | const ref = React.createRef() 41 | const App = forwardRef(staged<{}, number>((props, ref) => { 42 | useImperativeHandle(ref, () => 1) 43 | return null 44 | })) 45 | testing.render( 46 | 47 | ) 48 | expect(ref.current).toBe(1) 49 | }) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Staged Components 2 | 3 | Make React function component staged. 4 | 5 | ## Install 6 | 7 | ```bash 8 | yarn add staged-components 9 | # or 10 | npm install --save staged-components 11 | ``` 12 | 13 | ## Usages 14 | 15 | React Hook is awesome, but it has some kind of rules. One of these rules is ["Only Call Hooks at the Top Level"](https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level). 16 | 17 | So the component below will cause an error: 18 | 19 | ```jsx 20 | const App = function(props) { 21 | if (props.user === undefined) return null 22 | const [name, setName] = useState(props.user.name) 23 | // React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? 24 | return ( 25 | {setName(e.target.value)}}/> 26 | ) 27 | } 28 | ``` 29 | 30 | With `staged-components`, you can safely "break" this rule: 31 | 32 | ```jsx 33 | const App = staged((props) => { // stage 1 34 | if (props.user === undefined) return null 35 | return () => { // stage 2 36 | const [name, setName] = useState(props.user.name) 37 | return ( 38 | {setName(e.target.value)}}/> 39 | ) 40 | } 41 | }) 42 | ``` 43 | 44 | ## Advanced 45 | 46 | Usage with `forwardRef`: 47 | 48 | ```jsx 49 | const App = forwardRef(staged((props, ref) => { 50 | if (props.user === undefined) return null 51 | return () => { 52 | useImperativeHandle(ref, () => 'hello') 53 | return ( 54 |

{props.user.name}

55 | ) 56 | } 57 | })) 58 | ``` 59 | 60 | ```tsx 61 | type Props = {}; 62 | type Ref = {}; 63 | const App = forwardRef( 64 | staged((props, ref) => { 65 | if (props.user === undefined) return null; 66 | return () => { 67 | useImperativeHandle(ref, () => 'hello'); 68 | return

{props.user.name}

; 69 | }; 70 | }) 71 | ); 72 | ``` 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | 3 | # Webstorm 4 | ## User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | ## Generated files 11 | .idea/**/contentModel.xml 12 | # Sensitive or high-churn files 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | .idea/**/dbnavigator.xml 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Runtime data 29 | pids 30 | *.pid 31 | *.seed 32 | *.pid.lock 33 | 34 | # Directory for instrumented libs generated by jscoverage/JSCover 35 | lib-cov 36 | 37 | # Coverage directory used by tools like istanbul 38 | coverage 39 | 40 | # nyc test coverage 41 | .nyc_output 42 | 43 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 44 | .grunt 45 | 46 | # Bower dependency directory (https://bower.io/) 47 | bower_components 48 | 49 | # node-waf configuration 50 | .lock-wscript 51 | 52 | # Compiled binary addons (https://nodejs.org/api/addons.html) 53 | build/Release 54 | 55 | # Dependency directories 56 | node_modules/ 57 | jspm_packages/ 58 | 59 | # TypeScript v1 declaration files 60 | typings/ 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | 80 | # next.js build output 81 | .next 82 | 83 | ### macOS ### 84 | # General 85 | .DS_Store 86 | .AppleDouble 87 | .LSOverride 88 | 89 | # Icon must end with two \r 90 | Icon 91 | 92 | # Thumbnails 93 | ._* 94 | 95 | # Files that might appear in the root of a volume 96 | .DocumentRevisions-V100 97 | .fseventsd 98 | .Spotlight-V100 99 | .TemporaryItems 100 | .Trashes 101 | .VolumeIcon.icns 102 | .com.apple.timemachine.donotpresent 103 | 104 | # Directories potentially created on remote AFP share 105 | .AppleDB 106 | .AppleDesktop 107 | Network Trash Folder 108 | Temporary Items 109 | .apdisk 110 | --------------------------------------------------------------------------------