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

Discussion: are callbacks to withStyles always the best way to obtain the theme? #9272

Closed
pelotom opened this issue Nov 22, 2017 · 25 comments
Closed
Assignees
Labels
discussion support: question Community support but can be turned into an improvement typescript

Comments

@pelotom
Copy link
Member

pelotom commented Nov 22, 2017

This question occurred to me while thinking about #9192, and I'll repost part of my comment there:

If one is not developing reusable MUI components, but instead making an app, is there any advantage to using withStyles with a callback, vs. exporting your theme from a module and then importing it everywhere that you want to use it? For example,

// src/theme.ts

import createMuiTheme from 'material-ui/styles/createMuiTheme';
import { red, purple } from 'material-ui/colors';

export default createMuiTheme({
  palette: {
    primary: red,
    secondary: purple,
  },
});
// src/index.tsx

import * as React from 'react';
import App from './App';
import theme from './theme';

ReactDOM.render(
  <MuiThemeProvider theme={theme}>
    <App />
  </MuiThemeProvider>,
  document.getElementById('root')
);
// src/component.ts

import * as React from 'react';
import { withStyles } from 'material-ui/styles';
import theme from './theme';

export default withStyles({
  root: {
    margin: theme.spacing.unit * 3,
  },
})(...);

This is particularly relevant to TypeScript for 2 reasons:

Simply exporting a global theme and then importing it where you need it rather than using callbacks everywhere magically makes all these problems melt away.

It seems like the advantage of using a callback is that it makes your components more modular; you can ship them as a library and others can use them within any MuiThemeProvider. But in many cases people aren't making reusable libraries, they're making an app, in which case it seems like it's fine to just reference a global theme.

@pelotom pelotom added discussion support: question Community support but can be turned into an improvement labels Nov 22, 2017
@pelotom pelotom changed the title Design question: why use a callback to obtain the theme in withStyles? Discussion: are callbacks to withStyles always the best way to obtain the theme? Nov 22, 2017
@stunaz
Copy link
Contributor

stunaz commented Nov 22, 2017

Building an app here. So no need for withStyles with callback.

@TheHolyWaffle
Copy link
Contributor

TheHolyWaffle commented Nov 23, 2017

I don't agree, mostly due to the following missing feature #7633 (passing props as a parameter to the callback).

Having a callback with the props of the component as arguments is something that will definitely enhance the readability of styling of components. As of right now, this is not yet the case, so the workaround is to make use of inline styling. Which is not the best solution, as we miss out on all the great jss plugin features such as auto-prefixer for example.

For clarification, I'm also building an app, but that doesn't mean I don't want to create my own re-usable components.

Edit: If it would be possible to pass both the props and theme to the callbacks of the individual css properties (such as react-jss) then remark becomes invalid. For example:

const styles = {
  button: {
    background: props => props.color
  }
}

@pelotom
Copy link
Member Author

pelotom commented Nov 23, 2017

@TheHolyWaffle I agree that it would be useful to use callbacks for making use of props in styles... this discussion is specifically about the need for callbacks to obtain the theme.

@sebald
Copy link
Member

sebald commented Nov 24, 2017

@TheHolyWaffle I guess this is a bit off topic, but anyway 🙂 The API your're describing is what styled-components, glamorous and emotion are doing. AFAIK jss decided to it differently.

Basically, you create all CSS classes beforehand and apply them later on based on props via helpers like classnames. Here is an example from the <Button>:

https://github.com/mui-org/material-ui/blob/865fb416f62940bff4de8b8f4b4bc5f3884c70c7/src/Button/Button.js#L252-L268


@pelotom Had the same discussion at my company multiple times. If you're only consuming MUI standard components, there is no downside of your mentioned approach. At least none I can think off. If is only relevant for "3rd party" MUI components, like https://github.com/dmtrKovalenko/material-ui-pickers.

@rosskevin
Copy link
Member

rosskevin commented Nov 25, 2017

I'm still getting my bearings with typescript, but I wanted to add that we have a few libraries that use withStyles (just like the mui uses it), then a composite app that uses those libraries and mui. Any contemplated solution should allow for a similar pattern of use.

Are you suggesting that the theme: Theme properties in the callback would no longer be accessible in this manner?

