Styled-jsx: Passing style to child component without using global styles

Created on 7 Aug 2017  路  11Comments  路  Source: vercel/styled-jsx

I've got a child component, let's call it Link.

This is the code for that:

import React from 'react';
import Link from 'next/link';

export default class CustomLink extends React.Component {
  render() {
    if (this.props.onClick) {
      return (
        <span className={this.props.className} onClick={this.props.onClick}>
          {this.props.children}
        </span>
      );
    }

    return (
      <Link
        href={this.props.href}
        params={this.props.params}
        as={this.props.as}>
        <a className={this.props.className}>{this.props.children}</a>
      </Link>
    );
  }
}

Now say that I import that component within another component, called Header

The code for that is:

import Link from './Link';

export default class Header extends React.Component {
  render() {
    return (
      <div className="root">
        <style jsx>{`
          .link {
            background-color: #fff;
            border-bottom: 1px solid #eee;
            color: #222;
          }
          `}
        </style>
       <Link className="link">Click me</Link>
    );
  }
}

However, unless I use <style jsx global>, then unfortunately the style doesn't get passed to the child component.

I think a related issue is #197, but I haven't seen any outcome or update on that issue since June.

Is there a solution to fixing this issue?

discussion enhancement

Most helpful comment

@colinmeinke that doesn't really work well, because what if Link comes from some npm module (e.g. a UI library), and I don't have control over it?

That's the use case where this becomes a real problem.

All 11 comments

So it seems if I do this:

          .root .link {
            display: inline-block;
            border-bottom: 2px solid transparent;
            padding: 0 15px;
            text-decoration: none;
            text-transform: uppercase;
          }

          .root .link,
          .root .link:active,
          .root .link:visited {
            color: #222;
          }

          .root .link.active,
          .root .link:hover {
            border-bottom: 2px solid #222;
            color: #222;
          }

It seems to work.

Alternatively I can also do:

          :global(.link) {
            display: inline-block;
            border-bottom: 2px solid transparent;
            padding: 0 15px;
            text-decoration: none;
            text-transform: uppercase;
          }

          :global(.link),
          :global(.link:active),
          :global(.link:visited) {
            color: #222;
          }

          :global(.link.active),
          :global(.link:hover) {
            border-bottom: 2px solid #222;
            color: #222;
          }

from the parent component's styles.

What is the _right_ thing to do here?

I think the the modifier/variant styles should be applied to the Link component, not the component in which they are used.

import React from 'react';
import Link from 'next/link';

export default class CustomLink extends React.Component {
  render() {
    if (this.props.onClick) {
      return (
        <span className={this.props.className} onClick={this.props.onClick}>
          {this.props.children}
        </span>
      );
    }

    return (
      <Link
        href={this.props.href}
        params={this.props.params}
        as={this.props.as}>
        <a className={this.props.className}>{this.props.children}</a>
        <style jsx>{`
          .link {
            background-color: #fff;
            border-bottom: 1px solid #eee;
            color: #222;
          }
        `}</style>
      </Link>
    );
  }
}

use:

import Link from './Link';

export default class Header extends React.Component {
  render() {
    return (
      <div>
        <Link className="link">Click me</Link>
      </div>
    );
  }
}

@colinmeinke that doesn't really work well, because what if Link comes from some npm module (e.g. a UI library), and I don't have control over it?

That's the use case where this becomes a real problem.

what if Link comes from some npm module (e.g. a UI library), and I don't have control over it?

That's the main point, we'd have to figure out a way to control styles from the parent because passing a prop (eg. className) is an half baked solution that won't work all the times.

As for now the best way to do this is to use .someSelectorInTheParentComponent > :global() from within the parent component eg.

<div className="root">
    <Link />
    <style jsx>{`
        .root > :global(a) { font-size: 60px }
     `}</style>
</div>

I've found a better workaround for this issue.

function HighlightedLink(props) {
  const {
    theme,
    children,
    ...otherProps,
  } = props;

  /**
   * `scope` element is needed to properly parse `style` element
   * and it could be any DOM element you want.
   */
  const scope = resolveScopedStyles((
    <scope>
      <style jsx>{`
        .link {
          background: ${theme.background};
        }
      `}</style>
    </scope>
  ));

  return (
    <Link
      {...otherProps}
      className={scope.wrapClassNames('link')}
    >
      {children}
      <scope.styles />
    </Link>
  );
}

function resolveScopedStyles(scope) {
  return {
    className: scope.props.className,
    styles: () => scope.props.children,
    wrapClassNames: (...classNames) => [scope.props.className, ...classNames].filter(Boolean).join(' '),
  };
}

@a-ignatov-parc it might add some runtime overhead but it is genius! :D

might add some runtime overhead

Yeah. But usually you shouldn't use child combinator selectors. Child component's markup is its private area and may change at any time.

But usually you shouldn't use child combinator selectors

Agreed, unfortunately that's the only way to style 3rd parties components that don't accept a className.

By the way styles could be just scope.props.children and used like this {scope.styles}. You could expose Styles: () => scope.props.children and styles: scope.props.children :)

I've been thinking to implement automatic className propagation for components and support this syntax:

<Link className="link">
  {children}
  <style jsx>{`
     .link { background: ${theme.background} }
  `}</style>
</Link>

Although I like the explicitness of your solution since on can do anything with the className and styles.
Today I am giving a talk about styled-jsx if it is ok with you I would like to use your example (with credits) in my presentation.

You could expose Styles: () => scope.props.children and styles: scope.props.children

Sounds good 馃憤

I would like to use your example (with credits) in my presentation.

Sure 馃槈

Agreed, unfortunately that's the only way to style 3rd parties components that don't accept a className.

True. But it's better to choose another component/lib than apply such implicit styling. No one likes magic that can break your UI.

Added @a-ignatov-parc workaround to the docs:

https://github.com/zeit/styled-jsx#styling-third-parties--child-components-from-the-parent

Was this page helpful?
0 / 5 - 0 ratings