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

[POC] Styled render callback component and refactored withStyles #9503

Closed
wants to merge 21 commits into from

Conversation

rosskevin
Copy link
Member

@rosskevin rosskevin commented Dec 15, 2017

DO NOT MERGE, for review/feedback only

POC towards #9272 (comment)

related: #8542 #8447 #7636

Approach

Move the implementation of withStyles to Styled and retain withStyles API exactly as it has been.

Deficiencies

  • anything with the comment REVIEW
  • determining stylesheet names (will be impossible? without HOC)?
  • no attempt at TS types yet
  • indexCounter could be irrelevant now?

Usage

current withStyles HOC or new render callback:

<Styled styles={myStyles}>
  {(classes, theme) => <div className={classes.root} />}
</Styled>

}

warning(
indexCounter < 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

< 0 ?

Copy link
Member

Choose a reason for hiding this comment

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

This warning isn't that useful. It' in case you called the component many many times. But would have probably found the issue before.

Copy link
Member

Choose a reason for hiding this comment

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

Still, the warning doesn't harm.

Copy link
Member

@oliviertassinari oliviertassinari 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 an API people have been asking for different time in the past. I like the flexibility compound component provides. I would vote for keeping both API. I have been giving some thought on the tradeoff both API offer on an old issue

On a side note. We need to be cautious with this new API. I think that we should keep using the HOC API. I don't want newcomers to ask themselves which API they should be using. What's the best one? This is unneeded complexity for newcomers. It doesn't matter for them. Just use one and stick to it.

@@ -1,11 +0,0 @@
// @flow

module.exports = {
Copy link
Member

Choose a reason for hiding this comment

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

Why? Isn't it better to have all our configuration in js?

Copy link
Member Author

Choose a reason for hiding this comment

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

Easier to use yml, it's text only. Shorter file name. Dot file name, more hidden in the src tree.

Copy link
Member

Choose a reason for hiding this comment

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

My order of preference for configuration files is: js > yml > json. But as you prefer :).

}

warning(
indexCounter < 0,
Copy link
Member

Choose a reason for hiding this comment

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

This warning isn't that useful. It' in case you called the component many many times. But would have probably found the issue before.

return <Component classes={classes} {...more} {...other} ref={innerRef} />;
}
}