@pelotom
Copy link
Member Author

pelotom commented Nov 25, 2017

Are you suggesting that the theme: Theme properties in the callback would no longer be accessible in this manner?

Not at all! I'm not actually suggesting anything should change about the library... if you're developing a reusable library of components, which it looks like this is an example of, then the callback method definitely seems necessary.

@rosskevin
Copy link
Member

rosskevin commented Nov 26, 2017

FYI - so searchers may easier find this issue.

I ran into this with a stateless functional component (gist):

Error:(9, 3) TS2345:Argument of type '({ palette, shadows, transitions }: { direction: "ltr" | "rtl"; palette: Palette; typography: Typ...' is not assignable to parameter of type 'Record<"root" | "scrolled" | "avatar" | "content" | "contentSecondary", Partial<CSSProperties>> |...'.
  Type '({ palette, shadows, transitions }: { direction: "ltr" | "rtl"; palette: Palette; typography: Typ...' is not assignable to type 'StyleRulesCallback<"root" | "scrolled" | "avatar" | "content" | "contentSecondary">'.
    Type '{ root: { display: string; overflow: string; zIndex: number; backgroundColor: string; transition:...' is not assignable to type 'Record<"root" | "scrolled" | "avatar" | "content" | "contentSecondary", Partial<CSSProperties>>'.
      Types of property 'root' are incompatible.
        Type '{ display: string; overflow: string; zIndex: number; backgroundColor: string; transition: string; }' is not assignable to type 'Partial<CSSProperties>'.
          Types of property 'overflow' are incompatible.
            Type 'string' is not assignable to type '"initial" | "inherit" | "unset" | "auto" | "scroll" | "hidden" | "visible" | undefined'.

which implies the inference failure. The fix being declaring an explicit ClassKey and parameterizing my call to withStyles

type ClassKey = 'root' | 'scrolled' | 'avatar' | 'content' | 'contentSecondary'
const decorate = withStyles<ClassKey>(

Thanks for the guidance @pelotom

@mbrookes
Copy link
Member

@rosskevin / @pelotom Would this be worth adding to the docs (Typescript guide)?

@pelotom
Copy link
Member Author

pelotom commented Nov 27, 2017

@mbrookes yes, I'll try to add something when I get a chance.

@oliviertassinari
Copy link
Member

#7633 starts to be referenced by a lot of people. I will see if I can make it happen. As it was linked here. I'm eager to know if we can improve the DX and the performance with such feature.

is there any advantage to using withStyles with a callback, vs. exporting your theme from a module and then importing it everywhere that you want to use it?

@pelotom At work, we have been relying on the theme callback for most of our styles. The rationale is simple. We are lazy. We find it simpler to add one argument than to add one import.
Ok, it's not the best argument 🤔 . If you don't need the live theme update, or the theme nesting feature (I don't, and most people don't), then yes, you can do what you prefer :).

@pelotom
Copy link
Member Author

pelotom commented Dec 4, 2017

At work, we have been relying on the theme callback for most of our styles. The rationale is simple. We are lazy. We find it simpler to add one argument than to add one import.

I'm lazy too! In TypeScript, it auto-imports when you start to type theme 🙂

@Bnaya
Copy link

Bnaya commented Dec 12, 2017

In that case, that we don't pass the theme via the context,
i would take that one step forward.
Another issue with TS and withStyles, is that, withStyles returns a component that expects classes prop ts-wise, even tho withStyles supply it. (ts current limitation)

What about:

export default function withStyles<ClassKey extends string>(
  style: StyleRules<ClassKey> | StyleRulesCallback<ClassKey>,
  options?: WithStylesOptions
): <P>(
  classes: WithStyles<ClassKey>
) => React.ComponentType<P>;


withStyles({root:...})((classes) => {
  return function MyComponent() {
    return <div className={classes.root} >MyComponent</div>
  }
})

// or
withStyles({root:...}, (classes) => {
  return function MyComponent() {
    return <div className={classes.root} >MyComponent</div>
  }
})

@Bnaya
Copy link

Bnaya commented Dec 13, 2017

Or, why not just:

const classes = withStyles({root:...});

