yandex / reshadow

Markup and styles that feel right
https://reshadow.dev
Other
363 stars 15 forks source link

Dynamic theme #84

Open Danilqa opened 5 years ago

Danilqa commented 5 years ago

Hi! Thank you for your wonderful job! :)

I haven't found approaches for creating a dynamic theme as we've usually done it in Style Component. What do you think about this solution? Does another more correct way exist?

export function ThemeProvider({ theme, children }) {
    return React.createElement(ThemeContext.Provider, { children, value: theme });
};

export function withTheme(component) {
    return props => (
        <ThemeContext.Consumer>
            {theme => React.createElement(component, { ...props, theme })}
        </ThemeContext.Consumer>
    );
}

Each component I wrap by withTheme and get need properties inside it.

lttb commented 5 years ago

Hi @Danilqa, thank you very much for the issue!

It's a very interesting topic, and I hope you will not mind if I try to dive a little deeper into the subject.

There are a couple of ways to apply some styles to the Component:

The first two are well known in the context of React applications, and the last one is something that we've introduced with reshadow (but of course it might be achievable somehow else) and I'll try to explain these parts better.

Component's modifiers

Component's modifiers - is a clear and simple way to implement the styling. We just need to write the Component's behaviour depends on that modifiers, and I won't focus on this approach at the moment (but with reshadow there are also some interesting things).

This is a great way to declare Component's own styling behaviour by design system, but it's important to mention that this approach is not really flexible, because if you need to change something on styling, you need to change Component's styles or change its API (add modifier or change modifier for example).

So, this is the basic idea and it's often combined with others.

Theming

With theme we can "detach" some styling parts from the Component to the theme and make styling more flexible.

There are also some limits, like Theme's API, because if you need to style something that's not supported by theme, you need to extend theme's API or, again, change Component's code.

It is also might be tricky sometimes to design the Theme API the best way.

Anyway, it looks like the most common way to apply different styles in React application, Context Theming Variables is a popular kind of implementation.

Context Theming Variables

This approach is very popular on styled-components or jss way. You can use any theme context provider for React, for example cssinjs/theming, or the custom implementation that was mentioned.

In fact, with reshadow it looks similar.

In example, with theming (on the codesandbox):

import {ThemeProvider} from 'theming';

const theme = {bg: 'white', fg: 'black'};

<ThemeProvider theme={theme}>
  <App />
</ThemeProvider>
import styled from 'reshadow'
import {withTheme} from 'theming'; 

const Button = ({theme, children, ...props}) => styled`
  button {
    color: ${theme.fg};
    background-color: ${theme.bg};
    border: 2px solid;
    padding: 0.25em 1em;
  }
`(
  <button {...props}>
    {children}
  </button>
)

export default withTheme(Button)

Dynamic values will fallback to the css custom properties and styles will be still extractable to the separate css files during the build time, without runtime styles parsing.

BTW, @reshadow/styled has theming support out of the box with the same API and approaches that styled-components has, but it's an experimental package.

Important to note, that because of css custom properties there is no IE11 support with dynamic values in the tagged template literal.

Context Theme

The previous approach might look natural and classic, but there are also some interesting things that might be nice to consider.

A theme might be not just about some set of different variables for sizes, fonts, colors and so on, but it also affects the style behaviour of the component. Moreover, it might be no real intersection between Component styles in different themes (imagine, if we'd like to have Material and iOS themes on the same core codebase, that have different layouts, animations, property combinations etc.). So we can imagine an example, that's not that easy to describe by theming variables.

But we can adjust the Component to the theme, for example, with styled-components:

Button/index.js

import styled, {css} from 'styled-components'

const themes = {
  light: props => css` /* light theme */ `,
  dark: props => css` /* dark theme */ `,
}

export const Button = styled.button`
  ${props => themes[props.theme]}
`

With reshadow it might be implemented this way:

import {ThemeProvider} from 'theming';

const theme = 'light';

<ThemeProvider theme={theme}>
  <App />
</ThemeProvider>

Button/themes.js

export * as light from './light.css'
export * as dark from './dark.css'

Button/index.js

import styled from 'reshadow'
import {withTheme} from 'theming'; 

import themes from './themes'

const Button = ({theme, children, ...props}) => styled(themes[theme])(
  <button {...props}>{children}</button>
)

export default withTheme(Button)

This is the basic example in an attempt to show an idea, but we can take a step forward and load themes in a lazy way by dynamic imports for example when the current theme is needed.

Thus, we can compose different styles, themes for Components in the isolated ways and share our components independently.

In contrast to styled-components example, css with reshadow is extractable and does not have any runtime processing as in the previous example, and it also supports IE11, because there is no need in css custom properties. It is also easy to style not only Components, but its elements with any attributes and modifiers.

Styling

If we'll take a look on the example with Context Theme, we can notice that our styles were detached from Component to the separate values. Let's take a little next step and inject our styles from props, for example:

Button/index.js

import styled from 'reshadow'

export const Button = ({styles, children, ...props}) => styled(styles)(
  <button {...props}>
    <content>{children}</content>
  </button>
)

With reshadow you can just write your markup and map the Component's state to it without carrying about the styling, and style the Component somewhere outside:

import {css} from 'reshadow'

const styles = css`
  button { ... }
  button[disabled] { ... }
  content { ... }
`

<Button styles={styles} ... />

It is possible to put the styles to DI and achieve the Styles Injection by the context. It is useful for styling components with the same UI logic and markup, but with really different styles, like this:

image

But that's another interesting topic 😄

I hope I've understandably described different dynamic theme approaches with reshadow, please feel free to ask for any clarification or help 🙂

s-kobets commented 4 years ago

How I think. It is not work, because when you calculate hash for class name will be other, if use local style in Button/index.js. Fix me, if I am not right.