Detuned Detuned - 11 days ago 8
React JSX Question

Is there a way to pass down props to a component implicitly?

I'd like to be able to set a global theme (set of variables) for all of my components to inherit from and extend their default variables. For example, I have a

button
component that has default styles (inline CSS) that refer to a set of variables such as
primaryColor
, ... and I'd like to be able to update these variables easily wherever I use these components without needing to explicitly pass them to the components.

For example, I'd like the following behavior where I could either (1) wrap them in a component and have
primaryColor
cascade down to each
Button
component or (2) export this component in a higher order component and feed update the props, etc... however, I cannot get ay of these methods to work. Perhaps, there is a better way or ...

(1)

render() {
return (
<Theme variables={{ 'primaryColor': 'red' }}>
<Button />
<SecondButton />
<ThirdButton />
</Theme>
);
}


(2)

render() {
return (
<div>
<Button />
<SecondButton />
<ThirdButton />
</div>
);
}

export default Theme(SampleComponent)


This method works, as it's, obviously, passed down explicitly to each component:

render() {
return (
<div>
<Button variables={{ 'primaryColor': 'red' }} />
<SecondButton variables={{ 'primaryColor': 'red' }} />
<ThirdButton variables={{ 'primaryColor': 'red' }} />
</div>
);
}

Answer

I can see a few ways that you might accomplish this:

Extending child props

Limited, but allows passing 'extra' props to direct children of a component:

import React, { Component, Children } from 'react';

class Theme extends Component {
  getChildren () {
    const { children, variables } = this.props;
    // Clone the child components and extend their props
    return Children.map(children, (child) => React.cloneElement(child, {
      variables
    }));
  }

  render () {
    return Children.only(this.getChildren());
  }
}

// Example
<Theme variables={{ 'primaryColor': 'red' }}>
  <Button />
  <SecondButton />
  <ThirdButton />
</Theme>

React context

The easiest way to pass variables to any part of the React tree is by using context as described in the React documentation (this exact use case too!):

// Context provider
class ThemeProvider extends Component {
  getChildContext() {
    return {
      theme: this.props.variables
    };
  }

  render() {
    return Children.only(this.props.children);
  }
}

ThemeProvider.childContextTypes = {
  theme: PropTypes.object.isRequired
};

// HOC
function Theme(WrappedComponent) {
  class ThemeWrapper extends Component {

    render() {
      return <WrappedComponent { ...this.props } />;
    }
  }

  ThemeWrapper.contextTypes = {
    theme: PropTypes.object.isRequired
  };

  return ThemeWrapper;
};

// Using the HOC
class Button extends Component {
  render () {
    return <button style={{ color: this.context.theme.primaryColor }} />;
  }
}

const ThemeButton = Theme(Button);

// Example
<ThemeProvider variables={{ 'primaryColor': 'red' }}>
  <div>
    <ThemeButton />
  </div>
</ThemeProvider>

Redux store

As you are using Redux, you could wrap each component that needs to be themed in the connect HOC and store your theme information in the store state. This is a simple way to share data and avoids the complexities of context:

class Button extends Component {
  render () {
    return <button style={{ color: this.props.theme.primaryColor }} />
  }
}

const ConnectedButton = connect((state) => ({ theme: state.theme }))(Button);

// Example
// During app setup
store.dispatch(setTheme({ 'primaryColor': 'red' }));

// Later
<div>
  <ConnectedButton />
</div>

Hope this helps.