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

Use new context API #1894

Merged

Conversation

alexandernanberg
Copy link
Member

@alexandernanberg alexandernanberg commented Aug 6, 2018

This is just to get the ball rolling with the new context API

Fixes #1459
Fixes #1655

Pretty happy with how the ThemeProvider turned out, a lot cleaner. I'll continue working on this and hopefully fully landing this PR.

Enzyme does not support the react > 16.3 yet so it's hard to test this until that gets fixed. We probably want to use a polyfill for this as well, like create-react-context unless we drop React < 16.3 support

No hard feelings if you decide not to use this!

@quantizor
Copy link
Contributor

quantizor commented Aug 6, 2018

Good start! Let's remove that memoize-one dep that looks to have slipped in.

At this point for 4.0 I think we will just plan to drop old React support and just target 16.4+ to keep things easy.

@alexandernanberg
Copy link
Member Author

Oh I actually just forgot to use the memoize function, we need to either memoize the getContext method or move the context to state because otherwise we'll provide a new context value each render which will make consumers update when they might not need to.

Cool, nice to save some KBs as well! 👍

@quantizor
Copy link
Contributor

Let's use state and recalculate upon prop change by hard equality

@alexandernanberg
Copy link
Member Author

alexandernanberg commented Aug 6, 2018

Okay, although I think using the memoized approach is a bit cleaner and simpler, because then you don't need to recalculate in getDerivedPropsFromState. But either way is fine

Edit: Btw I think this is really useful info https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization

@alexandernanberg
Copy link
Member Author

I think the ThemeProvider should work now 😮

Switched the test to react-testing-library because enzyme wasn't working (no react 16.3+ compability). If you don't want to switch over entirely to react-testing-library we could migrate back later.

Left the memoization for now but should be fairly simple to change if you'd still like that

@@ -110,7 +87,8 @@ class BaseStyledComponent extends Component<*, BaseState> {

generateAndInjectStyles(theme: any, props: any) {
const { attrs, componentStyle, warnTooManyClasses } = this.constructor
const styleSheet = this.context[CONTEXT_KEY] || StyleSheet.master
// const styleSheet = this.context[CONTEXT_KEY] || StyleSheet.master
const styleSheet = StyleSheet.master
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm unsure what this previously was for, but I think it will be fine just assigning it to Stylesheet.master?

Copy link
Contributor

Choose a reason for hiding this comment

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

@kitten can you comment on this? I think it's okay

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh just realized that that context comes from the StyleSheetManager, so I'll have to wrap it in a StyleSheetConsumer or similar to get the correct value

return createElement(target, propsForElement)
return (
<ThemeConsumer>
{({ theme } = {}) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you use the empty object from utils/empties?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also cache this function somehow instead of recreating it every render?

Copy link
Member Author

Choose a reason for hiding this comment

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

If I use EMPTY_OBJECT I get a flow error though, so the best way might create a type for the context and not destructure it, right? I've not really worked with flow before so I might need some help on certain bits

Yeah good point, although I don't it's that expensive to recreate it

Copy link
Contributor

Choose a reason for hiding this comment

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

It's more about memory pressure. This is a hot path, so we want it to be as maximally-efficient as possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Makes sense, will refactor to take this into account!

getContext(theme: (outerTheme: ?Theme) => void, outerTheme?: Theme) {
return {
theme: this.getTheme(theme, outerTheme),
}
Copy link
Member Author

Choose a reason for hiding this comment

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

What if we instead of using setting context to { theme } we set it to theme without a wrapping object, we won't be able to add any other properties on the context but I don't think we will want that anyways?

@alexandernanberg
Copy link
Member Author

alexandernanberg commented Aug 9, 2018


Edit: Wow I actually used `this.target` instead of `this.constructor.target` 🤦‍♂️ 

@alexandernanberg
Copy link
Member Author

Tests are passing 🎉

Would love to get some second opinions and feedback on this now @kitten @mxstbr @probablyup

@quantizor
Copy link
Contributor

Nice! Ok so the last thing I want to do here is some performance analysis.

Specifically making sure we aren't regressing against current master. Could you rig up a test using ThemeProvider in the benchmark suite (it's in this repo under benchmarks.)

@quantizor
Copy link
Contributor

Let's also switch this branch to be pointing at "develop" which is the 4.0 development branch I just made.

@quantizor quantizor mentioned this pull request Aug 9, 2018
19 tasks
@alexandernanberg alexandernanberg changed the base branch from master to develop August 9, 2018 17:23
@alexandernanberg alexandernanberg changed the title [WIP] Use new React context API Use new context API Aug 10, 2018
@alexandernanberg
Copy link
Member Author

Okay so I’m not sure that I interpret the benchmark results correctly, is a high or low number better? First thought it was high=better but I just noticed the ms suffix which made me uncertain

Anything you want me to fix before this can get merged?

@quantizor
Copy link
Contributor

Lower ms is better. I think @kitten is right though, there are a bunch of other changes we're going to make in this branch so perf analysis might be premature.

package.json Outdated
@@ -150,7 +148,7 @@
"typescript": "^2.4.1"
},
"peerDependencies": {
"react": ">= 0.14.0 < 17.0.0-0"
"react": ">= 16.3.0 < 17.0.0-0"
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just make this >= 16.3 for simplicity.

return React.createElement(Component, props)
return (
<ThemeConsumer>
{(theme?: Theme) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's cache the function on the class so it's not getting recreated per render.


return (
<StyleSheetConsumer>
{(styleSheet?: StyleSheet) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, let's cache it on the class.

return createElement(target, propsForElement, children)
return (
<ThemeConsumer>
{(theme?: Theme) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

And here


constructor(props: Props) {
super(props)
this.getContext = memoize(this.getContext.bind(this))
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need to do constructor if you use this pattern on the class itself:

class ThemeProvider extends Component<Props> {
  getContext = memoize(
    (theme: (outerTheme: ?Theme) => void, outerTheme?: Theme) => this.getTheme(theme, outerTheme)
  )
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I actually wrote it similar to that first, but refactored after reading this thread. I was worried about booth perf and that the output isn't the same. Example here.

However if you feel that that's not a problem I'll happily change

Copy link
Contributor

Choose a reason for hiding this comment

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

You can leave it though, I'll experiment with it both ways later

@quantizor
Copy link
Contributor

This is looking great! Let's do this final cleanup pass and get it in!

@quantizor quantizor added the 4.0 label Aug 11, 2018
@alexandernanberg
Copy link
Member Author

Cool, in that case this PR actually outperforms the master branch, correct?

So I tested to use class properties instead of binding the function in the constructor and it performed worse :/ Feel free to try this yourself as well, in case I did something wrong.

Also tested to break out the inlined functions inside the render method, and that didn't seem to make any notable difference other than we run into the same problem described above.

It might be easier to get this in the develop branch and then iterate on these things?

@quantizor
Copy link
Contributor

@alexandernanberg Yeah that's possibly true! We did make a lot of changes.

@@ -2,117 +2,93 @@
/* eslint-disable react/no-multi-comp */
import React from 'react'
import { shallow, render, mount } from 'enzyme'
import ThemeProvider, { CHANNEL_NEXT, CONTEXT_CHANNEL_SHAPE } from '../ThemeProvider'
import ThemeProvider, { ThemeConsumer } from '../ThemeProvider'
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe instead of ThemeConsumer we should test this with the withTheme HOC and normal styled constructors instead. That way we are ensuring the external API doesn't change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay so would this be OK or do we want to use expectCSSMatches function as it's done in theme.test.js?

  it('should merge its theme with an outer theme', () => {
    const outerTheme = { main: 'black' }
    const innerTheme = { secondary: 'black' }

    const childrenSpy = jest.fn()
    const MyDiv = ({ theme }) => {
      childrenSpy(theme)
      return null
    }
    const MyDivWithTheme = withTheme(MyDiv)

    render(
      <ThemeProvider theme={outerTheme}>
        <ThemeProvider theme={innerTheme}>
          <MyDivWithTheme />
        </ThemeProvider>
      </ThemeProvider>
    )

    expect(childrenSpy).toHaveBeenCalledTimes(1)
    expect(childrenSpy).toHaveBeenCalledWith({
      ...outerTheme,
      ...innerTheme,
    })
  })

@quantizor
Copy link
Contributor

I'm happy with this once the last testing point is resolved. Excellent work 👊

@alexandernanberg
Copy link
Member Author

Superb! Thank you 😄

@quantizor
Copy link
Contributor

It's going in!

@quantizor quantizor merged commit f242f9a into styled-components:develop Aug 12, 2018
@mxstbr
Copy link
Member

mxstbr commented Aug 12, 2018

Thank you so much for helping us improve styled-components! Based on our Community Guidelines every person that has a PR of any kind merged is offered an invitation to the styled-components organization—that is you right now!

Should you accept, you'll get write access to the main repository. (and a fancy styled-components logo on your GitHub profile!) You'll be able to label and close issues, merge pull requests, create new branches, etc. We want you to help us out with those things, so don't be scared to do them! Make sure to read our contribution and community guidelines, which explains all of this in more detail. Of course, if you have any questions just let me know!

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.

None yet

4 participants