├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── .expo-shared │ └── assets.json ├── .gitignore ├── .watchmanconfig ├── App.js ├── README.md ├── app.json ├── assets │ ├── icon.png │ └── splash.png ├── babel.config.js ├── package.json ├── rn-cli.config.js ├── screenshots │ ├── listview-cropped.png │ └── listview.png ├── src │ ├── GenericListExample.js │ ├── ImmutableListViewExample.js │ ├── ImmutableVirtualizedListExample.js │ ├── mockData.js │ ├── styles.js │ └── utils.js └── yarn.lock ├── images └── cover.png ├── index.d.ts ├── package.json ├── scripts └── setup-jest.js ├── src ├── ImmutableListView │ ├── EmptyListView.js │ ├── ImmutableListView.js │ ├── __tests__ │ │ ├── EmptyListView.test.js │ │ ├── ImmutableListView.test.js │ │ └── __snapshots__ │ │ │ ├── EmptyListView.test.js.snap │ │ │ └── ImmutableListView.test.js.snap │ └── index.js ├── ImmutableVirtualizedList │ ├── EmptyVirtualizedList.js │ ├── ImmutableVirtualizedList.js │ ├── __tests__ │ │ ├── EmptyVirtualizedList.test.js │ │ ├── ImmutableVirtualizedList.test.js │ │ └── __snapshots__ │ │ │ ├── EmptyVirtualizedList.test.js.snap │ │ │ └── ImmutableVirtualizedList.test.js.snap │ └── index.js ├── __tests__ │ ├── comparison.test.js │ └── utils.test.js ├── index.js ├── styles.js ├── test-utils.js └── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "module:metro-react-native-babel-preset" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-class-properties" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | android 4 | ios 5 | 6 | lib 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "cooperka/react-native", 3 | 4 | "parser": "babel-eslint", 5 | 6 | "env": { 7 | "jest": true 8 | }, 9 | 10 | "rules": { 11 | "react-native/no-color-literals": "off" 12 | }, 13 | 14 | "globals": { 15 | "__DEV__": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | 3 | node_modules 4 | .idea 5 | 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/__tests__/ 2 | /scripts/ 3 | src/test-utils.js 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | - "12" 6 | 7 | cache: 8 | yarn: true 9 | directories: 10 | - node_modules 11 | - jest-cache 12 | 13 | script: 14 | - yarn run lint 15 | - yarn test -- --cacheDirectory="jest-cache" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kevin Cooper 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 | # React Native Immutable ListView 2 | 3 | [![Build status](https://travis-ci.org/cooperka/react-native-immutable-list-view.svg?branch=master)](https://travis-ci.org/cooperka/react-native-immutable-list-view) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-native-immutable-list-view.svg)](https://www.npmjs.com/package/react-native-immutable-list-view) 5 | [![npm version](https://img.shields.io/npm/v/react-native-immutable-list-view.svg)](https://www.npmjs.com/package/react-native-immutable-list-view) 6 | [![Latest GitHub tag](https://img.shields.io/github/tag/cooperka/react-native-immutable-list-view.svg)](https://github.com/cooperka/react-native-immutable-list-view) 7 | 8 |
9 | 10 | [![Logo](images/cover.png)](#README) 11 | 12 |
13 | 14 | Drop-in replacement for React Native's [`ListView`](https://facebook.github.io/react-native/docs/listview.html), 15 | [`FlatList`](https://facebook.github.io/react-native/docs/flatlist.html), 16 | and [`VirtualizedList`](https://facebook.github.io/react-native/docs/virtualizedlist.html). 17 | 18 | ![ImmutableListView screenshot](example/screenshots/listview-cropped.png "ImmutableListView screenshot") 19 | 20 | It supports [Immutable.js](https://facebook.github.io/immutable-js/) to give you faster performance and less headaches. 21 | 22 | ## Motivation 23 | 24 | - Do you use Immutable data, only to write the same boilerplate over and over in order to display it? 25 | - Do you want to show 'Loading...', 'No results', and 'Error!' states in your lists? 26 | - Do you have nested objects in your state so a shallow diff won't cut it for pure rendering? 27 | - Do you want better performance while animating screen transitions? 28 | 29 | If you answered yes to ANY of these questions, this project can help. Check out the examples below. 30 | 31 | ## How it works 32 | 33 | For FlatList and VirtualizedList: 34 | 35 | ```jsx 36 | 40 | ``` 41 | 42 | For ListView (deprecated as of React Native v0.59): 43 | 44 | ```jsx 45 | 49 | ``` 50 | 51 | The screenshot above shows two different lists. The first uses this data: 52 | 53 | ```js 54 | Immutable.fromJS({ 55 | 'Section A': [ 56 | 'foo', 57 | 'bar', 58 | ], 59 | 'Section B': [ 60 | 'fizz', 61 | 'buzz', 62 | ], 63 | }) 64 | ``` 65 | 66 | The second list is even simpler: 67 | 68 | ```js 69 | Immutable.Range(1, 100) 70 | ``` 71 | 72 | There's an example app [here](https://github.com/cooperka/react-native-immutable-list-view/tree/master/example) 73 | if you'd like to see it in action. 74 | 75 | ## Installation 76 | 77 | 1. Install: 78 | - Using [npm](https://www.npmjs.com/#getting-started): `npm install react-native-immutable-list-view --save` 79 | - Using [Yarn](https://yarnpkg.com/): `yarn add react-native-immutable-list-view` 80 | 81 | 2. Import it in your JS: 82 | 83 | For FlatList and VirtualizedList: 84 | 85 | ```js 86 | import { ImmutableVirtualizedList } from 'react-native-immutable-list-view'; 87 | ``` 88 | 89 | For ListView: 90 | 91 | ```js 92 | import { ImmutableListView } from 'react-native-immutable-list-view/lib/ImmutableListView'; 93 | ``` 94 | 95 | ## Example usage -- replacing FlatList 96 | 97 | Goodbye, `keyExtractor` boilerplate! 98 | 99 | > Note: This example diff looks much better on [GitHub](https://github.com/cooperka/react-native-immutable-list-view#example-usage----replacing-flatlist) than on npm's site. 100 | > Red means delete, green means add. 101 | 102 | ```diff 103 | -import { Text, View, FlatList } from 'react-native'; 104 | +import { Text, View } from 'react-native'; 105 | +import { ImmutableVirtualizedList } from 'react-native-immutable-list-view'; 106 | 107 | import style from './styles'; 108 | import listData from './listData'; 109 | 110 | class App extends Component { 111 | 112 | renderItem({ item, index }) { 113 | return {item}; 114 | } 115 | 116 | render() { 117 | return ( 118 | 119 | 120 | Welcome to React Native! 121 | 122 | - items.get(index)} 125 | - getItemCount={(items) => items.size} 126 | - keyExtractor={(item, index) => String(index)} 127 | + 131 | 132 | ); 133 | } 134 | 135 | } 136 | ``` 137 | 138 | ## Example usage -- replacing ListView 139 | 140 | You can remove all that boilerplate in your constructor, as well as lifecycle methods like 141 | `componentWillReceiveProps` if all they're doing is updating your `dataSource`. 142 | `ImmutableListView` will handle all of this for you. 143 | 144 | > Note: This example diff looks much better on [GitHub](https://github.com/cooperka/react-native-immutable-list-view#example-usage----replacing-listview) than on npm's site. 145 | > Red means delete, green means add. 146 | 147 | ```diff 148 | -import { Text, View, ListView } from 'react-native'; 149 | +import { Text, View } from 'react-native'; 150 | +import { ImmutableListView } from 'react-native-immutable-list-view/lib/ImmutableListView'; 151 | 152 | import style from './styles'; 153 | import listData from './listData'; 154 | 155 | class App extends Component { 156 | 157 | - constructor(props) { 158 | - super(props); 159 | - 160 | - const dataSource = new ListView.DataSource({ 161 | - rowHasChanged: (r1, r2) => r1 !== r2, 162 | - sectionHeaderHasChanged: (s1, s2) => s1 !== s2, 163 | - }); 164 | - 165 | - const mutableData = listData.toJS(); 166 | - 167 | - this.state = { 168 | - dataSource: dataSource.cloneWithRowsAndSections(mutableData), 169 | - }; 170 | - } 171 | - 172 | - componentWillReceiveProps(newProps) { 173 | - this.setState({ 174 | - dataSource: this.state.dataSource.cloneWithRows(newProps.listData), 175 | - }); 176 | - } 177 | - 178 | renderRow(rowData) { 179 | return {rowData}; 180 | } 181 | 182 | renderSectionHeader(sectionData, category) { 183 | return {category}; 184 | } 185 | 186 | render() { 187 | return ( 188 | 189 | 190 | Welcome to React Native! 191 | 192 | - 199 | 200 | ); 201 | } 202 | 203 | } 204 | ``` 205 | 206 | ## Customization 207 | 208 | All the props supported by React Native's underlying List are simply passed through, and should work exactly the same. 209 | You can see all the [VirtualizedList props](https://facebook.github.io/react-native/docs/virtualizedlist.html#props) 210 | or [ListView props](https://facebook.github.io/react-native/docs/listview.html#props) on React Native's website. 211 | 212 | You can customize the look of your list by implementing [`renderItem`](https://facebook.github.io/react-native/docs/flatlist.html#renderitem) for FlatList and VirtualizedList 213 | or [`renderRow`](https://facebook.github.io/react-native/docs/listview.html#renderrow) for ListView. 214 | 215 | Here are the additional props that `ImmutableVirtualizedList` and `ImmutableListView` accept: 216 | 217 | | Prop name | Data type | Default value? | Description | 218 | |-----------|-----------|----------------|-------------| 219 | | `immutableData` | Any [`Immutable.Iterable`](https://facebook.github.io/immutable-js/docs/#/Iterable/isIterable) | Required. | The data to render. See below for some examples. | 220 | | `rowsDuringInteraction` | `number` | `undefined` | How many rows of data to initially display while waiting for interactions to finish (e.g. Navigation animations). | 221 | | `sectionHeaderHasChanged` | `func` | `(prevSectionData, nextSectionData) => false` | Only needed if your section header is dependent on your row data (uncommon; see [`ListViewDataSource`'s constructor](https://facebook.github.io/react-native/docs/listviewdatasource.html#constructor) for details). | 222 | | `renderEmpty` | `string` or `func` | `undefined` | If your data is empty (e.g. `null`, `[]`, `{}`) and this prop is defined, then this will be rendered instead. Pull-refresh and scrolling functionality will be **lost**. Most of the time you should use `renderEmptyInList` instead. | 223 | | `renderEmptyInList` | `string` or `func` | `'No data.'` | If your data is empty (e.g. `null`, `[]`, `{}`) and this prop is defined, then this will be rendered instead. Pull-refresh and scrolling functionality will be **kept**! See [below](#loading--empty--error-states) for more details. | 224 | 225 | Also see [React Native's `FlatListExample`](https://github.com/facebook/react-native/blob/master/RNTester/js/FlatListExample.js) 226 | for more inspiration. 227 | 228 | ## Methods 229 | 230 | Methods such as `scrollToEnd` are passed through just like the props described above. 231 | You can read about them [here](https://facebook.github.io/react-native/docs/listview.html#methods) for ListView 232 | or [here](https://facebook.github.io/react-native/docs/virtualizedlist.html#methods) for FlatList and VirtualizedList. 233 | 234 | The references to the raw `VirtualizedList` or `ListView` component are available via `getVirtualizedList()` or `getListView()`. 235 | These references allow you to access any other methods on the underlying List that you might need. 236 | 237 | ## How to format your data 238 | 239 | `ImmutableListView` accepts several [standard formats](https://facebook.github.io/react-native/releases/0.37/docs/listviewdatasource.html#constructor) 240 | for list data. Here are some examples: 241 | 242 | #### List 243 | 244 | ```js 245 | [rowData1, rowData2, ...] 246 | ``` 247 | 248 | #### Map of Lists 249 | 250 | ```js 251 | { 252 | section1: [ 253 | rowData1, 254 | rowData2, 255 | ... 256 | ], 257 | ... 258 | } 259 | ``` 260 | 261 | #### Map of Maps 262 | 263 | ```js 264 | { 265 | section1: { 266 | rowId1: rowData1, 267 | rowId2: rowData2, 268 | ... 269 | }, 270 | ... 271 | } 272 | ``` 273 | 274 | To try it out yourself, you can use the [example app](https://github.com/cooperka/react-native-immutable-list-view/tree/master/example)! 275 | 276 | Support is coming soon for section headers with `ImmutableVirtualizedList` too, similar to [`SectionList`](https://facebook.github.io/react-native/docs/sectionlist.html). 277 | See [PR #34](https://github.com/cooperka/react-native-immutable-list-view/pull/34). 278 | 279 | ## Loading / Empty / Error states 280 | 281 | The optional `renderEmptyInList` prop takes a string and renders an Immutable List displaying the text you specified. 282 | By default, this text is simply `No data.`, but you can customize this based on your state. For example: 283 | 284 | ```jsx 285 | render() { 286 | const emptyText = this.state.isLoading 287 | ? "Loading..." 288 | : this.state.errorMsg 289 | ? "Error!" 290 | : "No data."; 291 | 292 | return ( 293 | 298 | ); 299 | } 300 | ``` 301 | 302 | The empty list will receive all the same props as your normal list, so things like pull-to-refresh will still work. 303 | -------------------------------------------------------------------------------- /example/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | yarn-error.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | web-report/ 13 | .idea/* 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | // Choose one: 2 | // import Example from './src/ImmutableListViewExample'; 3 | import Example from './src/ImmutableVirtualizedListExample'; 4 | 5 | export default Example; 6 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # ImmutableListView Examples 2 | 3 | ## VirtualizedList 4 | 5 | By default, the example app uses an `ImmutableListView`. If you want to try out the new `ImmutableVirtualizedList`, 6 | you can simply uncomment the line in `index.android.js` or `index.ios.js` and it will use that component instead. 7 | 8 | ## Usage 9 | 10 | 1. `(cd .. && yarn install)` to install in the parent directory 11 | 2. `yarn install` here 12 | 3. `react-native run-android` or `react-native run-ios` 13 | 14 | You can also use `npm` instead of `yarn` if you prefer. 15 | 16 | ## Screenshot 17 | 18 | ![ImmutableListView screenshot](screenshots/listview.png) 19 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Immutable List View Example", 4 | "slug": "example", 5 | "privacy": "public", 6 | "sdkVersion": "35.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^35.0.0", 12 | "immutable": "4.0.0-rc.12", 13 | "prop-types": "15.7.2", 14 | "react": "16.8.3", 15 | "react-dom": "16.8.3", 16 | "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz", 17 | "react-native-immutable-list-view": "file:..", 18 | "react-native-web": "^0.11.7" 19 | }, 20 | "devDependencies": { 21 | "babel-preset-expo": "^7.0.0" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /example/rn-cli.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const blacklist = require('metro-config/src/defaults/blacklist'); 3 | 4 | module.exports = { 5 | resolver: { 6 | blacklistRE: blacklist([ 7 | /node_modules\/.*\/node_modules\/react-native\/.*/, 8 | ]), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /example/screenshots/listview-cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/screenshots/listview-cropped.png -------------------------------------------------------------------------------- /example/screenshots/listview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/screenshots/listview.png -------------------------------------------------------------------------------- /example/src/GenericListExample.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import PropTypes from 'prop-types'; 3 | import React, { Component } from 'react'; 4 | import { Text, View, Button, RefreshControl } from 'react-native'; 5 | 6 | import style from './styles'; 7 | 8 | const EMPTY_LIST = Immutable.List(); 9 | const MOCK_DELAY = 800; 10 | 11 | class GenericListExample extends Component { 12 | defaultStateA = { 13 | data: undefined, // Will be manually set on mount. 14 | isLoading: false, 15 | errorMsg: undefined, 16 | }; 17 | 18 | defaultStateB = { 19 | data: undefined, // Will be manually set on mount. 20 | isLoading: false, 21 | errorMsg: undefined, 22 | }; 23 | 24 | static propTypes = { 25 | ListComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]).isRequired, 26 | listComponentProps: PropTypes.object.isRequired, 27 | 28 | initialDataA: PropTypes.object.isRequired, 29 | dataMutatorA: PropTypes.func.isRequired, 30 | extraPropsA: PropTypes.object, 31 | 32 | initialDataB: PropTypes.object.isRequired, 33 | dataMutatorB: PropTypes.func.isRequired, 34 | extraPropsB: PropTypes.object, 35 | }; 36 | 37 | componentWillMount() { 38 | const { initialDataA, initialDataB } = this.props; 39 | 40 | this.defaultStateA.data = initialDataA; 41 | this.defaultStateB.data = initialDataB; 42 | 43 | this.setState({ 44 | listA: { 45 | ...this.defaultStateA, 46 | }, 47 | listB: { 48 | ...this.defaultStateB, 49 | }, 50 | }); 51 | } 52 | 53 | changeDataA(delay = 0) { 54 | const { listA } = this.state; 55 | const { dataMutatorA } = this.props; 56 | 57 | if (delay) { 58 | this.setState({ 59 | listA: { 60 | ...listA, 61 | isLoading: true, 62 | }, 63 | }); 64 | } 65 | 66 | setTimeout(() => { 67 | this.setState({ 68 | listA: { 69 | ...listA, 70 | data: dataMutatorA(listA.data), 71 | isLoading: false, 72 | errorMsg: undefined, 73 | }, 74 | }); 75 | }, delay); 76 | } 77 | 78 | changeDataB(delay = 0) { 79 | const { listB } = this.state; 80 | const { dataMutatorB } = this.props; 81 | 82 | if (delay) { 83 | this.setState({ 84 | listB: { 85 | ...listB, 86 | isLoading: true, 87 | }, 88 | }); 89 | } 90 | 91 | setTimeout(() => { 92 | this.setState({ 93 | listB: { 94 | ...listB, 95 | data: dataMutatorB(listB.data), 96 | isLoading: false, 97 | errorMsg: undefined, 98 | }, 99 | }); 100 | }, delay); 101 | } 102 | 103 | toggleDefaultState() { 104 | this.setState({ 105 | listA: { 106 | ...this.defaultStateA, 107 | }, 108 | listB: { 109 | ...this.defaultStateB, 110 | }, 111 | }); 112 | } 113 | 114 | toggleLoadingState() { 115 | this.setState({ 116 | listA: { 117 | ...this.defaultStateA, 118 | data: EMPTY_LIST, 119 | isLoading: true, 120 | }, 121 | listB: { 122 | ...this.defaultStateB, 123 | data: EMPTY_LIST, 124 | isLoading: true, 125 | }, 126 | }); 127 | } 128 | 129 | toggleErrorState() { 130 | this.setState({ 131 | listA: { 132 | ...this.defaultStateA, 133 | data: EMPTY_LIST, 134 | errorMsg: 'Error! Fake data A has gone rogue!', 135 | }, 136 | listB: { 137 | ...this.defaultStateB, 138 | data: EMPTY_LIST, 139 | errorMsg: 'Error! Fake data B has gone rogue!', 140 | }, 141 | }); 142 | } 143 | 144 | render() { 145 | const { listA, listB } = this.state; 146 | const { 147 | ListComponent, extraPropsA, extraPropsB, listComponentProps, 148 | } = this.props; 149 | 150 | const emptyTextA = listA.isLoading ? 'Loading...' : listA.errorMsg; 151 | const emptyTextB = listB.isLoading ? 'Loading...' : listB.errorMsg; 152 | 153 | return ( 154 | 155 | 156 | {ListComponent.displayName || ListComponent.name} 157 | 158 | 159 | 160 | State: 161 | 162 |