├── .prettierrc ├── .eslintrc.js ├── README.md ├── src ├── index.tsx ├── IncrementalGroupContext.tsx ├── IncrementalGroup.tsx ├── IncrementalPresenter.tsx └── Incremental.tsx ├── tsconfig.json ├── LICENSE ├── package.json └── .gitignore /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "bracketSameLine": false 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@react-native-community'], 3 | rules: { 4 | 'react-native/no-inline-styles': 'off' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-incremental 2 | 3 | Vendor of https://github.com/facebook/react-native/tree/2ee11711e0351f695ec1d5e591ac904e5c9187c4/Libraries/Experimental 4 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Incremental'; 2 | export * from './IncrementalGroup'; 3 | export * from './IncrementalPresenter'; 4 | export * from './IncrementalGroupContext'; 5 | -------------------------------------------------------------------------------- /src/IncrementalGroupContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export interface IncrementalGroupContextType { 4 | enabled: boolean; 5 | groupId: string; 6 | incrementalCount: number; 7 | } 8 | 9 | export const IncrementalGroupContext = 10 | createContext(null); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowUnreachableCode": false, 5 | "allowUnusedLabels": false, 6 | "esModuleInterop": true, 7 | "importsNotUsedAsValues": "error", 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react-native", 10 | "lib": ["dom", "esnext"], 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noStrictGenericChecks": false, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noUncheckedIndexedAccess": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "esnext", 23 | "outDir": "lib" 24 | }, 25 | "include": [ 26 | "src", 27 | ".eslintrc.js", 28 | "babel.config.js", 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | "lib" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Th3rdwave 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/IncrementalGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Incremental, IncrementalProps } from './Incremental'; 2 | import * as React from 'react'; 3 | import { IncrementalGroupContext } from './IncrementalGroupContext'; 4 | 5 | let _groupCounter = -1; 6 | const DEBUG = false; 7 | 8 | export interface IncrementalGroupProps extends IncrementalProps { 9 | disabled?: boolean; 10 | } 11 | 12 | /** 13 | * `` components must be wrapped in an `` (e.g. 14 | * via ``) in order to provide the incremental group 15 | * context, otherwise they will do nothing. 16 | * 17 | * See Incremental.tsx for more info. 18 | */ 19 | export function IncrementalGroup({ 20 | name, 21 | children, 22 | disabled, 23 | onDone, 24 | }: IncrementalGroupProps) { 25 | const groupIncRef = React.useRef(`g${++_groupCounter}-`); 26 | const context = React.useContext(IncrementalGroupContext); 27 | 28 | const getGroupId = React.useCallback(() => { 29 | const prefix = context != null ? context.groupId + ':' : ''; 30 | return prefix + groupIncRef.current + name; 31 | }, [context, name]); 32 | 33 | React.useEffect(() => { 34 | DEBUG && console.log('create IncrementalGroup with id ' + getGroupId()); 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, []); 37 | 38 | const childContext = React.useMemo( 39 | () => ({ 40 | enabled: !(disabled || context?.enabled === false), 41 | groupId: getGroupId(), 42 | incrementalCount: -1, 43 | }), 44 | [context?.enabled, disabled, getGroupId], 45 | ); 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@th3rdwave/react-native-incremental", 3 | "version": "2.0.0", 4 | "description": "Vendor of Incremental that was removed from react-native", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "react-native": "src/index.tsx", 8 | "types": "lib/typescript/index.d.ts", 9 | "source": "src/index.tsx", 10 | "sideEffects": false, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "prepare": "bob build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/th3rdwave/react-native-incremental.git" 18 | }, 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/th3rdwave/react-native-incremental/issues" 22 | }, 23 | "homepage": "https://github.com/th3rdwave/react-native-incremental#readme", 24 | "peerDependencies": { 25 | "react": "*", 26 | "react-native": "*" 27 | }, 28 | "devDependencies": { 29 | "@react-native-community/eslint-config": "^3.0.1", 30 | "@react-native/eslint-plugin-specs": "^0.0.3", 31 | "@types/jest": "^27.4.1", 32 | "@types/react-native": "^0.67.2", 33 | "@types/react-test-renderer": "^17.0.1", 34 | "@types/react": "^17.0.39", 35 | "@typescript-eslint/eslint-plugin": "^5.13.0", 36 | "@typescript-eslint/parser": "^5.13.0", 37 | "eslint-config-prettier": "^8.5.0", 38 | "eslint-plugin-prettier": "^4.0.0", 39 | "eslint": "^8.10.0", 40 | "jest": "^27.5.1", 41 | "prettier": "^2.5.1", 42 | "react-native-builder-bob": "^0.18.2", 43 | "react-native": "0.67.3", 44 | "react-test-renderer": "^17.0.2", 45 | "react": "^17.0.2", 46 | "typescript": "^4.6.2" 47 | }, 48 | "react-native-builder-bob": { 49 | "source": "src", 50 | "output": "lib", 51 | "targets": [ 52 | "commonjs", 53 | "module", 54 | "typescript" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/IncrementalPresenter.tsx: -------------------------------------------------------------------------------- 1 | import { IncrementalGroup } from './IncrementalGroup'; 2 | import * as React from 'react'; 3 | import { LayoutChangeEvent, StyleProp, View, ViewStyle } from 'react-native'; 4 | import { IncrementalGroupContext } from './IncrementalGroupContext'; 5 | 6 | /** 7 | * `` can be used to group sets of `` renders 8 | * such that they are initially invisible and removed from layout until all 9 | * descendants have finished rendering, at which point they are drawn all at once 10 | * so the UI doesn't jump around during the incremental rendering process. 11 | * 12 | * See Incremental.js for more info. 13 | */ 14 | export interface IncrementalPresenterProps { 15 | children?: React.ReactNode; 16 | disabled?: boolean; 17 | name: string; 18 | onDone?: () => void; 19 | onLayout?: (event: LayoutChangeEvent) => void; 20 | style?: ViewStyle; 21 | } 22 | 23 | export function IncrementalPresenter({ 24 | children, 25 | disabled, 26 | name, 27 | onDone, 28 | onLayout, 29 | style, 30 | }: IncrementalPresenterProps) { 31 | const isDoneRef = React.useRef(false); 32 | const context = React.useContext(IncrementalGroupContext); 33 | const viewRef = React.useRef(null); 34 | 35 | const onDoneInternal = () => { 36 | isDoneRef.current = true; 37 | if (disabled !== true && context?.enabled !== false) { 38 | // Avoid expensive re-renders and use setNativeProps 39 | viewRef.current?.setNativeProps({ 40 | style: [style, { opacity: 1, position: 'relative' }], 41 | }); 42 | } 43 | onDone?.(); 44 | }; 45 | 46 | let viewStyle: StyleProp; 47 | if (disabled !== true && context?.enabled !== false && !isDoneRef.current) { 48 | viewStyle = [style, { opacity: 0, position: 'absolute' }]; 49 | } else { 50 | viewStyle = style; 51 | } 52 | return ( 53 | 54 | 55 | {children} 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | lib 107 | -------------------------------------------------------------------------------- /src/Incremental.tsx: -------------------------------------------------------------------------------- 1 | import { InteractionManager } from 'react-native'; 2 | import * as React from 'react'; 3 | import { IncrementalGroupContext } from './IncrementalGroupContext'; 4 | 5 | const DEBUG = false; 6 | 7 | /** 8 | * React Native helps make apps smooth by doing all the heavy lifting off the 9 | * main thread, in JavaScript. That works great a lot of the time, except that 10 | * heavy operations like rendering may block the JS thread from responding 11 | * quickly to events like taps, making the app feel sluggish. 12 | * 13 | * `` solves this by slicing up rendering into chunks that are 14 | * spread across multiple event loops. Expensive components can be sliced up 15 | * recursively by wrapping pieces of them and their descendants in 16 | * `` components. `` can be used to make sure 17 | * everything in the group is rendered recursively before calling `onDone` and 18 | * moving on to another sibling group (e.g. render one row at a time, even if 19 | * rendering the top level row component produces more `` chunks). 20 | * `` is a type of `` that keeps it's 21 | * children invisible and out of the layout tree until all rendering completes 22 | * recursively. This means the group will be presented to the user as one unit, 23 | * rather than pieces popping in sequentially. 24 | * 25 | * `` only affects initial render - `setState` and other render 26 | * updates are unaffected. 27 | * 28 | * The chunks are rendered sequentially using the `InteractionManager` queue, 29 | * which means that rendering will pause if it's interrupted by an interaction, 30 | * such as an animation or gesture. 31 | * 32 | * Note there is some overhead, so you don't want to slice things up too much. 33 | * A target of 100-200ms of total work per event loop on old/slow devices might 34 | * be a reasonable place to start. 35 | * 36 | * Below is an example that will incrementally render all the parts of `Row` one 37 | * first, then present them together, then repeat the process for `Row` two, and 38 | * so on: 39 | * 40 | * render: function() { 41 | * return ( 42 | * 43 | * {Array(10).fill().map((rowIdx) => ( 44 | * 45 | * 46 | * {Array(20).fill().map((widgetIdx) => ( 47 | * 48 | * 49 | * 50 | * ))} 51 | * 52 | * 53 | * ))} 54 | * 55 | * ); 56 | * }; 57 | * 58 | * If SlowWidget takes 30ms to render, then without `Incremental`, this would 59 | * block the JS thread for at least `10 * 20 * 30ms = 6000ms`, but with 60 | * `Incremental` it will probably not block for more than 50-100ms at a time, 61 | * allowing user interactions to take place which might even unmount this 62 | * component, saving us from ever doing the remaining rendering work. 63 | */ 64 | export interface IncrementalProps { 65 | onDone?: () => void; 66 | name?: string; 67 | children: React.ReactNode; 68 | } 69 | 70 | export function Incremental({ 71 | name, 72 | children, 73 | onDone, 74 | }: IncrementalProps): React.ReactElement | null { 75 | const [doIncrementalRender, setDoIncrementalRender] = React.useState(false); 76 | const context = React.useContext(IncrementalGroupContext); 77 | const incrementalId = (context?.incrementalCount ?? 0) + 1; 78 | const incrementalIdRef = React.useRef(incrementalId); 79 | const mountedRef = React.useRef(false); 80 | const renderedRef = React.useRef(false); 81 | 82 | const getName = () => { 83 | return context?.groupId + ':' + incrementalIdRef.current + '-' + name; 84 | }; 85 | 86 | React.useEffect(() => { 87 | if (context == null) { 88 | return; 89 | } 90 | InteractionManager.runAfterInteractions({ 91 | name: 'Incremental:' + getName(), 92 | gen: () => 93 | new Promise((resolve) => { 94 | if (!mountedRef.current || renderedRef.current) { 95 | resolve(); 96 | return; 97 | } 98 | DEBUG && console.log('set doIncrementalRender for ' + getName()); 99 | setDoIncrementalRender(true); 100 | resolve(); 101 | }), 102 | }) 103 | .then(() => { 104 | DEBUG && console.log('call onDone for ' + getName()); 105 | if (mountedRef.current) { 106 | onDone?.(); 107 | } 108 | }) 109 | .catch((ex) => { 110 | ex.message = `Incremental render failed for ${getName()}: ${ 111 | ex.message 112 | }`; 113 | throw ex; 114 | }); 115 | // eslint-disable-next-line react-hooks/exhaustive-deps 116 | }, []); 117 | 118 | React.useEffect(() => { 119 | mountedRef.current = true; 120 | if (context == null) { 121 | onDone?.(); 122 | } 123 | return () => { 124 | mountedRef.current = false; 125 | }; 126 | // eslint-disable-next-line react-hooks/exhaustive-deps 127 | }, []); 128 | 129 | if ( 130 | renderedRef.current || // Make sure that once we render once, we stay rendered even if incrementalGroupEnabled gets flipped. 131 | !context?.enabled || 132 | doIncrementalRender 133 | ) { 134 | DEBUG && console.log('render ' + getName()); 135 | renderedRef.current = true; 136 | return children as React.ReactElement; 137 | } 138 | return null; 139 | } 140 | --------------------------------------------------------------------------------