cherniavskii.com

Writing reusable React components

April 01, 2019|7 min read

In this article I will share my experience and lessons learned while using React for the last couple of years. You will learn how to write reusable and extensible React components.

lego

Read React docs

I assume, that you have basic knowledge of React. If you haven’t read React docs yet - this is where you should start.

Yeah, I know. You don’t have time to read docs right now. Or maybe you’ve started to read them, but then realized that you already know how to use React, so further reading doesn’t make a lot of sense.

OK, I understand that. I’ve been there. I know that feeling, when you can’t wait to actually start writing something.

But I encourage you to double think about that, because React docs are really great. I recommend to read all the docs, including “Advanced Guides” and “FAQ” sections. This won’t take long, but I bet you’ll learn a lot. Even if you have some experience with React.

Spread rest of props to root element

Whenever you write a component that renders something, it always has a root element. Remember to pass rest of props to that element - this will allow to modify component from outside without changing component itself.

function FancyButton(children, ...props) {  return (
    <button {...props}>      {children}
    </button>
  );
}

Now FancyButton accepts any prop supported by root element (HTML button) - e.g. disabled, type, name etc:

<FancyButton
  onClick={() => alert('Clicked')}  disabled={isLoading}  name="submit">
  Click me!
</FancyButton>

Same technique applies to non-HTML root elements, e.g:

function FancyIconButon(children, iconName, ...props) {  return (
    <FancyButton {...props}>      <Icon name={iconName} />
      {children}
    </FancyButton>
  );
}

Allow to modify root element styles

If component’s root element uses className or styles prop, spreading rest of props to it may lead to resetting its style:

// file.css
.fancy-button {
  background-color: 'orange';
}
.margin-top {
  margin-top: 8px;
}

// FancyButton.jsx
function FancyButton(children, ...props) {
  return (
    // ⚠️ `props.className` will override "fancy-button" class    <button className="fancy-button" {...props}>      {children}
    </button>
  );
}

<FancyButton className="margin-top">Click me!</FancyButton>

In this case, I only want to add margin to button, but it turns out that background color is gone now.

To solve this, you should handle className prop separately. You can use clsx or classnames utils to concat class names:

function FancyButton(children, className, ...props) {  return (
    // ✅ "fancy-button" class is preserved    <button className={clsx('fancy-button', className)} {...props}>      {children}
    </button>
  );
}

It’s even easier in React Native, since style prop can accept an array of styles:

function FancyButton(children, style, ...props) {  return (
    // ✅ `FancyButton` internal styles are preserved    <TouchableOpacity style={[{ backgroundColor: 'orange' }, style]}>      {children}
    </TouchableOpacity>
  )
}

Group props for nested components

Let’s take a FancyIconButon for example. It takes iconName prop and passes it to Icon component, so we can use different icons in different buttons:

function FancyIconButon(children, iconName, ...props) {
  return (
    <FancyButton {...props}>
      <Icon name={iconName} />      {children}
    </FancyButton>
  );
}

But what if we want to slightly modify Icon styles, and maybe pass some additional props to it?

Sure, we can add iconClassName, iconStyles, icon<Whatever> props to FancyIconButton - it’s a good starting point. But often it’s easier and better to group all Icon props into one IconProps prop, so we don’t have to maintain each prop separately:

function FancyIconButon(children, IconProps, ...props) {  return (
    <FancyButton {...props}>
      <Icon {...IconProps} />      {children}
    </FancyButton>
  );
}

// ...

<FancyIconButon
  IconProps={{    name: 'accept',    className: 'icon-styles',  }}>
  Click me!
</FancyIconButon>

Use containment (aka “slots”)

Sometimes props grouping is not enough. What if we want to pass different icon component to our FancyIconButton from example above? It’s not possible right now.

But with containment pattern, we can delegate full control over icon to parent component:

function FancyIconButon(children, icon, ...props) {  return (
    <FancyButton {...props}>
      {icon}      {children}
    </FancyButton>
  );
}