Style.propTypes = {
/**
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this?

Copy link
Member

Choose a reason for hiding this comment

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

I mean, the propTypes ?

Copy link
Member Author

@rosskevin rosskevin Dec 15, 2017

Choose a reason for hiding this comment

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

innerRef is exclusive to withStyles, classes though can be omitted. --Actually, I don't think we need any (including innerRef), just pass-through now.

'Material-UI: you might have a memory leak.',
'The indexCounter is not supposed to grow that much.',
].join(' '),
const Style = () => (
Copy link
Member

Choose a reason for hiding this comment

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

We almost never use anonymous arrow function for the components definitions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, why not? (curious) What do you prefer here?

}

class Styled extends React.Component {
static defaultProps = {
Copy link
Member

Choose a reason for hiding this comment

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

I have been removing all the static properties as they get in the way of refactoring components from classes to functions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Don't do that if it is still a class, it is invalid typescript and could harm our potential conversion. If it's a class, static props are the right way, the other way is just a hack after all.

Copy link
Member

@oliviertassinari oliviertassinari Dec 15, 2017

Choose a reason for hiding this comment

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

Oh, TypeScript doesn't support this?

Copy link
Member Author

Choose a reason for hiding this comment

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

You have to: augment the definition of the target, or cast it. So it supports it, but you have to cast it to the type first. In other words it is uglier on the outside, vs static class prop.

Copy link
Member

Choose a reason for hiding this comment

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

Alright. At least I can keep using this pattern at work. It's saving us time refactoring component class <-> function.

}

warning(
indexCounter < 0,
Copy link
Member

Choose a reason for hiding this comment

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

Still, the warning doesn't harm.

@rosskevin rosskevin added the on hold There is a blocker, we need to wait label Dec 15, 2017
@rosskevin
Copy link
Member Author

rosskevin commented Dec 15, 2017

@kof @oliviertassinari perhaps you might have a thought on this one.

When using Styled - styles is passed as a prop, and is not the same object as const styles in the original component. This causes a sheet to be not found, and a new one created on every render. Specifically:

    let sheetManager = this.sheetsManager.get(this.stylesCreatorSaved);

    if (!sheetManager) {
      sheetManager = new Map();
      this.sheetsManager.set(this.stylesCreatorSaved, sheetManager);
    }

where this.stylesCreatorSaved is withStyles(styles)/<Styled styles={styles}/> sourced from const styles in the component file. Before when styles was passed in as an option to the HOC, this was not a problem because styles === this.stylesCreatorSaved. It appears react passes a copy.

This is a show stopper if not solved because it causes massive sheet growth.

^^^Next question: If this can be solved, won't we still have the same growth issue with interpolating props into styles?

If you want to run it - look at withStyles.spec.js, specifically: should provide a classes property and debug Styled:160

@rosskevin
Copy link
Member Author

rosskevin commented Dec 15, 2017

I’m wrong about the props being modified - I committed a hack that proves it. It is this.stylesCreator = getStylesCreator(props.styles)

But, my comment about sheet growth with props interpolation is something to be weary of with this approach.

Moving forward again...

@rosskevin
Copy link
Member Author

rosskevin commented Dec 15, 2017

So it appears that the result of:
getStylesCreator(styles) attempt 1 !== getStylesCreator(styles) attempt 2
even though
styles attempt 1 === styles attempt 2

@rosskevin
Copy link
Member Author

rosskevin commented Dec 15, 2017

I think this may have been the source of the need for the hot loader hack. Maybe... just speculating at this point.

@rosskevin
Copy link
Member Author

Targeting the withStyles.spec essentially as-it-was, only HMR with same state is failing and while I took a shot at it, I have to admit I don't understand it. I'm going to try some in browser tests and see if the parent-child order is a problem.

As @oliviertassinari said in chat:

@rosskevin I have just realized one thing. I don't think it's technically possible to solve the CSS priority issue with the Styled architecture and index based counting approach. Maybe I'm missing something. I'm going to sleep. I will see clearer tomorrow.
Man, you just started a week+ task (I took me 3 weeks, 1 week to implement and 2 weeks to stabilize after user feedbacks, when revamping Nathan solution). This Styled API might require a rewrite.

@rosskevin
Copy link
Member Author

Ok @oliviertassinari @pelotom - I've got to go for the night. I've got a full day into this (plus a little yesterday).

Two things need to be considered:

  1. parent/child issue - my guess is that is what is at play in the docs AppBar as well as Menu:
    appbar
  2. spec changes - I have made Paper.spec work as a sample. A lot of repetition for getting to the component's first child. We can either create a utility for this or use some enzyme foo to make this easier, but I don't want to move on without a more readable solution (as well as to understand if we have the parent/child issue)

Any thoughts are welcome. I think this is a decision point - I need to confirm before spending more time.

}}
</Styled>
);
};
Copy link
Member Author

Choose a reason for hiding this comment

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

This is an example where the render callback pattern is better. withTheme and innerRef are HOC options not necessary using Styled. With a render callback pattern, the child chooses what it uses and what it ignores (e.g. theme), and the innerRef indirection is unnecessary because the parent can put the ref where ever it chooses. This code shows what it took to match the various current usage patterns of the HOC.

@kof
Copy link
Contributor

kof commented Dec 16, 2017

We should switch to react-jss, really, maintaining both projects with edge cases bugs is hard. I know there are some things missing, we need to figure out how to add them.

@rosskevin
Copy link
Member Author

We should switch to react-jss, really, maintaining both projects with edge cases bugs is hard. I know there are some things missing, we need to figure out how to add them.

@kof several thoughts come to mind:

  1. I would have liked to have this comment 24 hours ago when I put in 1h and asked for a review from you and before I spent 9h yesterday! Nonetheless, I think I'm up to speed on our styling solution now and I know you are busy yourself.
  2. I don't know the reasons why we have ours vs react-jss - and I would like to understand the details now that I am thoroughly familiar with our solution
  3. react-jss doesn't seem to offer a render callback component, which this PR (and linked issues) is targeting.

A cursory review of react-jss seems to share many similarities, but the devil is in the details.

  • Is there any reason for us to continue with our mui solution vs react-jss?
  • Is there a way with react-jss to get a render callback pattern and solve the linked issues concerns?

@kof
Copy link
Contributor

kof commented Dec 16, 2017

@rosskevin

We talked with @oliviertassinari many times and he created a bunch of issues on react-jss in order to move most parts of the logic at some point (most of them have been already resolved and released). In order to move faster, it is easier to implement things in mui directly first and then find a way to add them to react-jss if they make sense for more general use cases. MUI might still have very specific requirements, where react-jss can't or shohuldn't cover them.

React-JSS has a render callback interface issue for a long time now, it was never clear why we would want that, it still isn't, but it is certainly an interesting interface.

I am always hesitating to introduce an alternative interface, because it will make users to choose, without having a very clear explanation of "why".

@rosskevin
Copy link
Member Author

/cc @oliviertassinari - I forgot that I'm so far down the road of static typing, that others might not recognize the pain of HOCs. So @oliviertassinari, some of this is I owe to you based on our chat yesterday.

Users can choose one or the other, where it suits their use case. In my code (for styles), I would use the render callback pattern exclusively because styling is a render-only use case - in other words - none of the props injected have any bearing on the rest of the implementation of the components as they are only needed to render (e.g. not needed in componentWill*). I also have enough other HOCs (in app code) where adding another is just painful (especially with tying together a strongly typed component).

Render callbacks are significantly less static type coupling and coupling in general:

  1. no merging of external facing fabricated component props (e.g. hoc optional props + component props)
  2. no requirement of direct child to implement injected props (hoc requires child component to accept all injected props - then omit them from it's children)
  3. freedom of render structure (e.g. insert or reorder children, the top level doesn't have to accept injected props anymore)
  4. explicit clarity of source of props (no magic wondering where the prop is coming from)
  5. debugging is easier
  6. static type errors are dramatically easier

I need no more convincing that this pattern is dramatically easier to create, maintain, and use. BUT, with that said, there are cases (probably 10%) where a HOC is a nicer pattern of use (albeit not a better pattern to have to maintain). Since any render callback pattern can also be easily wrapped and exposed as a HOC, it seems like a no-brainer to do.

@kof
Copy link
Contributor

kof commented Dec 16, 2017

@rosskevin yeah, sounds very convincing as you say it, worth a blog post with all the details! Now I am actually more motivated to add it to React-JSS.

@rosskevin
Copy link
Member Author

So @kof - is react-jss going to implement it - or do we just continue here on our own?

@kof
Copy link
Contributor

kof commented Dec 16, 2017

Would you like to try add it to react-jss? I haven't thought much about technical implications or challenges yet, but it would be certainly worth trying.

@Bnaya
Copy link

Bnaya commented Dec 16, 2017

A Hint,
There's a pitfall with mobx and this pattern.

const MyComponent = observer(({ model }: { model: MyMobxModel }) => {
  return (
    <Styled styles={myStyles}>
      {({ classes }) => <div className={classes.root}>{model.name}</div>}
    </Styled>
  );
});

The function inside the Styled is actually another component, which is not observer, and will not render on model.name changes.
So you will need to do something like:

const MyComponent = observer(({ model }: { model: MyMobxModel }) => {
  return (
    <Styled styles={myStyles}>
      {observer(({ classes }) => <div className={classes.root}>{model.name}</div>)}
    </Styled>
  );
});

But i'm not sure if this will work

Edit - all of the above no relevant, my mistake,
You can use mobx observer also as component. so you just need to nest it inside the Styled

<Styled styles={myStyles}>
  {({ classes }) => (
    <Observer>
      {() => <div className={classes.root}>{model.name}</div>}
    </Observer>
  )}
</Styled>;

@mweststrate
Copy link

@Bnaya note that Observer should be uppercased to use it as component

Copy link
Member

@pelotom pelotom left a comment

Choose a reason for hiding this comment

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

I just want to note here that unfortunately I don't think Styled will be very usable in TypeScript until microsoft/TypeScript#6395 is solved. The reason is that the Styled component needs to be generic in the keys of its style prop. IOW, you want to give it a type like

interface StyledProps<ClassKey extends string> {
  styles: Record<ClassKey, React.CSSProperties>;
  children(classes: WithStyles<ClassKey>): JSX.Element;
}

class Styled<ClassKey> extends React.Component<StyledProps<ClassKey>> {
  // ...
}

but then when you go to use it there's no easy way to instantiate the generic parameter, and type inference won't help you:

// doesn't work--`ClassKey` can't be inferred
<Styled styles={myStyles}>
  {classes => /* ... */}