@pelotom
Copy link
Member Author

pelotom commented Dec 13, 2017

Great questions... I've never really understood the need for the indirection of passing classes through the props. What do you think @oliviertassinari?

Another option that has been discussed is render props.

@rosskevin
Copy link
Member

rosskevin commented Dec 13, 2017

A render callback component (I prefer using children as a function, not an unnecessarily verbose extra render prop) is something we should convert WithStyles to. We can then offer an additional HOC (the current withStyles signature) easily that simply calls the render callback component.

I recently switched my patterns to render callbacks, and while it has been great and works for most, it doesn't always work best for every scenario. Nonetheless I believe the render callback component should have the implementation, with the hoc being a convenience wrapper. react-i18next (<I18n/> and translate) is a good example this.

@pelotom
Copy link
Member Author

pelotom commented Dec 13, 2017

Yeah, I'm a big fan of using children as a render prop. Are you thinking something like

class MyComponent extends React.Component<Props> {
  render() {
    return (
      <WithStyles styles={styles}>
        {classes =>
          <...>
        }
      </WithStyles>
    )
  }
}

?

@rosskevin
Copy link
Member

Yes, where current args styles and options are props on WithStyles. I haven't though much about the WithStyles name, open to other ideas.

@pelotom
Copy link
Member Author

pelotom commented Dec 13, 2017

Maybe just Styled if we're bikeshedding names 😄

@pelotom
Copy link
Member Author

pelotom commented Dec 13, 2017

This gives a natural way to accomplish a feature many have been asking for: styles that depend on props. Just move the definition of styles inside the component.

class MyComponent extends React.Component<Props> {
  render() {
    return (
      <Styled styles={this.styles} options={options}>
        {classes =>
          <...>
        }
      </Styled>
    )
  }

  styles = {
    // ... can depend on `this.props`
  }
}

@rosskevin
Copy link
Member

I agree with using Styled plus keeping withStyles name for the HOC.

@Bnaya
Copy link

Bnaya commented Jan 9, 2018

I have another iteration
It gives the class names by inferr, supports the them via context, expose type that you can use separately. (See the style: typeof Styled.style)

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(style: 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);
      }),
    );
  };

  Object.defineProperty(createdStyled, "style", {
    get() {
      throw new Error("style prop is here only for TypeScript inference. use typeof Styled.style for the type");
    },
  });

  // style is just for pulling the types
  return createdStyled as typeof createdStyled & { style: WithStyles<ClassKey> };
}

How to use:

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

const Styled = createStyled(styles);

function InnerComponent(props: { title: string; style: typeof Styled.style }) {
  return <div className={props.style.classes.root}>{props.title}</div>;
}

function OutterComponent(props: { title: string }) {
  return (
    <Styled>
      {s => {
        return <InnerComponent {...props} style={s}/>;
      }}
    </Styled>
  );
}

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 10, 2018

@Bnaya 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(): styled and Styled. 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?

@oliviertassinari oliviertassinari self-assigned this Jan 20, 2018
@oliviertassinari
Copy link
Member

I'm gonna document the two styling API alternatives.

@Bnaya
Copy link

Bnaya commented Jan 21, 2018

I must point out that Conditional types are on the way, and will be with us in typescript 2.8
That will allow you to create HOC that omits some of the props of the component it wraps
microsoft/TypeScript#21316

@caub
Copy link
Contributor

caub commented Feb 23, 2018

Should theme data remain simple constants, agnostic to CSS property keys? (only defining their possible values)

or is it still good practice to put full classes in it like that:

export const theme = createMuiTheme({
  palette: {
    primary: blue,
  },
  card: { // some custom reusable classes
    header: {
      background: '...',
      // ...
      '&::before': {
        // ....
      }
    },
    progress: {
      height: 64, 
      // ...
    }
  }
});

then in my components:

const styles = ({ card: { header, progress }, palette }) => ({
  header,
  progress,
  otherProp: {
    color: palette.primary[600],
    // ...
  }
});

export default withStyles(styles)(Component)

@ghost ghost mentioned this issue Feb 15, 2021
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion support: question Community support but can be turned into an improvement typescript
Projects
None yet
Development

No branches or pull requests

9 participants