// ...

<FancyIconButton
  icon={<FancyIcon name="accept" />}>
  Click me!
</FancyIconButon>

Use render props

Render props pattern allows to create stateful view-agnostic components. It means, that we can share same logic between components that look (or even behave) differently.

Consider the following simplified example:

class Stepper extends React.Component {
  state = {
    currentStep: 0,
  };

  setStep = (stepIndex) => {
    const { stepCount } = this.props;
    if (stepIndex < 0 || stepIndex >= stepCount) {
      return;
    }
    this.setState({
      currentStep: stepIndex,
    });
  }

  render() {
    const { currentStep } = this.state;
    const { children } = this.props;
    return children({      currentStep,      setStep: this.setStep,    });  }
}

Note, that render method doesn’t actually render anything itself - it just returns what children function returns. This makes Stepper component platform-agnostic and is especially useful when sharing same logic between React and React Native:

// React
<Stepper stepCount={3}>
  {({ currentStep, setStep }) => (
    <div>      <button onClick={() => setStep(currentStep - 1)}>Previous step</button>      <span>Current step is {currentStep}</span>      <button onClick={() => setStep(currentStep + 1)}>Next step</button>    </div>  )}
</Stepper>

// React Native
<Stepper stepCount={3}>
  {({ currentStep, setStep }) => (
    <View>      <Button title="Previous step" onPress={() => setStep(currentStep - 1)} />      <Text>Current step is {currentStep}</Text>      <Button title="Next step" onPress={() => setStep(currentStep + 1)} />    </View>  )}
</Stepper>

Use PropTypes

Describe component API using PropTypes. It’s easy to use and can be helpful in development - you’ll get warnings in console, if propTypes and actual props do not match. Also, it’s some kind of component API documentation - if propTypes are specified, looking at them is usually enough to understand how to use component.

Always define defaultProps for optional (not-required) props. Good default value is the one, which is used most of time.

If prop is optional, but has no meaningful default value - set its default value to null or {}, for example:

See PropTypes docs for more info.

Naming component props

When naming component props, try to find the nearest platform equivalent for it (e.g. HTML) and design your API similarly to platform API.

Some examples:

// ⚠️ Prop name is too specific
<SubmitButton onSubmit={...} />

// ✅ Web - similar to HTML `<button>`
<SubmitButton onClick={...} />

// ✅ React Native - similar to `Button`
<SubmitButton onPress={...} />
// ⚠️ This doesn't look bad, but maybe we can name it better?
<Dialog isOpened={...} />

// ✅ Web - similar to HTML `<dialog>`
<Dialog open={...} />

// ✅ React Native - similar to `Modal`
<Dialog visible={...} />
// ⚠️ Those props names are too specific
<PasswordInput
  password={...}
  onPasswordChange={...}
/>

// ✅ Web - similar to HTML `<input>`
<PasswordInput
  value={...}
  onChange={...}
/>

// ✅ React Native - similar to `TextInput`
<PasswordInput
  value={...}
  onChangeText={...}
/>

The biggest benefit of naming consistency is intuitiveness. It’s easier to get familiar with other components and remember props names.

Note, that it’s not always possible to find component’s platform equivalent. Or sometimes platform’s API is not the best example when it comes to naming. But it’s always worth to draw parallels with platform/third-party libraries.

Naming controlled components props

Most controlled components receive value and handler, which is called when that value changes. It’s a good practice to name those props value and onChange - this way naming will be consistent across different components (or even projects).

This is one of design approaches, used by material-ui. I strongly recommend to check out their component API - that’s a great place for inspiration and learning useful tricks.

Use React Hooks

Get familiar with React Hooks. They are less error-prone and easier to understand, allow to split logic by context instead of splitting logic by lifecycle hooks. This makes reusing logic between components way easier than using HOCs.

Do not rewrite your app to React Hooks. Instead, adopt them incrementally in new components.


Andrew Cherniavskii
Written by Andrew Cherniavskii, Frontend Engineer.