Repository: StreakYC/react-draggable-list Branch: main Commit: 09672e398ad8 Files: 28 Total size: 69.5 KB Directory structure: gitextract_v5ra7eiw/ ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── example/ │ ├── Example.tsx │ ├── index.html │ ├── main.css │ └── main.tsx ├── index.js.flow ├── jest.config.js ├── package.json ├── src/ │ ├── MoveContainer.tsx │ ├── OnUpdate.tsx │ ├── TemplateContainer.tsx │ ├── index.test.tsx │ └── index.tsx ├── test-types/ │ └── typescript.tsx └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], "plugins": [ "@babel/plugin-transform-runtime", [ "add-module-exports", { "addDefaultProperty": true } ] ] } ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 ================================================ FILE: .eslintignore ================================================ /example/dist /dist *.js.flow ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { browser: true, jest: true, node: true, es6: true, }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', ], rules: { 'react/no-children-prop': ['off'], '@typescript-eslint/no-non-null-assertion': ['off'], '@typescript-eslint/no-explicit-any': ['off'], '@typescript-eslint/explicit-function-return-type': ['off'], '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '[iI]gnored' }, ], '@typescript-eslint/ban-types': ['off'], '@typescript-eslint/explicit-module-boundary-types': ['off'], indent: ['off'], 'react/jsx-indent': ['error', 2], 'react/jsx-indent-props': ['error', 2], 'linebreak-style': ['error', 'unix'], quotes: ['off'], semi: ['error', 'always'], 'no-var': ['error'], 'brace-style': ['error'], 'array-bracket-spacing': ['error', 'never'], 'block-spacing': ['error', 'always'], 'no-spaced-func': ['error'], 'no-whitespace-before-property': ['error'], 'space-before-blocks': ['error', 'always'], 'keyword-spacing': ['error'], // We use Typescript for this 'react/prop-types': ['off'], }, settings: { react: { version: '16.6', }, }, }; ================================================ FILE: .flowconfig ================================================ [ignore] .*/node_modules/.*/\(test\|lib\|example\|samplejson\)/.*\.json /dist/ [include] [libs] [options] ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/workflows/node.js.yml ================================================ name: Node.js CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci --force - run: npm test ================================================ FILE: .gitignore ================================================ *~ .DS_Store /node_modules npm-debug.log /dist /example/dist /.cache ================================================ FILE: .prettierignore ================================================ /example/dist /dist .cache ================================================ FILE: .prettierrc ================================================ { "singleQuote": true } ================================================ FILE: .vscode/settings.json ================================================ { "javascript.validate.enable": false, "flow.useNPMPackagedFlow": true, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ] } ================================================ FILE: CHANGELOG.md ================================================ ## 4.2.1 (2024-05-23) - Reupload package to work around minor file metadata issue caused by Yarn (https://github.com/yarnpkg/yarn/issues/8109). This version contains no other changes. ## 4.2.0 (2024-02-19) - Added `draggedItem` parameter to the `onDragStart` and `onDragEnd` callback props. ## 4.1.0 (2022-08-12) - Implemented `onDragStart` and `onDragEnd` props. ([@JayHales](https://github.com/JayHales) in [#52](https://github.com/StreakYC/react-draggable-list/pull/52)) ## 4.0.4 (2021-09-22) - Updated peerDependencies list to mark compatibility with React 17. ## 4.0.3 (2019-06-05) - Updated to use immutability-helper 3.0. ## 4.0.2 (2019-04-05) - Fixed issue where the DraggableList could have an incorrect height during drag depending on the styling of its parents. ## 4.0.0 (2018-11-28) ### Breaking Changes - React v16.6+ is now required. - The `dragHandle` function prop was removed. Now the Template component is instead given a prop `dragHandleProps` which is an object that must be spread as props on the HTML element to be used as the drag handle. ReactDraggableList v3: ```js
{this.props.dragHandle(
drag me
)}
content
``` ReactDraggableList v4: ```js
drag me
content
``` ### Improvements - No longer uses any deprecated APIs (lifecycle methods and ReactDOM.findDOMNode). - Fixed bug where the `oldIndex` parameter passed to `onMoveEnd` was incorrect if the `list` prop was updated while the user was dragging an item. ## 3.7.0 (2018-11-05) - Added TypeScript type definitions. - Changed Flow type definitions to use `$ReadOnlyArray` where applicable. Users may need to change the type annotations on the function they give to the `onMoveEnd` prop to keep Flow's type-check passing. ## 3.6.0 (2018-08-24) - Added `constrainDrag` prop. [#30](https://github.com/StreakYC/react-draggable-list/pull/30) ## 3.5.3 (2018-05-15) - Updated for compatibility with Flow v0.72. ## 3.5.2 (2018-04-13) - Improved Flow type definitions to cover the proper return type of `getItemInstance`. ## 3.5.1 (2018-04-13) - Fixed accidental usage of `event` global variable. This didn't cause any user-visible bugs to my knowledge. ## 3.5.0 (2018-04-13) - Flow types for DraggableList now include a type parameter representing the list item's type, enabling fuller type-checking coverage. ## 3.4.1 (2017-10-02) - Updated package.json to mark compatibility with React v16. - Internal: tests now use React v16. ## 3.4.0 (2017-09-11) - Updated for compatibility with Flow v0.54.1. This made it incompatible with older versions of Flow, so I'm making this update be a semver-minor change so users still on an older Flow version can pin to the previous minor version. Because of its frequent changes, I'm not considering incompatibilities with old Flow versions as semver-major breaking changes. ## 3.3.1 (2017-07-07) - Updated for compatibility with Flow v0.49.1. ## 3.3.0 (2017-04-25) - Stop using the newly deprecated `React.PropTypes` and now use the separate prop-types module. ## 3.2.1 (2017-03-24) - Fixed issue where components didn't re-render when the value of the `commonProps` prop changed. [#20](https://github.com/StreakYC/react-draggable-list/pull/20) ## 3.2.0 (2017-03-06) - Added `autoScrollMaxSpeed` and `autoScrollRegionSize` props to DraggableList. [#15](https://github.com/StreakYC/react-draggable-list/pull/15) - Added `commonProps` prop to DraggableList. [#18](https://github.com/StreakYC/react-draggable-list/pull/18) ## 3.1.4 (2017-03-06) - Updated for compatibility with Flow v0.41. ## 3.1.3 (2017-01-24) - Updated for compatibility with Flow v0.38. ## 3.1.2 (2016-10-26) - Fixed issue with the dragged item itself being removed during the post-drag animation. ## 3.1.1 (2016-10-25) - Fixed handling of items being removed from list during the post-drag animation. ## 3.1.0 (2016-10-25) - Added `getItemInstance` method. ## 3.0.4 (2016-09-26) - Fixed Flow type-checking issue when used with newer version of react-motion. ## 3.0.3 (2016-09-13) - Updated for compatibility with Flow v0.32. ## 3.0.2 (2016-08-05) - Updated for compatibility with Flow v0.30. ## 3.0.1 (2016-06-27) - Fixed handling of props being changed while the user is dragging an item. - Fixed issue where DraggableList could prevent elements from having a natural layout applied after being dragged when the container prop was not used. ## 3.0.0 (2016-04-07) ### Breaking Changes - React v15 is now required. ## 2.1.0 (2016-03-02) - Added `unsetZIndex` prop. ## 2.0.0 (2016-02-29) ### Breaking Changes - If the `getDragHeight` method isn't present on the template component, the drag height now defaults to the element's natural height instead of the arbitrary height of 30px. ## 1.0.6 (2016-02-29) - Fixed the DraggableList element changing height when the last item is grabbed. ## 1.0.5 (2016-02-25) - Re-use dragHandle prop value given to component to reduce amount of re-renders. ## 1.0.3 (2016-02-25) - Fixed a scroll animation glitch when DraggableList was not in a scrollable container. ## 1.0.2 (2016-02-25) - Fixed an accuracy issue with the scroll animation on drop. ## 1.0.1 (2016-02-25) - Fixed animation glitch if you pick up an item while it's still animating. - Efficiency improvements: minimize amount of re-renders needed during dragging. ## 1.0.0 (2016-02-24) Initial stable release. ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2018 Rewardly, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # react-draggable-list [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/StreakYC/react-draggable-list/blob/master/LICENSE.txt) [![npm version](https://img.shields.io/npm/v/react-draggable-list.svg?style=flat)](https://www.npmjs.com/package/react-draggable-list) [![Node.js CI](https://github.com/StreakYC/react-draggable-list/actions/workflows/node.js.yml/badge.svg)](https://github.com/StreakYC/react-draggable-list/actions/workflows/node.js.yml) This component lets you make a user re-orderable list that animates nicely so that the user can easily move large items: ![Example](https://streakyc.github.io/react-draggable-list/video/dragitem.gif) The above example can be tried here: https://streakyc.github.io/react-draggable-list/example/ You can find its code in the `example` directory. The example may be compiled by running: ``` npm i --force npm run example-build # or use this to auto-rebuild on changes: npm run example-watch ``` ## DraggableList This module exports the `DraggableList` React component, which takes the following props: - `list` must be an array of objects representing your list's items. - `itemKey` must be the name of a property of the list's objects to use as a key to identify the objects, or it must be a function that takes an object as an argument and returns a key. - `template` must be a React component used to render the list items. This must not be a stateless-functional component. If possible, don't pass a new class instance on every render. See the next section for more information on the template including a description of the props passed to the component. - `onMoveEnd` may be a function which will be called when the user drags and drops an item to a new position in the list. The arguments to the function will be `(newList: Array, movedItem: Object, oldIndex: number, newIndex: number)`. A component using DraggableList should immediately store the newList into its state and then pass the new list (or an equivalent list) as the `list` prop to DraggableList. - `container`: If the DraggableList is inside a scrollable element, then this property should be set to a function which returns a reference to it. When the user moves an item in the list, the container will be scrolled to keep the item in view. If the DraggableList is in no scrollable elements besides the page itself, then a function returning a reference to `document.body` should be given. - `springConfig` is an optional object which sets the [SpringHelperConfig object passed to React-Motion](https://github.com/chenglou/react-motion/tree/85ca75c6de9ed85937d1c95646b6044a66981eee#--spring-val-number-config-springhelperconfig--opaqueconfig) for animations. This prop defaults to `{stiffness: 300, damping: 50}`. - `padding` is an optional number of pixels to leave between items. Defaults to 10. - `unsetZIndex` is an optional property that defaults to false. If set to true, then the z-index of all of the list items will be set to "auto" when the list isn't animating. This may have a small performance cost when the list starts and stops animating. Use this if you need to avoid having the list item create a stacking context when it's not being animated. - `constrainDrag` is an option property that defaults to false. If it is set to true, then the y-coordinate of a dragged item will be constrained vertically to the bounds of the list. - `autoScrollMaxSpeed` is an optional number that allows the scroll speed when the user drags to the top or bottom of the list to be overridden. - `autoScrollRegionSize` is an optional number that allows the height of the region that triggers auto-scrolling when dragged onto to be overridden. - `commonProps` is an optional value that will be passed as the `commonProps` prop to every template component instance. - `onDragStart` is an optional function which is called once a list item starts being dragged. Receives the dragged item as an argument. - `onDragEnd` is an optional function which is called once a list item is no longer being dragged. Receives the dragged item as an argument. It differs from `onMoveEnd` in that it's called even if the user does not reorder any items in the lists, like when an item is just picked up and then dropped. A DraggableList instance has the following methods: - `getItemInstance(key)` will return a reference to the mounted instance of the template for a given key. ## Template The template component is passed the following props: - `item` is an object from the list prop passed to DraggableList. - `itemSelected` is a number from 0 to 1. It starts at 0, and quickly increases to 1 when the item is picked up by the user. This may be used to animate the item when the user picks it up or drops it. - `anySelected` is a number from 0 to 1. It starts at 0, and quickly increases to 1 when any item is picked up by the user. - `dragHandleProps` is an object which should be spread as props on the HTML element to be used as the drag handle. The whole item will be draggable by the wrapped element. See the [example](https://github.com/StreakYC/react-draggable-list/blob/master/example/Example.tsx) to see how it should be used. - `commonProps` will be set to the same value passed as the `commonProps` prop to the DraggableList component. The template component should be styled with max-height set to "100%" for best results. The template component will have its props updated many times quickly during the animation, so implementing `shouldComponentUpdate` in its children is highly recommended. The template component may have a `getDragHeight` method which may return a number to set the height in pixels of the item while the user is dragging it. If the method returns null or is not present, then the drag height will be equal to the element's natural height. ## Bundling Note To use this module in browsers, a CommonJS bundler such as Parcel, Browserify, or Webpack should be used. This project relies on the javascript Map object being available globally. A global polyfill such as [Babel's polyfill](https://babeljs.io/docs/usage/polyfill/) is required to support [older browsers that don't implement these](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Browser_compatibility). ## Types Both [TypeScript](https://www.typescriptlang.org/) and [Flow](https://flowtype.org/) type definitions for this module are included! The type definitions won't require any configuration to use. ================================================ FILE: example/Example.tsx ================================================ /* eslint-disable no-console */ import * as React from 'react'; import cx from 'classnames'; import DraggableList from '../src'; interface PlanetListItem { name: string; subtitle?: boolean; } interface PlanetProps { item: PlanetListItem; itemSelected: number; dragHandleProps: object; } interface PlanetState { value: number; } class PlanetItem extends React.Component { state = { value: 0, }; _inc() { this.setState({ value: this.state.value + 1, }); } getDragHeight() { return this.props.item.subtitle ? 47 : 28; } render() { const { item, itemSelected, dragHandleProps } = this.props; const { value } = this.state; const scale = itemSelected * 0.05 + 1; const shadow = itemSelected * 15 + 1; const dragged = itemSelected !== 0; return (

{item.name}

{item.subtitle && (
This item has a subtitle visible while dragging
)}
some description here
this planet orbits the sun
this planet is mostly round
{item.subtitle && (
subtitled planets are better
and have longer descriptions
)}
State works and is retained during movement:{' '} this._inc()} />
); } } type ExampleState = { useContainer: boolean; list: ReadonlyArray; }; export default class Example extends React.Component<{}, ExampleState> { private _container = React.createRef(); state = { useContainer: false, list: [ { name: 'Mercury' }, { name: 'Venus' }, { name: 'Earth', subtitle: true }, { name: 'Mars' }, { name: 'Jupiter' }, { name: 'Saturn', subtitle: true }, { name: 'Uranus', subtitle: true }, { name: 'Neptune' }, ], }; private _togglePluto() { const noPluto = this.state.list.filter((item) => item.name !== 'Pluto'); if (noPluto.length !== this.state.list.length) { this.setState({ list: noPluto }); } else { this.setState({ list: this.state.list.concat([{ name: 'Pluto' }]) }); } } private _toggleContainer() { this.setState({ useContainer: !this.state.useContainer }); } private _onListChange(newList: ReadonlyArray) { this.setState({ list: newList }); } render() { const { useContainer } = this.state; return (

This is a demonstration of the{' '} react-draggable-list {' '} library.

Each item has a drag handle visible when the user hovers over them. The items may have any height, and can each define their own height to use while being dragged.

When the list is reordered, the page will be scrolled if possible to keep the moved item visible and on the same part of the screen.

this._togglePluto()} /> this._toggleContainer()} />
itemKey="name" template={PlanetItem} list={this.state.list} onMoveEnd={(newList) => this._onListChange(newList)} container={() => useContainer ? this._container.current! : document.body } />
Footer here.
); } } ================================================ FILE: example/index.html ================================================ React Draggable List Test Page
================================================ FILE: example/main.css ================================================ .intro, footer { width: 400px; margin: auto; } .list { width: 430px; margin: 10px auto; padding: 15px; } .item { border: 1px solid black; overflow: hidden; transform-origin: 30% 50% 0px; padding-left: 20px; background: white; max-height: 100%; } .item .dragHandle { visibility: hidden; position: absolute; top: 0; left: 0; cursor: move; width: 20px; height: 16px; background: url('grippy.png') 30% 50% no-repeat no-repeat; } .item:hover .dragHandle, .item.dragged .dragHandle { visibility: visible; } .item h2 { margin: 0; } .item .subtitle { font-weight: bold; } ::-webkit-scrollbar { -webkit-appearance: none; width: 7px; } ::-webkit-scrollbar-thumb { border-radius: 4px; background-color: rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); } ================================================ FILE: example/main.tsx ================================================ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import Example from './Example'; const onReady = new Promise((resolve) => { if (document.readyState === 'complete') { resolve(undefined); } else { document.addEventListener('DOMContentLoaded', resolve, false); window.addEventListener('load', resolve, false); } }); function main() { const mainDiv = document.getElementById('main'); if (!mainDiv) throw new Error(); ReactDOM.render(, mainDiv); } onReady.then(main).catch((e) => { console.error(e, e.stack); // eslint-disable-line no-console }); ================================================ FILE: index.js.flow ================================================ /* @flow */ import * as React from 'react'; export type TemplateProps = { item: I, itemSelected: number, anySelected: number, dragHandleProps: Object, commonProps: C, }; export type Props = { itemKey: string | ((item: I) => string), template: Class, list: $ReadOnlyArray, onMoveEnd?: ?( newList: $ReadOnlyArray, movedItem: I, oldIndex: number, newIndex: number ) => void, container?: ?() => ?HTMLElement, constrainDrag: boolean, springConfig: Object, padding: number, unsetZIndex: boolean, autoScrollMaxSpeed: number, autoScrollRegionSize: number, commonProps?: C, }; declare export default class DraggableList< I, C = *, T: React.Component<$Shape>, *> = * > extends React.Component> { getItemInstance(key: string): T; } ================================================ FILE: jest.config.js ================================================ module.exports = { testEnvironment: 'jsdom', modulePathIgnorePatterns: ['/dist/'] }; ================================================ FILE: package.json ================================================ { "name": "react-draggable-list", "version": "4.2.1", "description": "Make a list of draggable collapsible items.", "main": "dist/src/index.js", "sideEffects": false, "scripts": { "prepare": "rimraf dist && babel -s true -d dist/src/ src/ -x .ts,.tsx --ignore '**/*.test.tsx' && tsc", "example-build": "esbuild example/main.tsx --bundle --outdir=example/dist --public-path=.", "example-watch": "npm example-build --watch", "test": "npm run lint && flow check && jest && tsc --noEmit", "lint": "eslint . --ext js,jsx,ts,tsx", "lint-fix": "eslint . --ext js,jsx,ts,tsx --fix" }, "repository": { "type": "git", "url": "git+https://github.com/StreakYC/react-draggable-list.git" }, "keywords": [ "react", "react-component", "animation", "reorder", "move", "drag-and-drop", "draggable" ], "files": [ "dist", "index.js.flow" ], "author": "Chris Cowan ", "license": "MIT", "bugs": { "url": "https://github.com/StreakYC/react-draggable-list/issues" }, "homepage": "https://github.com/StreakYC/react-draggable-list#readme", "devDependencies": { "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", "@babel/plugin-transform-runtime": "^7.0.0", "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.6.0", "@types/classnames": "^2.2.9", "@types/jest": "^29.5.12", "@types/react": "^17.0.24", "@types/react-dom": "^17.0.9", "@types/react-motion": "^0.0.33", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "babel-jest": "^29.7.0", "babel-plugin-add-module-exports": "^1.0.0", "classnames": "^2.2.3", "esbuild": "^0.19.10", "eslint": "^8.21.0", "eslint-plugin-react": "^7.4.0", "flow-bin": "^0.184.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "pdelay": "^2.0.0", "prettier": "~2.4.1", "react": "^17.0.2", "react-dom": "^17.0.2", "rimraf": "^5.0.5", "typescript": "^5.8.3" }, "resolutions": { "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0" }, "dependencies": { "@babel/runtime": "^7.0.0", "@types/prop-types": "^15.7.3", "immutability-helper": "^3.0.0", "prop-types": "^15.6.0", "react-motion": "^0.5.2", "react-multi-ref": "^1.0.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0" } } ================================================ FILE: src/MoveContainer.tsx ================================================ import * as React from 'react'; import TemplateContainer from './TemplateContainer'; export interface HeightData { natural: number; drag: number; } interface Props { item: I; template: new (props: any, context?: any) => T; padding: number; y: number | undefined; itemSelected: number; anySelected: number; height: HeightData; zIndex: React.CSSProperties['zIndex']; makeDragHandleProps: (getY: () => number | undefined) => object; commonProps: C; } export default class MoveContainer< I, C, T extends React.Component > extends React.Component> { private readonly _templateContainer = React.createRef>(); private readonly _el = React.createRef(); getDOMNode(): HTMLElement { return this._el.current!; } getTemplate(): T { return this._templateContainer.current!.getTemplate(); } shouldComponentUpdate(nextProps: Props): boolean { return ( this.props.anySelected !== nextProps.anySelected || this.props.itemSelected !== nextProps.itemSelected || this.props.item !== nextProps.item || this.props.template !== nextProps.template || this.props.y !== nextProps.y || this.props.height !== nextProps.height || this.props.zIndex !== nextProps.zIndex || this.props.commonProps !== nextProps.commonProps ); } private _dragHandleProps = this.props.makeDragHandleProps(() => this.props.y); render() { const { item, y, padding, itemSelected, anySelected, height, zIndex, template, commonProps, } = this.props; return (
); } } ================================================ FILE: src/OnUpdate.tsx ================================================ import * as React from 'react'; interface Props { cb: () => void; } export default class OnUpdate extends React.Component { public componentDidUpdate() { this.props.cb(); } public render() { return null; } } ================================================ FILE: src/TemplateContainer.tsx ================================================ import * as React from 'react'; interface Props { item: I; template: new (props: any, context?: any) => T; itemSelected: number; anySelected: number; dragHandleProps: object; commonProps: C; } export default class TemplateContainer< I, C, T extends React.Component > extends React.Component> { private _template: T | undefined; private readonly _templateSetter = (cmp: any) => { this._template = cmp; }; public shouldComponentUpdate(nextProps: Props): boolean { return ( this.props.anySelected !== nextProps.anySelected || this.props.itemSelected !== nextProps.itemSelected || this.props.item !== nextProps.item || this.props.template !== nextProps.template || this.props.commonProps !== nextProps.commonProps ); } public getTemplate(): T { return this._template!; } public render() { const { item, itemSelected, anySelected, dragHandleProps, commonProps } = this.props; const Template = this.props.template; return (