├── .buckconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── demo └── useStateWithLayoutAnimation.gif ├── metro.config.js ├── package.json ├── src ├── __tests__ │ └── spring.test.ts ├── index.ts ├── types.ts └── useStateWithLayoutAnimation.ts ├── tsconfig.json └── yarn.lock /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | env: {'jest/globals': true}, 7 | }; 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | npm-publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@master 13 | - name: Set up NodeJS 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: '12' 17 | - name: Build package 18 | run: | 19 | npm i -g yarn 20 | yarn install 21 | yarn build 22 | - name: Publish 23 | uses: JS-DevTools/npm-publish@v1 24 | with: 25 | token: ${{ secrets.NPM_AUTH_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # Visual Studio Code 33 | # 34 | .vscode/ 35 | 36 | # node.js 37 | # 38 | node_modules/ 39 | npm-debug.log 40 | yarn-error.log 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | !debug.keystore 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | # For more information about the recommended setup visit: 53 | # https://docs.fastlane.tools/best-practices/source-control/ 54 | 55 | */fastlane/report.xml 56 | */fastlane/Preview.html 57 | */fastlane/screenshots 58 | 59 | # Bundle artifact 60 | *.jsbundle 61 | 62 | # CocoaPods 63 | /ios/Pods/ 64 | dist/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Karl Marx Lopez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | npm 2 | 3 | # useStateWithLayoutAnimation 4 | Abstraction for `React Native`'s `LayoutAnimation` and `useState` 5 | 6 | ## Install 7 | `yarn add use-state-with-layout-animation` 8 | 9 | Or 10 | 11 | `npm install --save use-state-with-layout-animation` 12 | 13 | ## Example 14 | 15 | ![Animated gif demo](demo/useStateWithLayoutAnimation.gif) 16 | 17 | [Download expo client and scan the QR code to run the snack on your `iOS` or `Android` device](https://snack.expo.io/@iamkarlmarx/usestatewithlayoutanimation). (It does not work on web) 18 | 19 | ## API 20 | 21 | ### `useStateWithLayoutAnimation` 22 | By default, `UIManager.setLayoutAnimationEnabledExperimental` is invoked, you can pass `false` as the second parameter if you want to call it on your own. 23 | ``` 24 | const [state, setState] = useStateWithLayoutAnimation(123, false); 25 | ``` 26 | ### `setState.spring` 27 | ### `setState.linear` 28 | ### `setState.easeInEaseOut` 29 | You can use this the same as `useState` setter, accepts values or optional callback function but accepts a second parameter for the animation finish callback. 30 | ```ts 31 | const [state, setState] = useStateWithLayoutAnimation(1); 32 | 33 | const animationDidFinish = () => console.log('Animation finished'); 34 | 35 | setState.spring(2, animationDidFinish); 36 | setState.linear(prev => prev + 10, animationDidFinish); 37 | setState.easeInEaseOut(4, animationDidFinish); 38 | ``` 39 | 40 | ### `setState.noAnimation` 41 | You can use this the same as `useState` setter, accepts values or optional callback function. 42 | ```ts 43 | setState.noAnimation(4); 44 | setState.noAnimation(prev => prev + 1); 45 | ``` 46 | 47 | 48 | ## License 49 | [MIT](https://github.com/karlmarxlopez/useStateWithLayoutAnimation/blob/master/LICENSE) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /demo/useStateWithLayoutAnimation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaghettiC0des/useStateWithLayoutAnimation/97b4eab27c7b82995e1a1801bf0a64f92ed68a7e/demo/useStateWithLayoutAnimation.gif -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-state-with-layout-animation", 3 | "version": "1.0.8", 4 | "private": false, 5 | "license": "MIT", 6 | "author": { 7 | "name": "Karl Marx Lopez", 8 | "email": "karlmarx.webdev@gmail.com" 9 | }, 10 | "repository": { 11 | "url": "https://github.com/karlmarxlopez/useStateWithLayoutAnimation" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "react hooks", 16 | "animation", 17 | "react-native", 18 | "hooks" 19 | ], 20 | "main": "dist/index.js", 21 | "module": "dist/index.js", 22 | "types": "dist/index.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "test": "jest", 29 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 30 | }, 31 | "peerDependencies": { 32 | "react": "16.13.1", 33 | "react-native": "0.63.2" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.8.4", 37 | "@babel/runtime": "^7.8.4", 38 | "@commitlint/cli": "^9.1.2", 39 | "@commitlint/config-conventional": "^9.1.2", 40 | "@react-native-community/eslint-config": "^1.1.0", 41 | "@testing-library/react-hooks": "^3.4.2", 42 | "@testing-library/react-native": "^7.0.2", 43 | "@types/jest": "^26.0.14", 44 | "@types/jest-in-case": "^1.0.2", 45 | "@types/react-native": "^0.63.2", 46 | "@types/react-test-renderer": "^16.9.2", 47 | "@typescript-eslint/eslint-plugin": "^2.27.0", 48 | "@typescript-eslint/parser": "^2.27.0", 49 | "babel-jest": "^26.3.0", 50 | "eslint": "^6.5.1", 51 | "husky": ">=4", 52 | "jest": "^26.4.2", 53 | "jest-in-case": "^1.0.2", 54 | "lint-staged": ">=10", 55 | "metro-react-native-babel-preset": "^0.59.0", 56 | "prettier": "^2.0.4", 57 | "react": "16.13.1", 58 | "react-native": "0.63.2", 59 | "react-test-renderer": "16.13.1", 60 | "typescript": "^3.8.3" 61 | }, 62 | "jest": { 63 | "preset": "react-native", 64 | "moduleFileExtensions": [ 65 | "ts", 66 | "tsx", 67 | "js", 68 | "jsx", 69 | "json", 70 | "node" 71 | ] 72 | }, 73 | "husky": { 74 | "hooks": { 75 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 76 | "pre-commit": "lint-staged" 77 | } 78 | }, 79 | "lint-staged": { 80 | "*.{js,jsx,ts,tsx}": "eslint --cache --fix", 81 | "*.{js,css,md}": "prettier --write" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/__tests__/spring.test.ts: -------------------------------------------------------------------------------- 1 | import {act, renderHook} from '@testing-library/react-hooks'; 2 | import {LayoutAnimation} from 'react-native'; 3 | import cases from 'jest-in-case'; 4 | import useStateWithLayoutAnimation from '../useStateWithLayoutAnimation'; 5 | 6 | jest.mock( 7 | '../../node_modules/react-native/Libraries/LayoutAnimation/LayoutAnimation.js', 8 | ); 9 | 10 | const mock = jest.fn(); 11 | 12 | beforeEach(() => jest.clearAllMocks()); 13 | 14 | cases( 15 | 'should methods change state and call callback', 16 | ({name, callback}) => { 17 | const {result} = renderHook(() => useStateWithLayoutAnimation(1)); 18 | const setState = result.current[1]; 19 | 20 | act(() => { 21 | setState[name](2, callback); 22 | }); 23 | 24 | const animationMock = LayoutAnimation[name]; 25 | const state = result.current[0]; 26 | 27 | expect(state).toBe(2); 28 | expect(animationMock).toBeCalledTimes(1); 29 | expect(animationMock.mock.calls[0][0]).toBe(callback); 30 | }, 31 | [ 32 | {name: 'spring', callback: mock}, 33 | {name: 'spring', callback: undefined}, 34 | {name: 'linear', callback: mock}, 35 | {name: 'linear', callback: undefined}, 36 | {name: 'easeInEaseOut', callback: mock}, 37 | {name: 'easeInEaseOut', callback: undefined}, 38 | ], 39 | ); 40 | 41 | test('should noAnimation method change only state', () => { 42 | const {result} = renderHook(() => useStateWithLayoutAnimation(1)); 43 | const setState = result.current[1]; 44 | 45 | act(() => { 46 | setState.noAnimation(2); 47 | }); 48 | 49 | const state = result.current[0]; 50 | 51 | expect(state).toBe(2); 52 | expect(LayoutAnimation.spring).toBeCalledTimes(0); 53 | expect(LayoutAnimation.linear).toBeCalledTimes(0); 54 | expect(LayoutAnimation.easeInEaseOut).toBeCalledTimes(0); 55 | }); 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './useStateWithLayoutAnimation'; 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {SetStateAction} from 'react'; 2 | 3 | export type OnAnimationDidEndFn = () => void; 4 | 5 | export type LayoutAnimationFn = ( 6 | onAnimationDidEnd?: OnAnimationDidEndFn, 7 | ) => void; 8 | 9 | export type SetStateWithLayoutAnimation = ( 10 | nextState: SetStateAction, 11 | onAnimationDidEnd?: OnAnimationDidEndFn, 12 | ) => void; 13 | 14 | export type StateSetter = { 15 | spring: SetStateWithLayoutAnimation; 16 | linear: SetStateWithLayoutAnimation; 17 | easeInEaseOut: SetStateWithLayoutAnimation; 18 | noAnimation: (nextState: SetStateAction) => void; 19 | }; 20 | -------------------------------------------------------------------------------- /src/useStateWithLayoutAnimation.ts: -------------------------------------------------------------------------------- 1 | import {useState, useRef, useEffect, SetStateAction, useCallback} from 'react'; 2 | import {LayoutAnimation, NativeModules} from 'react-native'; 3 | import {StateSetter, LayoutAnimationFn, OnAnimationDidEndFn} from './types'; 4 | 5 | const {UIManager} = NativeModules; 6 | 7 | const useStateWithLayoutAnimation = ( 8 | initialValue: S, 9 | setLayoutAnimationEnabledExperimental = true, 10 | ): [S, StateSetter] => { 11 | useEffect(() => { 12 | if (setLayoutAnimationEnabledExperimental) { 13 | UIManager.setLayoutAnimationEnabledExperimental && 14 | UIManager.setLayoutAnimationEnabledExperimental(true); 15 | } 16 | }, [setLayoutAnimationEnabledExperimental]); 17 | 18 | const [state, setState] = useState(initialValue); 19 | 20 | const partialSetState = useCallback( 21 | (animationFn: LayoutAnimationFn) => ( 22 | nextState: SetStateAction, 23 | onAnimationDidEnd?: OnAnimationDidEndFn, 24 | ) => { 25 | animationFn(onAnimationDidEnd); 26 | setState(nextState); 27 | }, 28 | [], 29 | ); 30 | 31 | const stateSetters = useRef({ 32 | spring: partialSetState(LayoutAnimation.spring), 33 | linear: partialSetState(LayoutAnimation.linear), 34 | easeInEaseOut: partialSetState(LayoutAnimation.easeInEaseOut), 35 | noAnimation: setState, 36 | }).current; 37 | 38 | return [state, stateSetters]; 39 | }; 40 | 41 | export default useStateWithLayoutAnimation; 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es6"], /* Specify library files to be included in the compilation. */ 7 | "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./dist", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | "noEmit": false, /* Do not emit outputs. */ 17 | // "incremental": true, /* Enable incremental compilation */ 18 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 20 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 21 | 22 | /* Strict Type-Checking Options */ 23 | "strict": true, /* Enable all strict type-checking options. */ 24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 27 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 28 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 29 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 30 | 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | 37 | /* Module Resolution Options */ 38 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 42 | // "typeRoots": [], /* List of folders to include type definitions from. */ 43 | // "types": [], /* Type declaration files to be included in compilation. */ 44 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 46 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 47 | 48 | /* Source Map Options */ 49 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 50 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 51 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 52 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 53 | 54 | /* Experimental Options */ 55 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 56 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 57 | }, 58 | "include": ["src/*.ts"], 59 | "exclude": [ 60 | "node_modules", "babel.config.js", "metro.config.js", "jest.config.js" 61 | ] 62 | } 63 | --------------------------------------------------------------------------------