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

[email protected]: hooks, forwardRef support, and better types #83

Merged
merged 9 commits into from
May 7, 2020

Conversation

aem
Copy link
Contributor

@aem aem commented Apr 6, 2020

This is a proposal for a v4 of general-store. There's not actually any new functionality, but I figure that correct TS typings + the ESM main module is enough to warrant a new major version given that those are both technically breaking changes. Realistically, internally at HubSpot I don't think our repackage will have to bump a major version, since we shim CJS modules anyway I believe the default import of general store will still work, and none of the other code changes are breaking - connect + useStoreDependency still work fine.

TypeScript

In v3, I did the TypeScript conversion, but given the complexity of the types (and the fact that I hadn't tried to use General Store in an actual TS project), the types were compiling successfully but didn't actually type anything in a meaningful way. The old types wouldn't properly validate the props on a connected component to verify they were a superset of the general store dependencies, and the output of a useStoreDependency call was always typed as any.

This PR fully reworks the TS types using generics and inferred conditional types to ensure that all General Store calls are typed properly. The TS compiler will fail the connect call if the wrapped component doesn't include all of the dependencies in the DependencyMap in its props type, and useStoreDependency correctly infers its return type assuming the dependency has an inferrable type (e.g. the TS compiler doesn't have to resort to any).

Type checks in action

Everything type checks just fine!

image

But if the store is of an unexpected type, connect and useStoreDependency both throw.

image

Connect also throws if the dependencies don't sufficiently overlap with the component props.

image

Lastly, we now publish the project's TS types and reference them in the package.json, allowing external projects to pull in the type definitions properly.

Connect

The old legacy version of connect was a bit bloated, and relied on a lot of extra mapping + reducing of objects, creating some internal performance problems. The new version just uses the same internals as useStoreDependency, allowing us to vastly simplify the internal code of General Store. By my estimate (some rough back of the napkin math) this reduces the bundle footprint of General Store by more than 30%.

One small change that was made - we no longer check the length on deref functions to determine whether or not to pass the state/dependency map. Passing extra arguments shouldn't cause a problem for existing dependencies, and the fact that there were actually different code paths based on the number of arguments your deref took always felt weird to me.

Misc

  • The GeneralStore.ts entry point now uses ES2015 exports rather than CJS. The rest of the module uses ES2015 modules, so by converting the entry point to do the same, the library is now fully ESM compliant and will be treeshake-able.
  • The docs have been updated to clarify how component composition with connect works - the second state argument was never documented up until now, you'd only find out about it from reading prior art or the library's actual source
  • Some tests are now obsolete with the new fully-hooks-based implementation. Those tests were removed and not backfilled, but the test suite is still comprehensive.

@colbyr

@aem aem requested review from henryqdineen and Friss April 6, 2020 14:32
@henryqdineen
Copy link
Contributor

I'm probably not the best to review the TypeScript changes but everything else looks great. 👍

@aem aem mentioned this pull request Apr 6, 2020
Copy link
Member

@Friss Friss left a comment

Choose a reason for hiding this comment

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

Couple thoughts inline.

One thought is also if the equals func should care / have a different branch for arrays over regular objects. Since the current for in will just give us the indexes. Or is it only used on objects?

Also the Typescript typing changes are a bit over my head but I saw more types and that is always good right? :badpokerface:


