Skip to content
This repository has been archived by the owner on Nov 26, 2018. It is now read-only.

Use WeakMap for component tracking #100

Merged
merged 5 commits into from
Mar 12, 2018

Conversation

helixbass
Copy link
Contributor

Fixes #97. Replaces #95

In this PR:

  • Use WeakMap (or polyfilled version) rather than static properties to keep track of whether isCompactable()/isMapperComponent()

@codecov
Copy link

codecov bot commented Mar 4, 2018

Codecov Report

Merging #100 into master will decrease coverage by 0.13%.
The diff coverage is 94.73%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #100      +/-   ##
==========================================
- Coverage   96.45%   96.32%   -0.14%     
==========================================
  Files          65       66       +1     
  Lines         480      490      +10     
==========================================
+ Hits          463      472       +9     
- Misses         17       18       +1
Impacted Files Coverage Δ
src/utils/createHOCFromMapper.js 100% <100%> (ø) ⬆️
src/utils/createCompactableHOC.js 100% <100%> (ø) ⬆️
src/utils/WeakMap.js 66.66% <66.66%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 748cb3a...4d39016. Read the comment docs.

@@ -0,0 +1,3 @@
import WeakMap from 'es6-weak-map'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used es6-weak-map to polyfill WeakMap. If it's undesirable to add this dependency, I guess we could manually implement a fallback to using a list here - I didn't try to dive deep into the react-proxy discussions re polyfills/fallbacks, but es6-weak-map appears to have O(1) lookup time (rather than O(n) for a list)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes but it seems that this polyfill is setting a property on the weakly referenced object (in our case, the enhanced React component) so it would defeat the purpose of this PR.

I'm afraid we'll have to stick to using a list unfortunately.

If we wanted to have O(1)-ish lookup time, we could bucketize this list by taking advantage of the fact that the weakly referenced objects are components and that they (may) have a displayName or name property that we could use as bucket names.

This somehow feels a bit "clumsy" to me though. I am starting to wonder if the better way would not simply be to disable the compactable behavior of components in environments where WeakMap is not supported (that is, IE < 11).

How do you feel about this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Pinging @neoziro as he may have an opinion on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat ah ok I thought that perhaps it was safe (hidden from hoist-non-react-statics) because the referenced implementation describes it as

In a nutshell, the WeakMap shim emulates a WeakMap by adding a hidden property to the key that associates the weak map with the retained object. The shim overrides the ECMAScript 5 methods to cover its tracks.

But indeed, if I change the import from es6-weak-map to es6-weak-map/polyfill (which always uses the polyfill), tests fail

So for now removed es6-weak-map and implemented your suggestion of disabling compactable behavior (by making our utils/WeakMap not track anything if WeakMap does not exist) - not sure how to test the fallback behavior, if I change export default WeakMap || NonTrackingDummyWeakMap to export default NonTrackingDummyWeakMap, it is just "should be merged" tests (and debug test) that fail

I'm not familiar enough with recompact internals to know if disabling compactable behavior when WeakMap is missing is reasonable, but seems like it would make sense if that means that it will behave more like recompose (separate HOCs in the component hierarchy for each recompact HOC used)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is reasonable. It will indeed behave more like recompose.

Copy link
Collaborator

Choose a reason for hiding this comment

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

One way of testing this behavior would be to create another test file and override the WeakMap global before requireing HOCs that directly or indirectly depend on src/utils/WeakMap:

delete global.WeakMap
const { withProps } = require('..')

test('NonTrackingDummyWeakMap fallback', () => { /* ... */ })

const createComponentFromMappers = (mappers, childFactory) => {
const { observablesKey: OBSERVABLES } = getConfig()
const CONTEXT_TYPES = { [OBSERVABLES]: observablePropType }

return class extends Component {
const Component = class extends React.Component {
static [MAPPERS_INFO] = { mappers, childFactory }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried following same pattern as in createCompactableHOC (which is what react-proxy does) to get rid of MAPPERS_INFO and store { mappers, childFactory } as the value in allMapperComponents, but was running into errors - don't see the harm in still accessing via MAPPERS_INFO as long as we aren't using its presence to determine whether isMapperComponent()

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you're right.

const isCompactable = Component =>
typeof Component === 'function' && Component[compactable]
typeof Component === 'function' && allCompactableComponents.has(Component)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't know if it makes sense to get rid of typeof Component === 'function' (wasn't sure what it was guarding against/optimizing previously)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO, it was defensive programming (so that isCompactable(null) would return false instead of throwing a TypeError for instance).

I think we can probably get rid of it now that we're using a WeakMap.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat ok removed typeof check here and in isMapperComponent()

@@ -39,4 +39,31 @@ describe('withProps', () => {
)
expect(wrapper.equals(<div />)).toBeTruthy()
})

it("doesn't compact surrounded HOC that hoists statics", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tests isCompactable() (test fails against master version of utils/createCompactableHOC)

expect(wrapper.prop('foo')).toBe('bar')
})

it("doesn't compact double-surrounded HOC that hoists statics", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tests isMapperComponent() (test fails against master version of utils/createHOCFromMapper

@@ -0,0 +1,9 @@
class NonTrackingDummyWeakMap {
get() {}
set() {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be worth retuning this so that our NonTrackingDummyWeakMap behaves like WeakMap with regard to chaining.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat ok updated

}
}

export default WeakMap || NonTrackingDummyWeakMap
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this would throw a ReferenceError if WeakMap is not defined.

Maybe we could go with something like the following:

export default (typeof WeakMap === 'undefined'
  ? NonTrackingDummyWeakMap
  : WeakMap)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat ok updated

@matthieuprat
Copy link
Collaborator

You can add /* eslint-disable class-methods-use-this */ to src/utils/WeakMap.js to fix the build.

@matthieuprat
Copy link
Collaborator

Also, you may want to run yarn test locally before pushing to be fairly confident that both tests and ESLint checks will pass on CI.

}
}

export default WeakMap || NonTrackingDummyWeakMap
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat ok updated

@@ -0,0 +1,9 @@
class NonTrackingDummyWeakMap {
get() {}
set() {}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat ok updated

@@ -0,0 +1,16 @@
/* eslint-disable class-methods-use-this */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat sorry for build failure, yarn test was passing, now I'm running yarn lint && yarn test

import React from 'react'
import { shallow } from 'enzyme'

describe('withProps using NonTrackingDummyWeakMap fallback', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthieuprat added tests for fallback NonTrackingDummyWeakMap behavior

@matthieuprat matthieuprat merged commit 109c56e into gregberge:master Mar 12, 2018
@matthieuprat
Copy link
Collaborator

Thank you! 🎉

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

Successfully merging this pull request may close these issues.

2 participants