</Styled>

// also doesn't work--syntax error
<Styled<keyof myStyles> styles={myStyles}>
  {classes => /* ... */}
</Styled>

@rosskevin
Copy link
Member Author

I'm closing this out (for now):

  1. We have a bit of a typescript pain that is not resolvable yet
  2. This shows the parent/child order problem
  3. We'd like to utilize react-jss as much as possible (this doesn't help there)

I'm still firmly for using this pattern, but I'm not sure the best place to do it is here vs react-jss, as well as how it impacts types (which I really like the validation as it sits today).

@rosskevin rosskevin closed this Dec 20, 2017
@oliviertassinari
Copy link
Member

@rosskevin Thanks for working on the POC. The conclusions are great starting points for anyone who would like to push the effort forward!

@Bnaya
Copy link

Bnaya commented Jan 9, 2018

interface StyledProps<ClassKey extends string> {
  children(classes: WithStyles<ClassKey>): JSX.Element;
}


function createStyled<ClassKey extends string>(stylesParam: Record<ClassKey, React.CSSProperties>) {
  return class InnerStyled extends React.Component<StyledProps<ClassKey>> {

  }
}

const Styled = createStyled({elementA: {}, e2: {}});
const b = <Styled>
  {(styles) => {
    return <div className={styles.classes.elementA}><a className={styles.classes.e2}></a></div>
  }}
