vitalets/react-native-extended-stylesheet

Questions about theming

brunolemos opened this issue · 18 comments

  • How to change theme at runtime?
    For example, the app loaded EStyleSheet.build(DarkTheme) in the index.js. How to change to Light Theme? Calling build again does not seem to work.
  • How to use a different theme per component? For example:
<View>
    <Button theme='dark'>Dark Button</Button>
    <Button theme='light'>Light Button</Button>
<View>

I have same use case.

  • Provide a user option in my app to select a dark or light theme.
  • When users selects a new theme it should be rendered immediately.
  • All child components will be rendered using the new theme.

I have spent days trying to find a solution.

Theme support feels like it should be built into react native core.

Any suggestions please?

@esutton have you tried this?

Calling build again does not seem to work.

@brunolemos does it produce some errors or just not working silently?

Calling a build() anytime during runtime, throws no error and still uses the old theme that was used by build() on app start.
How to make this work?

@AnuraagBasu may I ask you to make a simple full example for me to get in context?

@vitalets This is what I'm trying to achieve in my index file.

import React, {Component} from 'react';
import {
  Text,
  View,
  TouchableOpacity,
  AppRegistry
} from 'react-native';

import EStyleSheet from 'react-native-extended-stylesheet';

class TestApp extends Component {
  constructor (props) {
    super(props);

    this.state = {
      selectedTheme: 'classic'
    };

    EStyleSheet.build(Themes.classic);
  }

  render () {
    return (
      <View style={[Styles.bodyBackground, {flex: 1, justifyContent: 'center', alignItems: 'center'}]}>
        <TouchableOpacity activeOpacity={0.8}
                          onPress={this._changeTheme.bind(this)}
                          style={{
                            justifyContent: 'center',
                            alignItems: 'center',
                            width: 200,
                            height: 50,
                            padding: 20,
                            backgroundColor: '#efe'
                          }}>
          <Text style={{color: '#000'}}>Change Background</Text>
        </TouchableOpacity>
      </View>
    );
  }

  _changeTheme () {
    var changeThemeTo = 'classic';
    if (this.state.selectedTheme == 'classic') {
      changeThemeTo = 'winter';
    }

    EStyleSheet.build(Themes[changeThemeTo]);

    this.setState({
      selectedTheme: changeThemeTo
    });
  }
}

const Themes = {
  classic: {
    color1: '#301631'
  },
  winter: {
    color1: '#145E8B'
  }
};

const Styles = EStyleSheet.create({
  bodyBackground: {
    backgroundColor: '$color1'
  }
});

AppRegistry.registerComponent('TestApp', () => TestApp);`

I'm expecting the background color of the View to change on click of the TochableOpacity .

@vitalets I'm still trying to figure out how to make this work. Any thoughts on this?

@AnuraagBasu sorry for delay..
I've got you point. Currently dynamic theming is not possible, but I think it can be done.

It does not work now because EStyleSheet clears list or registered stylesheets after build:
https://github.com/vitalets/react-native-extended-stylesheet/blob/master/src/api.js#L94

You can try to remove that behavior and play with it.
And also keep storing stylesheets registered after build. Now they are not stored:
https://github.com/vitalets/react-native-extended-stylesheet/blob/master/src/api.js#L36

But the main problem here that I see is how to re-render all react components after theme change? In your example with single component you just call setState but imagine whole application with many components using styles. How to notify them that they should get re-rendered?
I would appreciate your ideas on that.

I have dynamic themes working using redux. I am a JS newbie and redux is magic to me and I do not understand the internals of how it works.

This is not a recommendation. It is only what I have been able to get to work for myself. Suggestions are welcome.

  1. User can select "light" or "dark" theme from a menu
  2. I have a reducer that sets theme and calls getThemeStyleSheet
    case types.SETTHEMENAME:
    console.log('SETTHEMENAME =', action.value);
    settingWriteString('themeName', action.value);
    return {
      ...state,
      themeName: action.value,
      themeStyle: getThemeStyleSheet(action.value),
    };
  1. getThemeStyleSheet returns a styleSheetObject
// Using https://material.google.com/style/color.html#color-themes
function getThemeStyleSheet(currentTheme) {
  const theme = themes[currentTheme];

  const styleSheetObject = {
    themeName: currentTheme,
    colorPrimary: theme.colorPrimary,
    colorPrimaryDark: theme.colorPrimaryDark,
    textColor: theme.textColor,
    textColorInverse: theme.textColorInverse,
    hyperlink: {
      color: theme.hyperlinkColor,
    },
    appBar: {
      backgroundColor: theme.colorPrimary,
    },
   };
  return styleSheetObject;
}
  1. My dark and light themes.

File: dark.js

module.exports = {
  colorPrimary: '#40AE49',
  colorPrimaryDark: '#308136',
  colorAccent: '#ae40a5',

  textColor: '#fff',
  textColorInverse: '#25292D',
  windowBackground: '#25292D',

  hyperlinkColor: '#FFFF11',

  menuIconColor: '#97ca3d',

File: light.js

export default {
  colorPrimary: '#40AE49',
  colorPrimaryDark: '#308136',
  colorAccent: '#ae40a5',

  textColor: '#000',
  textColorInverse: '#fff',
  windowBackground: '#fff',

  hyperlinkColor: '#0000EE',

  menuIconColor: '#000',

I pass themeStyle around the app as a prop.

File:MyScene.js

  render() {
    const themeStyle = this.props.state.themeStyle;
      <Text style={themeStyle.textPrimary} >
  1. Ugly "magic code" - I had to make a "parent" for every scene I wanted to change theme dynamically.

This "magic code" mapStateToProps results in dynamic theme changes triggering render of all tab scenes, menus, etc.

File: ViewProfileParent.js

import React, { Component } from 'react';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import * as appActions from '../actions/appActions';

import ViewProfile from './ViewProfile';

class ViewProfileParent extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    console.log('****** ViewProfileParent::render ViewProfile ****');
    return (
      <ViewProfile {...this.props} />
    );
  }
}

// mapStoreStateToComponentProps would be a more descriptive name for mapStateToProps
function mapStateToProps(state) {
  return {
    state: state.appState,
  };
}
function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Object.assign({}, appActions), dispatch),
  }
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ViewProfileParent)

-Ed

@esutton yes, if you store styles in component state - it will re-render on style change. It's good idea!
I think the best approach is to pre-build both themes on start and toggle afterwards. It also seems better for performance reason.

To pre-build themes you should not pass theme variables to EStyleSheet.build(). Instead just pass it as local variables to EStyleSheet.create():
Example:

// light.js
export default {
  colorPrimary: 'white'
}

// dark.js
export default {
  colorPrimary: 'black'
}

// app.js
import themeLight from './light.js';
import themeDark from './dark.js';

const srcStyles = {
  text: {
    color: '$colorPrimary'
  },
}

const themeStyles = {
  light: EStyleSheet.create(Object.assign({}, themeLight, srcStyles)),
  dark: EStyleSheet.create(Object.assign({}, themeDark, srcStyles)),
}

function getThemeStyleSheet(currentTheme) {
  return themeStyles[currentTheme];
}

I think this could be implemented in extended-stylesheet in future as more friendly API:

EStyleSheet.addTheme('light', variables);
EStyleSheet.addTheme('dark', variables);

const styles = EStyleSheet.create({
  text: {
    color: '$colorPrimary'
  },
}, {
  themes: ['light', 'dark']
});

@vitalets Thanks for guiding me in the right direction. It does work for me now.
I like your suggestion for the other implementation as a future enhancement in the library. That would really be a much cleaner way to achieve theming.

@AnuraagBasu ok, thanks for the feedback!

@vitalets
I've been trying to get dynamic theming to work using your above suggestion but this line returns a null object.
EStyleSheet.create(Object.assign({}, themeLight, srcStyles))
Do you have any idea why it would do that and not actually create the style sheet ?

@arthur-tse7 could you show a bit more code?

@vitalets I've abandoned dynamic theming for now, but when I get back to it, I'll reply again !

@vitalets

I have the same problem as @arthur-tse7 's. The object created by EStyleSheet.create(Object.assign({}, themeLight, srcStyles)) is empty.

app.js

import EStyleSheet from 'react-native-extended-stylesheet';
import {themeLight} from './styles/light';
import {themeDark} from './styles/dark';
const srcStyles = {
    text: {
        color: '$colorPrimary'
    },
};
const themeStyles = {
    light: EStyleSheet.create(Object.assign({}, themeLight, srcStyles)),
    dark: EStyleSheet.create(Object.assign({}, themeDark, srcStyles)),
};
function getThemeStyleSheet(currentTheme) {
    return themeStyles[currentTheme];
}
console.log( getThemeStyleSheet('light'));

dark.js

export const themeDark = {
    colorPrimary: 'black'
}

light.js

export const themeLight= {
    colorPrimary: 'white'
}

The console returns an empty {}.

Dependencies:
"react-native": "0.41.2",
"react-native-datepicker": "^1.4.4",
"react-native-extended-stylesheet": "^0.3.1",
"react-native-navigation": "^2.0.0-experimental.225",

Finally added working examples with static and dynamic themes:
https://github.com/vitalets/react-native-extended-stylesheet#theming

@stonecold123

@esutton yes, if you store styles in component state - it will re-render on style change. It's good idea!
I think the best approach is to pre-build both themes on start and toggle afterwards. It also seems better for performance reason.

To pre-build themes you should not pass theme variables to EStyleSheet.build(). Instead just pass it as local variables to EStyleSheet.create():
Example:

// light.js
export default {
  colorPrimary: 'white'
}

// dark.js
export default {
  colorPrimary: 'black'
}

// app.js
import themeLight from './light.js';
import themeDark from './dark.js';

const srcStyles = {
  text: {
    color: '$colorPrimary'
  },
}

const themeStyles = {
  light: EStyleSheet.create(Object.assign({}, themeLight, srcStyles)),
  dark: EStyleSheet.create(Object.assign({}, themeDark, srcStyles)),
}

function getThemeStyleSheet(currentTheme) {
  return themeStyles[currentTheme];
}

I think this could be implemented in extended-stylesheet in future as more friendly API:

EStyleSheet.addTheme('light', variables);
EStyleSheet.addTheme('dark', variables);

const styles = EStyleSheet.create({
  text: {
    color: '$colorPrimary'
  },
}, {
  themes: ['light', 'dark']
});

Is this code still valid? I need a static theme scheme