Writing API for React components, part 1: do not create conflicting props
We write API for React components, part 2: let's name the behavior, not the way of interaction
We write API for React components, part 3: the order of props is important
')
Writing an API for React components, part 4: beware of Apropacalypse!
Writing an API for React components, part 5: just use composition
We write API for React components, part 6: we create communication between components
Let's start with a simple React component that displays an anchor tag:
<Link href="sid.studio">Click me</Link> // : <a href="sid.studio" class="link">Click me</a>
Here is what the component code looks like:
const Link = props => { return ( <a href={props.href} className="link"> {props.children} </a> ) }
We also want to be able to add html-attributes such as id
, target
, title
, data-attr
, etc. to the element.
Since there are many HTML attributes, we can simply pass all the props, and add those we need (for example className
)
(Note: you do not have to pass attributes that you come up with for this component that are not in the HTML specification)
In this case, you can simply use className
const Link = props => { /* (spread ), ( ) */ return <a {...props} className="link" /> }
This is where it gets interesting.
It seems that everything is fine when someone sends an id
or target
:
<Link href="sid.studio" id="my-link">Click me</Link> // : <a href="sid.studio" id="my-link" class="link">Click me</a>
but what happens when someone passes className
?
<Link href="sid.studio" className="red-link">Click me</Link> // : <a href="sid.studio" class="link">Click me</a>
Well, nothing happened. React completely ignored the user class. Let's go back to the function:
const Link = props => { return <a {...props} className="link" /> }
Well, let's imagine how this ...props
compiles, the code above is equivalent to this:
const Link = props => { return ( <a href="sid.studio" className="red-link" className="link" > Click me </a> ) }
See the conflict? There are two className
. How does React handle this?
Well, React does nothing. Babel does!
Remember that JSX "produces" React.createElement
. Props are converted to an object and passed as an argument. Objects do not support duplicate keys, so the second className
will overwrite the first.
const Link = props => { return React.createElement( 'a', { className: 'link', href: 'sid.studio' }, 'Click me' ) }
OK, now that we know about the problem, how do we solve it?
It is useful to understand that this error arose because of a name conflict, and this can happen with any prop, and not just with className
. So the decision depends on the behavior you want to implement.
There are three possible scenarios:
Let's disassemble them one by one.
This is the behavior that you usually expect from other attributes - id
, title
, etc.
We often see the test id
setting in cosmos (the design system I'm working on). Each component gets a default data-test-id
; sometimes developers want to attach their own test ID instead to denote a specific use.
Here is one of these uses:
const Breadcrumb = () => ( <div className="breadcrumb" data-test-id="breadcrumb"> <Link data-test-id="breadcrumb.link">Home</Link> <Link data-test-id="breadcrumb.link">Parent</Link> <Link data-test-id="breadcrumb.link">Page</Link> </div> )
Breadcrumb
uses the link, but you want to be able to use it in tests with a more specific data-test-id
. This is washed away.
In most cases, custom props should take precedence over props by default.
In practice, this means that the props should go first by default, and then {...props}
to override them.
const Link = props => { return ( <a className="link" data-test-id="link" {...props} /> ) }
Remember that the second occurrence of the data-test-id
(from props) will override the first (by default). Therefore, when a developer attaches his own data-test-id
or className
, he overrides the default one:
1. <Link href="sid.studio">Click me</Link> 2. <Link href="sid.studio" data-test-id="breadcrumb.link">Click me</Link> // : 1. <a class="link" href="sid.studio" data-test-id="link">Click me</a> 2. <a class="link" href="sid.studio" data-test-id="breadcrumb.link">Click me</a>
We can do the same for className
:
<Link href="sid.studio" className="red-link">Click me</Link> // : <a href="sid.studio" class="red-link" data-test-id="link">Click me</a>
It looks rather strange, I'm not sure that we should allow this! Let's talk about it further.
Suppose we do not want developers to change the appearance (via className
), but we are not opposed to changing other props, such as id
, data-test-id
, etc.
We can do this by ordering the order of our attributes:
const Link = props => { return ( <a data-test-id="link" {...props} className="link" /> ) }
Remember that the attribute on the right will override the attribute on the left. Thus, everything before {...props}
can be redefined, but everything after it cannot be redefined.
To simplify the work of developers, you can display a warning that you cannot specify your className
.
I like to create my own props type checks for this:
Link.PropTypes = { className: function(props) { if (props.className) { return new Error( ` className Link, ` ) } } }
I have a video that talks about checking user types of props , in case you're wondering how to write them.
Now, when the developer tries to override the className
, it will not work, and the developer will receive a warning.
<Link href="sid.studio" className="red-link">Click me</Link> // : <a href="sid.studio" class="link">Click me</a>
: : className Link,
Honestly, I had to use this template only once or twice. Usually you trust the developer using your component.
Which leads us to sharing.
This is perhaps the most common use case for classes.
<Link href="sid.studio" className="underline">Click me</Link> // : <a href="sid.studio" class="link underline">Click me</a>
The implementation looks like this:
const Link = props => { /* className */ const { className, otherProps } = props /* */ const classes = 'link ' + className return ( <a data-test-id="link" className={classes} {...otherProps} /* */ /> ) }
This template is also useful for accepting event handlers (for example, onClick
) for a component that already has them.
<Switch onClick={value => console.log(value)} />
Here is the implementation of this component:
class Switch extends React.Component { state = { enabled: false } onToggle = event => { /* */ this.setState({ enabled: !this.state.enabled }) /* */ if (typeof this.props.onClick === 'function') { this.props.onClick(event, this.state.enabled) } } render() { /* ️ */ return <div class="toggler" onClick={this.onToggle} /> } }
There is another way to avoid name clashes in event handlers, I described it in the Write API for React components, part 2: give names to behavior, not methods of interaction .
For each scenario, you can use different approaches.
Source: https://habr.com/ru/post/459380/
All Articles