Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Section headers #34

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

benadamstyles
Copy link

This is just my initial commit – it would be really helpful if @cooperka you could take a glance at this before I continue. I have had to make some possibly controversial decisions, most notably introducing a setState in componentDidUpdate – for which I had to make a change in .eslintrc.

I welcome any comments, now matter how challenging they are!

if (treatAsSections && prevProps.immutableData !== immutableData) {
const flattenedData = utils.flattenMap(immutableData);
this.setState({
flattenedData,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cooperka I chose to hold flattenedData in state so that it is only created once on each update to immutableData, as the flattening transformation will be fairly expensive.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One alternative, to avoid using state at all, would be to use a WeakMap to cache the results of the flattening transformation. As we are working with immutable data this would be easy to reason about.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using state is fine, but if you put it in willUpdate instead of didUpdate I think it will be more performant (that's why the linter flagged this).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose didUpdate because in the react docs for componentWillUpdate() it says "Note that you cannot call this.setState() here; nor should you do anything else (e.g. dispatch a Redux action) that would trigger an update to a React component before componentWillUpdate() returns."

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, componentWillReceiveProps is what you should use here! Not willUpdate.

Copy link
Owner

@cooperka cooperka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great @Leeds-eBooks! Thank you again for the help. I left several comments for whenever you find time to look it over.

if (treatAsSections && prevProps.immutableData !== immutableData) {
const flattenedData = utils.flattenMap(immutableData);
this.setState({
flattenedData,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using state is fine, but if you put it in willUpdate instead of didUpdate I think it will be more performant (that's why the linter flagged this).

.eslintrc Outdated
@@ -8,7 +8,8 @@
},

"rules": {
"react-native/no-color-literals": "off"
"react-native/no-color-literals": "off",
"react/no-did-update-set-state": "off"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below for how to avoid this. And just a general tip: it's preferable to type // eslint-disable-next-line react/no-did-update-set-state above your code instead of setting a global rule 👍

* Whether to treat immutableData as a sectioned list,
* applying section headers.
*/
treatAsSections: PropTypes.bool,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can assume they want to render sections if renderSectionHeader is truthy, so this prop can be removed. ListView does the same thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

* to be rendered as a section row. It will be passed the item,
* and the item's key.
*/
renderRow: PropTypes.func,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just leave it as renderItem (i.e. remove this prop)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I think so.

flattenMap(data) {
return data.reduce(
(flattened, section, key) => flattened.set(key, section).merge(section),
Immutable.OrderedMap().asMutable(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand but please clarify -- is asMutable being used here for performance reasons, so we don't have to return a new Immutable object during each reduction step?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly, it's a little trick I learned after months of using immutablejs. Within our flattenMap function, it's safe to make the collection mutable while we build it as long as we call asImmutable() on it before we return it.

src/utils.js Outdated
},

isSectionHeader(item) {
return Immutable.Map.isMap(item) || Immutable.List.isList(item);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are Map and List necessarily section headers? I don't think I understand how this method works.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't totally happy about this. In the flattenMap method, I'm setting the value of the section header items to be their respective section, in other words:

Map {
  section1: Map {
    key1: val1,
    key2: val2
  },
  section2: Map {
    key3: val3,
    key4: val4
  }
}

// gets flattened to:

Map {
  section1: Map {
    key1: val1,
    key2: val2
  },
  key1: val1,
  key2: val2,
  section2: Map {
    key3: val3,
    key4: val4
  },
  key3: val3,
  key4: val4
}

One way, therefore, of identifying whether or not an item is a section header, is by querying if the item's value is a Map or a List. Do you think there's a better way?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you happen to have a link to where the original ListView does something like this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VirtualizedSectionList does it like this: https://github.com/facebook/react-native/blob/master/Libraries/Lists/VirtualizedSectionList.js#L175-L224

For each index, it runs through the entire collection to work out if the index refers to a section header. I think this could be expensive, especially given that we are working with a Map which has keys rather than indexes so we'd have to convert to an indexed collection too.

Actually, I think I can use stickyHeaderIndices, as we have already computed these. Will put this in the next commit!

benadamstyles and others added 2 commits January 31, 2018 18:47
removed isSectionHeader() in favour of querying stickyHeaderIndices
@cooperka
Copy link
Owner

cooperka commented Feb 9, 2018

I made a few minor style tweaks.

I think I might be passing in the wrong data format because I get TypeError: Expected [K, V] tuple at utils.flattenMap at reduce (this happens on both your original branch and after my changes). Any thoughts? Here's my example app diff to try it out:

diff --git a/example/index.js b/example/index.js
index 3fd0fe8..e5abcac 100644
--- a/example/index.js
+++ b/example/index.js
@@ -1,7 +1,7 @@
 import { AppRegistry } from 'react-native';
 
 // Choose one:
-import App from './src/ImmutableListViewExample';
-// import App from './src/ImmutableVirtualizedListExample';
+// import App from './src/ImmutableListViewExample';
+import App from './src/ImmutableVirtualizedListExample';
 
 AppRegistry.registerComponent('ImmutableListViewExample', () => App);
diff --git a/example/src/ImmutableVirtualizedListExample.js b/example/src/ImmutableVirtualizedListExample.js
index 5465c12..e5996c7 100644
--- a/example/src/ImmutableVirtualizedListExample.js
+++ b/example/src/ImmutableVirtualizedListExample.js
@@ -20,18 +20,23 @@ import utils from './utils';
  *
  */
 function ImmutableVirtualizedListExample() {
+  const sectionData = Immutable.fromJS({
+    sec1: [1, 2, 3],
+  });
+
   return (
     <GenericListExample
       ListComponent={ImmutableVirtualizedList}
       listComponentProps={{
         renderItem: utils.renderItem,
+        renderSectionHeader: utils.renderSectionHeader,
         keyExtractor: utils.trivialKeyExtractor,
       }}
 
-      initialDataA={Immutable.List(['Simple', 'List', 'of', 'Items'])}
+      initialDataA={sectionData}
       dataMutatorA={(data) => data.set(3, 'This value was changed!')}
 
-      initialDataB={Immutable.Range(1, 100)}
+      initialDataB={sectionData}
       dataMutatorB={(data) => data.toSeq().map((n) => n * 2)}
     />
   );

@benadamstyles
Copy link
Author

Thanks for making these tweaks. I've been unexpectedly busy this week which is why I haven't had a chance to continue working on this, but it's really high on my to-do list! At the latest, I have 17th Feb free to work on this. Got to add tests, mainly.

@benadamstyles
Copy link
Author

Oh, just seen your question! Will take a look at this asap. It's not obvious to me off the top of my head.

@cooperka
Copy link
Owner

cooperka commented Feb 9, 2018

It's all good, I know how life can be. Tests would be great, and will probably resolve my issue! Either by helping me understand the data format or by catching a bug ;) Take your time!

@benadamstyles
Copy link
Author

Ok it's because my usage of merge in flattenMap only handles a Map with Map rows, rather than List rows. Will work on this next!

@benadamstyles
Copy link
Author

Ok think this is working now!

@cooperka cooperka force-pushed the master branch 6 times, most recently from 255bb12 to efdc474 Compare February 11, 2018 23:14
@benadamstyles
Copy link
Author

Just catching up with this – does anything remain to be done? Does it need more tests do you think?

@cooperka
Copy link
Owner

Hmm, it still doesn't seem to work with the example app using the code diff I pasted above. The list is simply blank (though no errors anymore). It also gets into a strange state when loading and erroring (see screenshot). Do you see the same thing as me?

screenshot from 2018-02-18 10-57-35

@benadamstyles
Copy link
Author

@cooperka Ah yes, I never got round to testing with a live app – just doing that now. Is there a nicer way to make changes in the package and see it updated in the example app than killing the app and running yarn again?

@cooperka
Copy link
Owner

cooperka commented Feb 18, 2018

In an earlier version of RN + yarn you could simply npm install file:.. which took about 5 seconds and it would live update. This no longer works unfortunately.

It might work to yarn add file:.., restart the packager, and simply refresh the app (ctrl+R on ios or R+R on android).

@cooperka
Copy link
Owner

I know there are other automated solutions; if you're familiar with one feel free to recommend it. I've never needed something like that until now since the npm trick used to work.

? renderSectionHeader
: renderItem;

return renderMethod(info.item, this.keyExtractor(info.item, info.index));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok the problem stems from this line. In your example app, you’re (understandably!) assuming that renderItem() will be called with the whole info object (as it is if we are dealing with a flat list without section headers, 3 lines below this comment). Will push a fix asap!

@cooperka
Copy link
Owner

The items display now, but it shows 123 as the section header instead of sec1 as expected. There are also still 4 "Loading" rows. The code in GenericListExample might need to be updated to fix this; I haven't diagnosed any further than just visually.

@benadamstyles
Copy link
Author

Yeah sorry, got to change something in the example app's code. Coming right up 🙂

@benadamstyles
Copy link
Author

Ah I think I remember now why I didn't originally pass the whole info object to renderSectionHeader(): I think I took my cue from the test-utils renderers. In the row renderer, you expect an info object with a item property, but in the sectionHeader renderer, you expect the sectionData directly (i.e. not wrapped in an info object. Should I revert my last commit, or refactor all code throughout the test utils and example app to read the sectionData as being at info.item (as it is for row data)?

@cooperka
Copy link
Owner

cooperka commented Feb 18, 2018

My intuition is that we should use the same data format conventions as the original ListView, but the function arguments should be formatted like VirtualizedList for consistency.

Eventually I'd love to release ImmutableSectionList as a counterpart to SectionList, but that would be more work since it's a very different API with extra features.

My recommendation for this PR (feel free to suggest changes):

// This data:
Immutable.fromJS({
  sec1: [1, 2, 3],
})

// Or, equivalently:
Immutable.fromJS({
  sec1: { r1: 1, r2: 2, r3: 3 },
})

Would render as sec1 section with 3 items: 1, 2, 3.

The methods would be called as follows:

renderSectionHeader({ sectionData, sectionID })
renderItem({ item, index })

So test-utils.js will need to be updated to include a renderVirtualizedSectionHeader method with these arguments (the existing renderSectionHeader is for ImmutableListView only).

What do you think?

@taranda
Copy link

taranda commented Apr 11, 2019

@cooperka and @benadamstyles: Have you made any progress on this? I would love to see section headers implemented, and I like the shape of the input data.

Do you need any help pushing this over the goal line?

@alexiri
Copy link

alexiri commented Jun 22, 2019

We could really use this functionality as well. We're currently using ImmutableListView, but we're seeing some performance issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants