nikgraf / future-react-ui

A Playground to investigate third-party React UI Lib Architecture

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feedback

kof opened this issue · comments

  1. class names based only +1

  2. ui lib should ship without a theme +1

  3. ui components should be requirable separately (we often just need the f..g button))))) +1

  4. react-themeable +1

  5. We need to distinguish between a theme and system styles. While changing colors, background images and rounded corners should not have much impact on transitions, changing sizes and margins will. There is a need for clear definition of what is a theme so that it doesn't breaks components.

  6. How to pass the theme ... ? We can support 2 approaches at the same time.

    • A preferable approach - UI component wrapped into a theme component. This requires a user to wrap each component he is going to use. For large projects this overhead is not a problem, it allows also more project specific adjustments. For e.g. one could set a global default size for a button component within the wrapper.
      @useSheet(styles)
      export default class ThemedButton extends Component {
        static defaultProps = {
            width: ...,
            height: ...
        }
        render() {
          const {classes} = this.props.sheet
          return <Button theme={classes} {...this.props}  />
        }
      }

    In user land you import your themed button: import Button from '../ui/Button'

    • Optionally a theme over context can be supported by the UI lib. This is a dirtier but faster way. It can be used as a fallback if no theme prop has been passed. A ui lib name for e.g. "elemental" should be used as a convention for the namespace on the context object to avoid collisions.
        // This function can be part of react-theamable
        function getTheme(namespace, element) {
          let {theme} = element.props
          if (theme) return theme
          const options = element.context[namespace]
          if (options) theme = options.theme
          return theme
        }
    
    
        export default class Button extends Component {
          render() {
            const theme = getTheme('elemental', this)
            return (
              <div className={theme.button}>
              </div>
            )
          }
        }
  7. I am against using static props/class props. They don't play nicely with higher order components.

I've been working on an internal lib at work for awhile now and came to many similar conclusions.

Agree 100% on classnames as the base for styles. Just makes the most sense, as its the most generic/least specific, and should work well with solutions like react-themeable. The only problem I've been about to think of is that any change in class name is a breaking change, but thats obvious. I've been tinkering around with an idea to create a "deprecated soon" warning for css classnames being use on a page that will soon be changed (think react's console warning messages each time you upgrade react, but for css classnames).

React-themeable looks good also, though I've been have trouble wrapping my head using Jed Watson's ClassNames repo with it and without using JSX, but I'm sure that a failing on my part and not react-themeable.

Agree on self contained.

Shipping without a theme...I think the core lib should have as minimal styles as possible, but there are a few components that just need styling in them even at a base level. Think of things like Date Pickers and whatnot. If the lib is using a theme utility though, a must would be to have a default theme in that utility that ships with the lib. I think most people won't create their own theme from scratch, but rather build on top of/hack up whatever theme options are given to them by the lib author.

Oleg's number 5 is a good point. Which could be controlled with a theme component. Just restrict specifically what props/styles the theme component uses.

super invaluable feedback @kof @NogsMPLS

I like the idea of warning users for breaking changes in classnames. There could be utility function (only running in dev mode) which logs out errors in case the provided theme or global theme uses a certain function. Very much like React's dev experience.

@NogsMPLS I agree that a default theme should come with the library. I was thinking about dev have to make one import+function call to inject it. Thoughts?

I like the idea about number 5, but couldn't wrap my head around how this should work. When we shipped the initial version of Belle it came with a default theme + a Bootstrap theme. The same components were so different and hard to imagine what other themes might change or have in common. One experiment could be to create 4-5 themes and then see what they have in common.

Regarding number 5: maybe a theme should consist of 2 parts: classes and options. Options contains all constants needed to configure a component to work well with given classes. In case of JSS, I would create a stylesheet which would use the constants to create the JSON. And those constants will be passed to the component as a part of the theme, so that they can be used for calculations.

I like that @kof, that sounds reasonable.

Provide a default theme, but make it easily overridable. Maybe have the default theme in the component itself, and make it customizable via props?

  1. class names based only +1
    There is quite good article about this subject:
    http://jamesknelson.com/why-you-shouldnt-style-with-javascript/

  2. ui lib should ship without a theme +1

  3. ui components should be requirable separately +1

  4. We need to distinguish between a theme and core styles. + 1

4-6-7 - not sure right now.

Although I agree on all the points, I think that consumers (i.e. other UI developers) do not really care if some UI lib uses classes or inline styles underneath as long as you can override the look and feel of it easily. The good news is projects like https://github.com/markdalgleish/react-themeable or https://github.com/andreypopp/rethemeable allow using any available method of styling components. I think this is really important and should not be discouraged because of personal preferences. I'm not a fan of inline styles myself, but saying all libs should follow one approach will alienate it for some developers.

I really like the approach of VicotryJS in describing how to write components using its architecture: https://github.com/FormidableLabs/builder-victory-component/blob/master/dev/CONTRIBUTING.md#component-style

To me, the ultimate goal of such project would be an API description + a set of tools (docgen, linters, etc) to allow other UI-lib authors integrate into their existing projects with ease. It seems there are too many different UI-libs for React being developed ATM and it's almost impossible to combine or exchange them without rewriting lots of own code.

Regarding themes: I think the most idiomatic way to do would context. But I have too few experience with any of these techniques. So, I'd rather leave the decision to smart people :)

also it's good idea to separate validation. E.g. at my current project i'm using: https://github.com/davidkpiano/react-redux-form
and quite happy with it.

After letting your feedback sink in for a little while in addition to talking to some people I figured this Factory Pattern approach might do the job.

@kof the factory function to create a theme element also contains defaultProps next to theme which allows us to set sensible defaults.

@okonet love you idea with the linters. this we we could ensure certain quality & a good style.

Regarding point 5 (theme vs core style). I think this then could be an implementation detail of a styled UI kit package which the core (unstyled) components don't have any related code.

I'm curious why you think 😄

@andreypopp I wonder what you think about it …

@nikgraf 👍 I love that, especially with CSS Modules, i.e.

const customTheme = {
  questionMark: styles.questionMark,
  visibleContent: styles.visibleContent,
};

@mxstbr in this case you don't even have to do the mapping. You can directly pass the styles into the theme. Checkout https://github.com/nikgraf/future-react-ui/blob/master/app/components/HintCssModulesExample/index.js

For our internal UI component library we use an approach described as React Stylesheet.

Actually this conceptually similar to factory pattern described by @nikgraf but with more convention around how factory is implemented and how mechanism for theme overrides works.

The idea is that a component which is meant to be styled in a few different ways define a stylesheet as a mapping from names to React components, for example:

class Button extends React.Component {

  static stylesheet = {
    Root: 'button',
    Caption: 'div',
  }

  render() {
    let {caption} = this.props
    let {Root, Icon} = this.constructor.stylesheet
    return (
      <Root>
        <Caption>{caption}</Caption>
      </Root>
    )
  }
}

So that render() is defined not in terms of concrete DOM components but in terms of components from a stylesheet.

This way we are agnostic of what underlying mechanism is used for styling DOM elements.

Then React Stylesheet defines a function which allows to derive a styled component from an original one.

For example if we want to style out <Button /> component with classes from some global stylesheet:

import {style} from 'react-stylesheet'

let SuccessButton = style(Button, {
  Root(props) {
    return <button {...props} className="ui-Button--success" />
  },
  Caption(props) {
    return <div {...props} className="ui-Button__caption--success" />
  }
})

This works with CSS Modules and inline styles as well, with JSS or good old SASS.

Nested overrides

The mechanics is a little more sophisticated though, the style(Component, stylesheetOverride) applies in a nested fashion so you can restyle components deep inside easily.

What if you have a <Dialog /> component and it has a button:

class Dialog extends React.Component {

  static stylesheet = {
    Root: 'div',
    Button: Button, // defined above
  }

  render() {
    let {Root, Button} = this.constructor.stylesheet
    return <Root>{children}<Button /></Root>
  }
}

Now style() can be used to produce a new styled dialog variant <Dialog /> along with a new styled <Button /> inside:

let StyledDialog = style(Dialog, {
  Button: {
    Root: (props) => <button {...props} style={{background: 'red'}} />
  }
})

Now <StyledDialog /> has a button inside with red background.

Granular styling API

Components can define a granular interface for styling themselves by defining static style(override) method. That way you can control how and what exactly could be overridden:

class Button extends React.Component {
  static stylesheet = {
    Root: 'button',
    Caption: 'div',
    Icon: null
  }

  static style({icon, ...override}) {
    // allow easy icon override
    return style(this, {
      Icon: (props) => <span {...props} className={`glyphicon-${icon}`} />,
      ...override
    })
  }

  render() {
    let {Root, Icon, Caption} = this.props
    return ...
  }
}

Then style function will call into this static method when override happens:

let ButtonWithMakeIcon = style(Button, {icon: 'plus'})

Regarding propagating theme through the React's context mechanism.

Name-clashing. It can be solved by using not string keys but Symbol keys as theme keys:

class Button extends React.Component {
  static themeKey = Symbol('Button.themeKey')
  render() {
    // resolve theme using `this.constructor.themeKey` from context.
  }
}

Then

let Theme = {
  [Button.themeKey]: { 
    // theme for Button
  }
}

That's how Rethemeable lib works.

But we found that we usually don't want to use context for theming as we don't have such requirements as switching themes in an app via some sort of theme switcher. Instead we use React Stylesheet to predefine themed components for some particular app and then use them directly.

As for DOM styling mechanism we use a CSS in JS approach.

React Stylesheet allows to configure fallback for DOM components so we style DOM components directly when styling composite components. The example with <Button /> above would look like:

import {style} from 'react-stylesheet'
import {style as styleDOM} from 'react-don-stylesheet'

let SuccessButton = style(Button, {
  Root: { // this calls into `styleDOM` actually as `Root` is defined as `button` DOM component
    backgroundColor: 'green',
    ...
  }
}, {styleDOM: styleDOM})

The unstyled lib + theme ui kit idea is interesting, because it seems like the consensus would be that we end up with 3 "types" of components.

I'm going to take some terminology for this article: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.pbtswjdvc:

We would have a Presentational/Dumb Component for markup, a Styled Component that sets base styles and accepts themeing props, and a Container/Smart component in an actual App to handle state and business logic.

Which is interesting, because now it starts to look very much like the old school 'separation of concerns' pattern where we keep markup, styles and logic all separated, but still all inside React.

+1 on using Symbol when using context ( @andreypopp )
+1 component factory ( @nikgraf )

At the end, both approaches mean that receiver component needs to take a theme and theme options via props or context.

We can provide a function implemented by react-themeable which will pick the theme from props first, if not found from context. This way component creators don't need to care about how to get the theme.

A theme for one component should bring a map of classes and options object. This will allow to have static cachable styles as well as options to configure component to match those styles.

The need for both context and props theme passing I can see: flexibility to change a theme for a specific component at any node in react's render tree.

  • Use context based theme passing if you want just one theme
  • Use props if you want different themes everywhere (who knows why)
  • Use context as a base theme passing mechanism, but props for specific variations in edge cases.

I'm a bit worried/scared of introducing context for theming. If we solely rely on props then components theming has this functional character: "calling a function f twice with the same value for an argument x will produce the same result f(x) each time". Context behave like side-effects influencing the styling and also a bit unpredictable from where things are coming. In addition this opens up the question if we merge the theme or overwrite the whole thing.

In addition I wonder how much value it has to theme a whole section differently. Applying a few theme props might do the job as well or do I miss real world use-cases (that I haven't experienced yet)

Thoughts?

How about use custom-tag and structure of the selector to define style:

  • Component use custom-tag as the root, each component has a unique tag.
  • Using custom-tag as css-wrap to ensure localization in a component.
  • Do not use class attribute in inner component.

Component is the basic semantic unit, we do not care the internal implementation, so we do not need class attribute in inner component. Using structure of the selector can make html structure first. If you can not define your style clearly, maybe you need to split some children components.

@ustccjw can you provide an example? (would help a lot)

@nikgraf regarding context, I understand the fear that its possible to redefine theme at any point via context and it might be hard to find out where. For this case we can have a restriction that a theme over context for the same components set, can be defined only once. Once its defined, overwrites over context are not allowed, do it via props. So you can be sure where to find it.

I think the Idea for using context is all about to provide a great UX from the beginning with a minimal setup.

I have a feeling that contexts suits as well for theming as it suites great for localization and I just don't get a fear that a lot of people have about them.
Common misconception that contexts are implicit. Context is as explicit as props, maybe even more - if you don't declare smtn in contextTypes, you won't get it. Props will be passed even if you don't declare them in propsTypes.
Problem with the context that it is just another mechanism to pass data and a lot of people just don't know about it or don't know how it works and as the result smth unexpected for them happens.
@nikgraf As for functional greatness of props passing approach - you always can wrap your clean functional components in some dirty context based stuff, thats how redux connect works for example.

@Lapanoid the question is what context gives you for theming what cannot be implementing using props?
I see context being useful only for UI with theme switcher where user can dynamically change theme in application runtime (this is needed in i18n and this is why context for i18n is probably an ok idea). Usually you don't see such theme switcher in apps though and UI is assembled through pre-made themed components.

@andreypopp simplicity of the setup. You can wrap an entire application into one Theme component.

@kof I feel like this is only theoretically simpler. In practice you'd need to worry you have enough things configurable/styled via context vs. props.

@andreypopp Basically to theme whatever Component I want in any position in the tree.

<Root>
  <Component1>
    <Component2/>
  </Component1>
</Root>

I need to pass props here through Component1 to reach Component2, with context approach Component1 is totally unaware of this. Problem is getting much worse with project and team scale.

@nikgraf

export default class ArticleList extends React.Component {
    render() {
        const { articles } = this.props
        const articleComponents = articles.map(article =>
            <Card key={article.number} article={article} />
        )
        return (
            <ideal-articlelist>
                {articleComponents}
            </ideal-articlelist>
        )
    }
}
ideal-articlelist {
    display: block;
    ideal-card {
        max-width: 800px;
        margin: 40px auto;
    }
}
export default class Card extends React.Component {
    render() {
        const { article } = this.props
        return (
            <ideal-card>
                <Markdown content={article.introduction} />
                <Link to={`/articles/${article.number}/`}>View More</Link>
            </ideal-card>
        )
    }
}

More info can check: https://github.com/ustccjw/tech-blog/blob/master/client%2Fcomponent%2Farticle-list%2Findex.jsx

@Lapanoid my assumption is that you don't need to theme a component tree in an app but build one using themed components.

@andreypopp I would probably do that too, however building simple things would feel like big overhead ...

@ustccjw this is a very opinionated solution

@andreypopp my assumption that theming should be centralized on app level, that does not mean that I want to change it dynamically.

One of the basic ideas @nikgraf outlined was a Global Theming Utility. I really like this idea and think it's important for adoption. I think the context idea laid out by @kof seems like a decent way to do this.

Shipping themed components is great for a 'base theme' for any given library, but I'm having trouble on figuring out how to change specific theme aspects without having to define a custom theme for every view.

To help explain what I mean:

//import a base Styled Success Button from some lib Theme UI  Kit
import SuccessButton from 'someLib/SuccessButton';

//provide a custom theme for this view
const customTheme = {
  background: 'custom-green-class',
};

export default (props) => {
  return (
    <div>
      {/* Default themed component from Library Theme UI Kit */}
      <SuccessButton />
      {/* Overwriting the theme locally for this case */}
      <SuccessButton theme={customTheme}/>
    </div>
  );
};

The above illustrates someone that wants to use some library's UI Theme Kit Success Button, but the default green doesn't match their brand, so they override the second button with new .custom-green-class.

The capability to do this is great, but I'm wondering with this pattern - that without a context implementation like @kof suggested - how would I change ALL SuccessButtons ever used in my app to use .custom-green-class.

How do people change all SuccessButtons at the same time?

  • Instead of using a pre-packaged UI Theme Kit, would they fork a UI Theme Kit, and edit it to make it their own tailor made UI Theme Kit for their product's uses? Doesn't seem beginner friendly
  • Create a new SuccessButton component in their app that wraps the imported SuccessButton? This seems tedious and a bit redundant.
  • Just import the library's SuccessButton, find the default class associated with it, and globally override that class name with a CSS file, thus side-stepping all of this? Easy to do, but real messy in the long run.
  • With context you'd have a somewhat 'central' area for global theme changes. If I want my component to have different box-shadow stuff, I don't want to have to describe that every single time I use a card.

Most people don't use libraries exactly how they come out-of-the-box and will want to change things easily, and probably globally. That's actually one idea I really like with current Belle's configuration options. It's not classname based, but being able to just glance at that outline, and easily globally override all card styles really adds a lot of immediate value in my eyes.

..Or perhaps my thinking on this subject is too old school and influenced by old css non-component theme override thoughts. Maybe forking a UI Kit for any given project to create your own styled components really is the best way to handle this.

UI libraries shouldn't have to concern themselves with styling strategies or rebuilding common primitives. And their UI components should not be handling styles different to those from components exported by other libraries or defined in the app. Therefore, I'd suggest UI libraries exporting React components eventually want to be built upon common primitives and APIs, like those React Native provides. This is what I started react-native-web for at Twitter.

Yes, well, common primitives for styling on the web is basically class names.

What makes you think that?

.. we are stepping at the flaming ground ..

Well classes is not the only way, there are also lots of other selectors. But what do you use most of the time? Sounds like a common primitive if I understand correctly @necolas.

Classes is perfect because they are not opinionated about how static CSS rules have to be created.

By 'primitives' I meant UI components like View, Text, Touchable.

@kof Compared with css in js,i do not think this solution is very opinionated.

@Lapanoid I have 3 issues with context that come to my mind. Let's look at this example structure:

App.js

import Main from './Main';
import Navigation from './Navigation';
import Theme from 'belle/Theme';
import lightTheme from '../themes/lightTheme';

export default () => ((
  <div>
    <Theme theme={lightTheme}>
      <Navigation />
      <Main />
    </Theme>
  </div>
));

Navigation.js

import Link from 'router/Link';
import Theme from 'belle/Theme';
import darkTheme from '../themes/darkTheme';

export default () => ((
  <div>
    <Theme theme={darkTheme}>
      <Link to="/dashboard">Dashboard</Link>
      <Link to="/about">About</Link>
    </Theme>
  </div>
));

Main.js

import Revenue from './Revenue';
import DatePicker from 'belle/Datepicker';
import Button from 'belle/Button';
import Theme from 'belle/Theme';
import dashboardTheme from '../themes/dashboardTheme';
import datePickerTheme from '../themes/datePickerTheme';

export default () => ((
  <div>
    <Theme theme={dashboardTheme}>
      <DatePicker theme={datePickerTheme} />
      <Button>Save</Button>
      <Revenue />
    </Theme>
  </div>
));

The issues I see here:

  • I need to look at the whole Component-Tree to just understand which Theme is applied to let's say Revenue. As @kof suggested we could disallow to overwrite already defined components. I can imagine this as pretty annoying if you plan to style a subsection differently.
  • The theme could come from context or props. Having both options makes it harder to parse & understand the code (to me).
  • If you take out the Theme component in App.js you have no idea which sub-components are affected without an understanding of the whole Tree top to bottom.

Let's see how a factory approach would do in comparison:

App.js

import Main from './Main';
import Navigation from './Navigation';

export default () => ((
  <div>
    <Navigation />
    <Main />
  </div>
));

Link.js

import Link from 'router/Link';
import themeComponent from 'ui-utils/themeComponent';
import linkTheme = '../themes/linkTheme';

export default themeComponent(Link, linkTheme);

Navigation.js

import Link from './Link';

export default () => ((
  <div>
    <Link to="/dashboard">Dashboard</Link>
    <Link to="/about">About</Link>
  </div>
));

DashBoardButton.js

import Button from 'belle/Button';
import themeComponent from 'ui-utils/themeComponent';
import dashboardButtonTheme = '../themes/dashboardButtonTheme';

export default themeComponent(Button, dashboardButtonTheme);

Main.js

import Revenue from './Revenue';
import DatePicker from 'belle/Datepicker';
import DashBoardButton from './DashBoardButton';
import datePickerTheme from '../themes/datePickerTheme';

export default () => ((
  <div>
    <DatePicker theme={datePickerTheme} />
    <DashBoardButton>Save</DashBoardButton>
    <Revenue />
  </div>
));

Unfortunately more lines of code … I wonder if this is more sane or just annoying overhead to you and how limiting it is for developer experience. /cc @kof @andreypopp

@necolas Cool stuff @ react-web-native. I like the idea!

I agree with you on primitives. Though this proposal is targeted at UI libraries more for Ratings, Toogles, Datepickers or Tag Inputs. I think one of the most undervalued effect of React is the encapsulation React provides. You can have great UX for Desktop, Mobile & even Screen readers encapsulated in these components and just drop them into your App.

I believe components should be available unstyled so people can apply their own branding. Still for prototyping or building internal tools you often want to have an already styled version of all these components to move fast. I think Bootstrap showed us the way here :)

@NogsMPLS I think a lot about the concerns you mentioned. That's why we introduced the global theming for Belle. I also thought about doing it in a similar why in this experiment, but in the end it was a trick/workaround leveraging references. To me it felt people need to know a lot about JS references to avoid messing up here. https://github.com/nikgraf/future-react-ui/blob/master/ui-lib/StaticProperty/Hint.js

How do you feel about the example above? Is it that bad?

We maybe could build a helper function that generates your own themed UI lib with the factory approach:

import Button from 'belle/Button';
import buttonTheme from './buttonTheme';
import successButtonTheme from './successButtonTheme';
import Select from 'material/DatePicker';
import selectTheme from './selectTheme';
import generateUILib from './belle/generateUILib';

const elements = {
  Button: {
    component: Button,
    theme: buttonTheme
  },
  SuccessButton: {
    component: Button,
    theme: successButtonTheme
  },
  Select: {
    component: Select,
    theme: selectTheme
  }
}

export default generateUILib(elements);

@NogsMPLS does this qualify as understandable & useful Global Theming Utility?

@nikgraf we could also go with factory function first and keep the context option in mind for later.

@nikgraf @kof I don't think that components should in any way prescribe whether we have to use classes or inline styles or whatever. It's not their responsibility to decide that for the user. Best case: they play nice with whatever approach a consumer chooses to use.

+1 to @necolas's concept of building up of primitives, if I'm understanding it right. className and style totally make sense for the most base-level components, and for basic overriding. But if you also think of components (the specific children to be rendered, what @andreypopp has called stylesheets it looks like) as part of the "theme" of a given component, you can start to build things up nicely.

For example:

const MyButton = factory(Button, { 
  classNames: { ... },
  styles: { ... },
})

const MyDropdown = factory(Dropdown, {
  classNames: { ... },
  styles: { ... },
  components: {
    Button: MyButton,
  }
})

And then the functionality that lets you compose child components is the exact same code paths that let end developers override specific pieces of a UI component at will. (Eg. they could pass in their own Input to a UISearch that has custom functionality... completely limitless in terms of control.)

Using the primitives over just className and styles actually allows you to have re-use at the Javascript level, including behaviors, etc. Instead of at the super-awkward CSS level, where you can continue to use CSS Modules that keep everything completely isolated.

From what I've learned so far, I currently see "themes" as being made up of:

  • classNames — a map from component name to a className property.
  • styles — a map from component name to a style property.
  • components — a map from component name to a Component itself.
  • variants — a map of variant names to nested maps that obey the theme interface.

For variants, I think that kind of thing is critical to getting away from things like "primary, secondary, danger buttons", which are not semantic at all, and pretty opinionated. Instead, let the theme author provide specific variant names, and if they are enabled on the component, additional classNames, styles and even components.

Variants could be then converted to being as simple as:

<Button primary danger />

But instead of being hard-coded into the base UI Kit, they are able to be defined as whatever by the developer who builds the theme.

@ianstormtaylor I ended up with theme-standard which is essentially {...options, classes, styles, themes} = theme, themes is a map of {componentName: theme}, to style inner components, I think you call it components

I like your variants Idea, I think it should be added to the theme {variantName: theme} = variants.

@kof nice!

I think after adding variants, the only thing missing is the ability to compose actual components instead of just being able to style them with className and style properties. I think this is crucial to have a UI Kit that isn't frustrating to add/remove functionality from, not just to add/remove styles from.

Passing in components actually lets you override the Icon used inside of a Button for example. This way you can define low-level components, and then pass them in to higher-level components.