Skip to content

Instantly share code, notes, and snippets.

@tomprogers
Last active May 20, 2016 19:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomprogers/971e7512f5705a45e7c7c71297cdc28f to your computer and use it in GitHub Desktop.
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
// 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