Introducing “transient” props

A reference pattern for unstructured prop filtering in higher-order components.

If you use a higher order component (HOC) library like styled-components, you’ve likely run into the pain of dealing with your props vs unknown props. For s-c, this usually takes the form of props used for styling reasons and the resulting struggle to prevent leakage onto the DOM / React “unknown” prop warnings.

Consider the following:

import styled from "styled-components";
const SomeOtherComponent = props => <div {...props} />;
const Comp = styled(SomeOtherComponent)`
color: ${p => p.color};
`;
<Comp color="blue">Hello world!</Comp>

In the current state of things, the color prop would end up in the DOM as an HTML attribute and React will bark at you to filter it out. While it technically is doing no harm, React is right to bring the issue to your attention since garbage is getting through that has no purpose.

In the case of a simple styled component (e.g. styled.div), color would be filtered out via some code in the library that maintains a list of all known HTML attribute names. This is a bulky piece of functionality and really is a workaround for what I feel is a general limitation of the otherwise fantastic component paradigm.

For many HOCs, this probably is not an issue because their prop API is structured and consists of perhaps one or two dedicated props. However, APIs like styled-components allow consumption of any prop (unstructured), leaving a real conundrum of how to filter things that are real attributes and ones just used for “meta” styling purposes.

Enter transient props, prefixed via $. When a prop is prefixed as transient, that is a hint that it is meant exclusively for the uppermost component layer and should not be passed further down.

import styled from "styled-components";
const SomeOtherComponent = props => <div {...props} />;
const Comp = styled(SomeOtherComponent)`
color: ${p => p.$color};
`;
// $color will not be passed to SomeOtherComponent
<Comp $color="blue">Hello world!</Comp>

If you are an HOC author, you’d simply add a check like this when forwarding props:

const props = {};
for (let key in this.props) {
if (key[0] !== '$') props[key] = this.props[key];
}
React.createElement('whatever', props);

Caveat: this is not a silver bullet. If you happen into an unfortunate scenario where multiple HOCs consume the same prop, there are going to be conflicts and it will require some thoughtful composition. The chief one in this regard is probably the “as” prop, a way of dynamically changing what component child is rendered:

import styled from "styled-components";
const Comp = styled.div`
color: red;
`;
// Now it's a <span> element
<Comp as="span">Hello World!</Comp>

With transient props, it becomes:

import styled from "styled-components";
const Comp = styled.div`
color: red;
`;
// Now it's a <span> element
<Comp $as="span">Hello World!</Comp>

Note that paying attention to $as over as if both are present means that wrapping another component that accepts as will continue to work:

import styled from "styled-components";
const OtherComp = ({ as: asProp, ...props }) => <asProp {...props} onClick={somethingCool} />;
const Comp = styled.div`
color: red;
`;
// Now it's a <span> element with an onClick handler
// that does something cool
<Comp $as={OtherComp} as="span">Hello World!</Comp>

Will this situation happen often? Gosh I hope not. It gets pretty gnarly more than 2 levels deep… but that’s true today as well and we’re improving the 90% use case which I think is an acceptable tradeoff.

So the final algorithm is this:

  1. Do not forward transient props to the next layer ($something).
  2. If a transient and normal version ($something and something) of one of your accepted props are both given, consume the transient version and forward the normal version down.

There’s a PR open to add this capability to styled-components in a minor release, with the final change to remove the old way of prop filtering for the next major. Constructive feedback is appreciated.