Last active
May 20, 2016 19:30
-
-
Save tomprogers/971e7512f5705a45e7c7c71297cdc28f to your computer and use it in GitHub Desktop.
Example code showing how to work with react-native's ListView, with sticky headers and lots of explanatory comments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTE: I'm using a few ES6 features, like Arrow Functions and default argument values | |
// Hopefully they don't trip you up while reading this. | |
import React, { Component, ListView } from 'react-native'; | |
import Moment from 'moment'; // only needed for my example | |
// I reuse this configuration everywhere. As a rule, each component creates just one of them, | |
// and since changing the dataset doesn't require mutating this object, I define it as a const. | |
// | |
// Note: the getSectionHeaderData & getRowData method implementations are informed by and | |
// dependant upon my invented blob structure, which is constructed | |
// in arrayToListViewDataSourceBlob | |
const LVDS = new ListView.DataSource({ | |
sectionHeaderHasChanged: (h1, h2) => h1 !== h2, | |
rowHasChanged: (r1, r2) => r1 !== r2, | |
// ListView.renderSectionHeader is invoked with (sectionData, sectionId) | |
// the value of sectionData arg is determined by this method, which is invoked beforehand, | |
// and must be able to find the appropriate data within the blob, using only sectionId as | |
// a guide | |
getSectionHeaderData: (blob, sectionId) => blob.sections[ String(sectionId) ], | |
// ListView.renderRow is invoked with (rowData, sectionId, rowId) | |
// the value of rowData arg is determined by this method, which is invoked beforehand, | |
// and which must be able to find the appropriate data within the blob, using only | |
// sectionId and rowId as a guide | |
// I choose to ensure that every row has an ID that is unique across the entire dataset, | |
// and do the lookup based on that, but you are free to create a nested structure of | |
// arbitrary complexity that requires both IDs for lookup (and spend the rest of your | |
// career debugging it ;) | |
getRowData: (blob, sectionId, rowId) => blob.rows[ String(rowId) ] | |
}); | |
/** | |
* arrayToListViewDataSourceBlob is the most complicated part. | |
* It must generate a "blob" (POJO organized however you like), but the header & row | |
* render methods will not have access to any other data at render time, so any data | |
* you need for rendering purposes must be stored somewhere in the blob, and must | |
* be findable by the methods above. | |
* | |
* In general, I find myself with an array of hashes or objects that each describe a | |
* business object (like a User or a Post), and I wish to present a list of those | |
* objects, sometimes grouped by some common property. My need is to transform that list | |
* into whatever weird thing the react-native ListView requires. This function accomplishes that, | |
* and I re-use it everywhere (with case-specific tweaks). | |
*/ | |
function arrayToListViewDataSourceBlob(list=[]) { | |
return [].concat(list).reduce((blob, entry, i) => { | |
// figure out which section this entry belongs in by examining the entry itself | |
// entries whose sectionIds are the same will appear within the same section | |
// in this example, I'm grouping entries by date | |
// sectionIds must be valid property names (I forcibly cast them as Strings during lookup) | |
// this value is presented to both ListView.renderSectionHeader and ListView.renderRow | |
let sectionId = moment(entry.timestamp).format('YYYY-MM-DD'); | |
// must be unique within a section, and is provided to the lookup methods | |
// my simplified structure requires that all rowIds be unique within the dataset | |
let rowId = entry.id; | |
// if this sectionId isn't on the list yet, add it and recalculate sectionIndex | |
// also, create an empty rowlist for this new section | |
let sectionIndex = blob.sectionIds.indexOf(sectionId); | |
if(sectionIndex === -1) { | |
sectionIndex = blob.sectionIds.push(sectionId) - 1; | |
blob.rowIdsBySection[sectionIndex] = []; | |
} | |
// append this entry's rowId to the section's rowlist | |
blob.rowIdsBySection[sectionIndex].push(rowId); | |
// store section-header data for this section in blob.sections, keyed by sectionId | |
blob.sections[sectionId] = { | |
// assuming rows were sorted past-to-future, this will provide the precise timestamp of each day's | |
// earliest item as sectionData.timestamp | |
timestamp: entry.timestamp | |
}; | |
// add entry to blob.rows | |
blob.rows[rowId] = entry; | |
return blob; | |
}, { | |
// this is what BLOB looks like: | |
sections: {}, // will hold sectionData for each section header, keyed by sectionId | |
rows: {}, // will hold rowData for each row, keyed by rowId | |
sectionIds: [], // will be a list of all unique sectionIds, in desired display order | |
rowIdsBySection: [] // [ section0Idx: arrsection0RowIds, section1Idx: arrSection1RowIds, ... ] | |
}); | |
} | |
// Using a ListView in a component: | |
// | |
// Every time the dataset changes (which, in a react app, OUGHT to be construtor & propChange) | |
// you need to re-generate the blob, and then "clone" a new ListView.DataSource using that blob as | |
// an input. I store the blob in the comp's state, because that seems appropriate. | |
class MyComp extends Component { | |
constructor(props) { | |
super(props); | |
// generate a blob to feed into ListView based on mount-time props | |
let blob = arrayToListViewDataSourceBlob(props.listOfStuff); | |
// do the cloning, and store the result so that the ListView can access it at render time. | |
// although the last two args are not provided to the render or lookup methods; their | |
// generation is heavily intertwined with the generation of the blob itself, which is why | |
// arrayToListViewDataSourceBlob calculates & includes them as part of its return value | |
this.state = { | |
lvdsEntries: LVDS.cloneWithRowsAndSections(blob, blob.sectionIds, blob.rowIdsBySection) | |
}; | |
} | |
componentWillReceiveProps(nextProps) { | |
// you really only want to mess with the ListView's dataset if the new set of props | |
// includes a change to that prop | |
if(nextProps.hasOwnProperty('listOfStuff')) { | |
// we could probably diff the old & new listOfStuff to see if there even was a change | |
// ... meh (i.e. exercise for the reader) | |
let blob = arrayToListViewDataSourceBlob(nextProps.listOfStuff); | |
this.setState({ | |
// this is how you send new data to the ListView | |
lvdsEntries: LVDS.cloneWithRowsAndSections(blob, blob.sectionIds, blob.rowIdsBySection) | |
}); | |
} | |
} | |
render() { | |
return ( | |
<ListView | |
dataSource={this.state.lvdsEntries} | |
renderSectionHeader={(sectionData, sectionId) => { | |
// return the component you wish to display for the section header | |
} | |
renderRow={(entry, sectionId, rowId) => { | |
// return the component you wish to display for this row | |
} | |
/> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment