SASS Bootstrap allows us to configure theme or branding variables that affect all components (e.g. Primary Color or Link Color). When we isolate our styles inside React components we lose the ability to theme them. To get round this we can put our theme variables on the context of our app and then read them from individual components in order to make styling decisions. Recompose allows us to create the helper functions we need to do this yielding concise, functional declarative UI components. These components will respond in real time to changes in the theme.
The next problem we're going to address is theming or branding. When we place our styles inside our React components, they're encapsulated. This means we don't really have access to them. So what happens when we want to override them with global style variables?
In Sass, with Bootstrap, we have all these different kind of branding variables, and we're able to change things like the brand, primary color, link colors, and things like that, and these changes will be reflected right the way across our UI.
Back in our React button project, I've created my theme file, and I've added some theme variables in there. I've divided them into the various types of variables we have. We've got colors. We've got numbers. We've got strings.
In colors, we've got the key color, which is what will be in the background. We've got the text light color. We've got the button radius, and we've got the font family, which we're going to use for our UI. This would be shared across all our UIs. We may have buttons, tabs, and all these kind of things once our full UI was finished.
The next problem we have is, "How do we get our theme down into our button component?" I'm going to import my theme here. Probably the most obvious thing to do is to bring it in as a prop. But this is going to give us a problem, because we're going to have hundreds of components in our application, and we don't want to be passing our theme down from every component to its child to get hold of them.
The other thing we could do would be to import our theme into every file, but then we couldn't change it dynamically. Luckily, React has a solution to this, and it's called Context. Context is a bit like a component state, except that it can be broadcast down to all of a component's descendents -- a bit like listening to a radio station. These descending components have to tune in to get the exact data that they want.
The first thing we need to do is to define the child context types. Here, we're saying that our theme is an object. The next thing we need to do is to define the value of the property theme on the context, and so we use the getChild Context. We return an object and we say that theme equals this.props.theme. We're setting it to the value that's coming in from the component props. The reason that we do this is that this is going to make automatic updates work.
Next, we need a way of updating our component props, and the best way to do this is to wrap it in a stateful component. We can use Recompose to do this. I'm importing Compose, withState, and withHandlers from Recompose.
We create our Enhance function, and the first thing we're going to place in there is our higher-order component withState. What this does is it wraps our app component with another component that has state. On that state, it's going to have a theme property. It's going to actually pass that theme property on the state down to be a property on the app component which is underneath it, which is wraping.
It's also going to pass down a function that we can use to update that state, and that's going to be called update theme. Here we're naming that update theme, and then we also pass in a default value, which is going to be our theme, which we have imported here from my theme.
Next, we're going to need to wrap our export in Enhance and we can get rid of this theme from here. Now to get hold of an update theme in our app component, and I'll just destructure them out. Next, for debugging purposes, I'm going to render out my theme variables, and I want to create a form that I can use to edit them, so I'm going to add an input in here.
What's happening here? We've got a normal input, and we're taking in the value from our theme.color.keycolor. Then we've got this onChange function, which is calling our update function, which was coming from down here in this withState update theme. Then, we're spreading our new property into the appropriate place in this theme object.
Now, let's test that. You can see, as I type in here, everything is updating down here. For the sake of time, I'm going to paste in inputs for all of our other theme variables. In order for my button to get hold of my theme variables, I'm going to create a higher order component that takes them off the context and makes them accessible to my button.
I'm going go go into my HOCs file. I'm putting it in the HOCs file, because lots of my components are going to want these theme variables. I'm going to get hold of getContext from Recompose, and propTypes from React. Then I'll create higher-order component getTheme, and this is going to fish the theme property off the context.
Now I'm going to create a higher-order component called theme style, and it accepts a mapThemeToStyle function as an argument. What mapThemeToStyle does is it maps the theme and the props onto a new style. Notice, we're also extracting the style from the component properties, and then we're passing that in afterwards to the style array.
This means that any styles which are passed in from the outer component will override our theme styles. If you're not using Radium, you can have the same effect by replacing this array with an object and spreading in the properties.
Also note that all of this is contained in a mapProps function, so this is mapping the current props onto the props, which are coming in from outside, spreading those on, and then placing the new style in there, as well. Finally, we can go back in our button component, and we'll import theme style.
Now, let's create our mapThemeToStyle function. First of all, let's destructure color, number, and string from our theme object. Now we'll set the background color to be color.keycolor, the text color to be color.textLight, and the border radius to be number.borderRadius, and font family to be mainFontFamily. Fix a couple of errors here, and I need to add in getTheme, so I can get the theme off the context.
Now we can add getTheme to the top of our higher-order component stack, and we can also add our theme style here, and pass in our mapThemeToStyle function. Something is broken, because we've lost the border radius on these corners. This should be button radius.
Now we've got our rounded corners back, and I should be able to change the size of them using this control here. You can see they go up and down, and I should be able to change text color light, so now it's yellow, now it's green. The key color, it won't respond when I change it, and that's because we are overriding it in our button component. Then, let's try this font sans serif. Yay! Comic sans.
Now I'm going to try changing the background color, and I'm going to try and demonstrate the precedence that we have here. I'm going to change this style here to purple. This style has the lowest precedence, so you can see that I've got this addStyle here, and addStyle is below themeStyle in the higher-order component stack. This means that addStyle, these styles, have a lower precedence than these styles.
Back in my app.js, I've got my button instance, and you can see that I'm overriding the background color of this button using the style prop. Now, let's delete that, save, and the button color goes back to the color, which is coming in from the theme, which is this one here. When I change the theme, you can see that the button color updates.
I'm back now in my button component, and I'm in my mapThemeToStyle function. I'm going to make this background color conditional upon something actually being entered in this box. If nothing is entered, then it's going to revert to the default style.
I'm going to select this and use spread to make that conditional. What that's saying, basically, if key color exists, then spread in this. If key color doesn't exist, then spread in an empty object, which will mean there's no background color in this object here. I'm going to save that, so it's gone back to the blue color here. If I change that, it goes to green. If I delete this, then it goes to purple, which is the default color in here.
That may have seemed like a lot of work, but the end result is a really declarative component. We have our mapThemeToStyle function, and this defines how our theme, and also our props, can affect our styles. It allows us to define a set of default styles inside our component, and it gives us the ability to override those styles from outside, by passing down styles as props.
From Enhance function, we can see which styles have precedence over other styles. We're also building up a library of higher-order components, which we can apply to our future UI elements.
To recap, we wanted to be able to override styles on individual component instances. We also wanted to be able to define theme or branding variables that we could use for things like changing the primary color of our UI with Sass and Bootstrap. This required a recompile.
By putting our theme variables on the context, and connecting to them via Recompose, we can have themes that can be updated in real time by our clients. We have isolated, yet fully overrideable styles.