const newValue = calculate(dependency, props);
if (!shallowEqual(newValue, dependencyValue)) {
setDependencyValue(newValue);
if (!deepEqual(newValue, dependencyValue.dependency)) {
Copy link
Member

Choose a reason for hiding this comment

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

Curious why the change here from shallow to deep?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoops, copypasta from the new version of connect. connect requires the deepEqual because the state in that component is a DependencyMap, so we need to check that the entire map is the same and trigger the setState if any of the sub-objects is meaningfully different, but in useStoreDependency it's a single result so it can be a shallowEqual. I'll update

return false;
}
} else {
if (!_equal(obj1[property], obj2[property], false)) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: could just return 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.

:nod:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

quick update, ya can't actually! since this is iterating over every property, returning the value here will short circuit the logic and could return a false-positive if this logic isn't the last property checked


describe('deepEqual', () => {
it('deeply compares values', () => {
expect(deepEqual(1, 1)).toBe(true);
Copy link
Member

Choose a reason for hiding this comment

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

nit: Array test?

@aem
Copy link
Contributor Author

aem commented Apr 9, 2020

Also the Typescript typing changes are a bit over my head but I saw more types and that is always good right? :badpokerface:

I don't know if they're perfect, but they definitely do work, which is all I was really hoping for. ¯\_(ツ)_/¯ I'm excited to see how many CRM things explode when we introduce the TS types to those components. Excited for lots of weird stuff like prop: string | Map | List | null

Copy link
Contributor

@colbyr colbyr left a comment

Choose a reason for hiding this comment

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

Oh man. I've been having deep nostalgia for GeneralStore the last few weeks. I'm absolutely going to build something with it once you release this 🤩


Passing extra arguments shouldn't cause a problem for existing dependencies, and the fact that there were actually different code paths based on the number of arguments your deref took always felt weird to me.

I think that had to do with the StoreDependencyMixin and like [email protected] when dependencies could read from the state of the component they were bound to. We used the deref arg length, to decide if the dependency needed to re-evaluate when the component state changed.

export function calculateForStateChange(
dependencies: DependencyMap,
props: Object,
state: ?Object
): Object {
return oFilterMap(
dependencies,
(dep) => dep.deref && dep.deref.length > 1,
(dep) => calculate(dep, props, state)
);
}

At the time it saved a ton of cycles because mostly deps didn't use state, but I don't think that's relevant to the more recent connect strategies! It's probably safe to remove that behavior, but y'all would probably know better than me 😉

@aem
Copy link
Contributor Author

aem commented Apr 9, 2020

I think that had to do with the StoreDependencyMixin and like [email protected] when dependencies could read from the state of the component they were bound to. We used the deref arg length, to decide if the dependency needed to re-evaluate when the component state changed.

Wow, TIL. That makes perfect sense, and definitely explains why the connect/StoreDependencyMixin codepaths were so much more complex than what was required to get useStoreDependency working. Regardless, since everything is props now, seems like unifying the logic and shedding the extra weight of the code needed to support the old architecture is worthwhile.

@colbyr
Copy link
Contributor

colbyr commented Apr 9, 2020

Wow, TIL. That makes perfect sense, and definitely explains why the connect/StoreDependencyMixin codepaths were so much more complex than what was required to get useStoreDependency working.

Yeah, it was kind of a mess. Good riddance! 😛

@aem
Copy link
Contributor Author

aem commented Apr 13, 2020

Gonna look at integrating https://github.com/joarwilk/flowgen into the build process. If we're doubling down on shipping types we should probably output Flow types as well for completeness

@aem
Copy link
Contributor Author

aem commented May 1, 2020

So good news, I finally got flow typegen working. Bad news, I'm not sure the types will be super useful 😞 since the TS typings require conditional types to work well and Flow doesn't support conditional types I'm not sure how useful they'll be. At least there's something though?

@aem
Copy link
Contributor Author

aem commented May 1, 2020

Also, I think I'm going to just submit this as one huge PR. I tried to split things out atomically, but the types for useStoreDependency and the old iteration of connect don't coalesce very nicely, so without the connect refactor the types don't make sense, but without the type refactor the connect refactor doesn't work well either. Basically, I accidentally created a chicken + egg problem for myself and it'll be hard to untangle that

@aem aem marked this pull request as ready for review May 1, 2020 18:58
@aem aem changed the title RFC: [email protected] [email protected]: hooks, forwardRef support, and better types May 1, 2020
@aem
Copy link
Contributor Author

aem commented May 7, 2020

@colbyr @Friss @henryqdineen going to merge and publish under general-store@next and leaving npm latest at 3.2 for a bit. @Friss has volunteered to help test it out in a nontrivial app this weekend since the connect tests are a bit contrived

@aem aem merged commit 913f9f5 into master May 7, 2020
@aem aem deleted the adam/ts-types-connect-refactor branch May 7, 2020 16:45
@aem
Copy link
Contributor Author

aem commented May 7, 2020

$ npm info general-store@latest

[email protected] | MIT | deps: 1 | versions: 45
Simple, flexible store implementation for Flux.
https://github.com/HubSpot/general-store

keywords: flux, store, react

dist
.tarball: https://registry.npmjs.org/general-store/-/general-store-3.2.1.tgz
.shasum: 9a94e29204d7f3321044f4b38e572fed85dce269
.integrity: sha512-Mvgo3f/e/6Z+nSpNHWwqEE+hHEdE2nNwsOdKPTxT4JXqso7oUINjoFpkaPHCvjSotXarTFxDh9VuNpnPm6LVkQ==
.unpackedSize: 208.7 kB

dependencies:
hoist-non-react-statics: 3.3.0 

maintainers:
- colbyr <[email protected]>

dist-tags:
latest: 3.2.1  next: 4.0.0    

published 5 months ago by amarkon <[email protected]>

4.0.0 published under the next tag, will be published as latest once I get confirmation things seem to be working

@Friss
Copy link
Member

Friss commented May 11, 2020

4.0 seems to be working well. I did have to change from import GeneralStore from 'general-store' to import {define} from 'general-store' (which was/is expected) My app is made up of a few define and defineFactory stores that all use the connect HOC.

We should also update the docs to reflect that you would need to do a import * as GeneralStore or do import {define} for the few examples inline.

@aem
Copy link
Contributor Author

aem commented May 11, 2020

good call, I'll update the docs before publishing latest. probably not worth building automation for this internally since it's an easy find+replace?

@Friss
Copy link
Member

Friss commented May 11, 2020

Might take a bit of coordination to migrate libraries and UIs with this change but the underlying change is pretty simple.

@aem aem mentioned this pull request May 11, 2020
@aem
Copy link
Contributor Author

aem commented May 13, 2020

4.0.1 was just promoted to latest on npm and includes the performance regression fix from #84 on top of the changes in this PR

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