</Styled>;

@rosskevin The above pattern will work type safely also now.
You can also make createStyled to take a function with the theme lazily

@Bnaya
Copy link

Bnaya commented Jan 9, 2018

@oliviertassinari @rosskevin

I've made this function, would like to hear your feedback

import { WithStyles } from "material-ui";
import { Theme } from "material-ui/styles";
import withStyles from "material-ui/styles/withStyles";
import * as React from "react";

interface IStyledProps<ClassKey extends string> {
  children(classes: WithStyles<ClassKey>): JSX.Element;
}

type stylesParamType<ClassKey extends string> = (
  theme: Theme,
) => Record<ClassKey, React.CSSProperties> | Record<ClassKey, React.CSSProperties>;

export function createStyled<ClassKey extends string>(stylesParam: stylesParamType<ClassKey>) {
  const wrapperComponent = withStyles(stylesParam);

  const createdStyled: React.StatelessComponent<IStyledProps<ClassKey>> = function createdStyledComponent(props) {
    return React.createElement(
      wrapperComponent(stylePropsFromWithStyles => {
        return props.children(stylePropsFromWithStyles);
      }),
    );
  };

  return createdStyled;
}

const styles = (theme: Theme) => ({
  root: {
    width: "100%",
    background: theme.palette.background.paper,
    flex: "1",
    overflowY: "scroll",
  } as React.CSSProperties,
});

const Styled = createStyled(styles);

const a = <Styled>{(styleLocal) => {
  return <div className={styleLocal.classes.root}>Test</div>;
}}</Styled>;

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 9, 2018

@Bnaya I have been implementing something close in https://gist.github.com/oliviertassinari/dceb407e99d7279cc4228957c068d27c. It's a styled-components like API.
I fear the CSS precedence issue is a blocker for pushing the compound API forward :/.

@Bnaya
Copy link

Bnaya commented Jan 9, 2018

What's the CSS precedence issue?
My code also gives you correct typescript types for the class names

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 9, 2018

@Bnaya You can use this API outside of a React render method, you can't use it inside. If you do, the injected CSS won't get precedence on the children you are trying to customize.

@Bnaya
Copy link

Bnaya commented Jan 9, 2018

Can you link to the relevant issue (if any)?

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 10, 2018

@Bnaya There is no "opened" issue for this, nor link as it's pure speculation from my experience. If you build the component outside of the render method, in a static way, it should be good :).
This means that we potentially have 3 different styling APIs. The implementation of the 2 other APIs is simple and short sugar on top of withStyles(). This is exciting. Thanks for sharing. It's something people need to be aware of. But I'm wondering. What's the best way to deliver?

@rosskevin rosskevin deleted the styled-withstyles branch January 25, 2019 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
on hold There is a blocker, we need to wait
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants