In our last post we developed a simple CSS-in-JS library, but it was missing an important feature - supporting dynamic styles. We will patch that in this post.
End result
Dynamic styles are more useful when we are building components. In this post, we will use React as our component library. Here’s how our library may be used in production:
Dynamic class names
This is the simplest approach that requires making the smallest change to
our little library. Our approach will be to generate the class name based on the
props being passed, then supply that to our react component. This means
that Block
under the hood should look like this:
|
|
A simple implementation based on what we did previously may look like this
|
|
Done? Well, this works, but there’s a big issue here - every time
staticStyle
is executed, a new css class name and a set of corresponding
rules are created! We can fix this by doing a bit of memoization.
|
|
This fixes the performance issue we talked about above and is probably a good enough MVP, but there are still other issues.
Issues
Unbounded number of class names
In our toy example there can only be 4 possible sets of styles. But it’s easy to construct something that can have an infinite number of possible props:
|
|
One solution is to use inline styles for these properties.
No more descendant selectors
With staticStyle
we can do something like this
|
|
This is not trivial with dynamicStyle
. One solution here is to use CSS
variables, like this:
|
|
Generating CSS code during build time is hard
Unless we can iterate through all possible values of props
at build time,
we won’t be able to generate all CSS rules. We can also settle with some
‘default props’ and just generate that single one, but that’s not ideal.
Large generated stylesheet
To some extend this can be an issue with our previous static-only library as
well, but having dynamic styles makes the issue more pronounced. In our
example, even though border
stays the same, each new class will repeat
this property. We can split out the static styles into its own
staticStyle
call, but that’s a bit awkward.
One solution here is to use the atomic css pattern, detailed below.
Optimization - Atomic CSS
What we are doing doesn’t exactly match atomic css , but the end result is similar.
The apporach is to programmatically break down each ‘class’ into multiple classes, each with only one css property. Then, we will similarly memoize the class names for each rules. This way, if a CSS property-value is repeated across multiple style definitions, all of those styles will share the same class name.
For example, suppose we render our example Block
twice, once with
{ mode: "light"; size: "big" }
and once with
{ mode: "dark"; size: "big" }
. This will create the following css rules:
|
|
We will then pass to the first call the following classnames:
"c1 c2 c3"
, and to the second call: "c1 c4 c3"
.
Implementation
Firstly, we will write a helper to split out each style declaration into multiple atomic declarations.
|
|
And example output:
> isolateDeclaration({
... ":hover": {
..... color: "green"
..... },
... color: "red"
... })
[ { ':hover': { color: 'green' } }, { color: 'red' } ]
Next we will update our staticStyle
and dynamicStyle
method.
|
|
We updated the function interface to return an array instead of a single string, because we will now most likely return multiple class names. Moreover, we move the memoization into the class state since we want each staticStyle call to share the memoization.
Interestingly, we also reverted our optimization for dynamicStyle
since memoization
is now done at the staticStyle
level, so we no longer need to worry about
creating a new rule per call.
React factory
Finally let’s wrap this up by implementing what we have promised at the start of this post - a React factory to create styled components.
|
|
Are dynamic styles really necessary?
Probably not. After all, without CSS-in-JS, the usual pattern is to have some classes defined in css files, then reference them in the UI code. We can replicate that easily by generating these classes with static styles. This allows for writing CSS code in the same file/dependency tree as the JavaScript code, and the dynamic-ness is handled separately by the JavaScript UI logic instead of encapsulated in the CSS-in-JS magic. That being said, dynamic styles does provide convenience and some sort of elegance (depending on who you ask).
Other ideas
We haven’t really dealt with the other issues mentioned above other than the issue of
generating a large stylesheet. Our API for the react component is also a bit awkward:
if we intend the component to only have static styles, we have to pass something like
() => CssLikeObject
. We can work around this by trying to allow both (T) => CssLikeObject
and CssLikeObject
, but can we come up with something more elegant than that?
In our next post , we will resolve all of these issues by switching to a different approach - using CSS variables.