Radium y así

Daniel Venegas
KarmaPulse Developers
10 min readSep 30, 2016

Estilos en línea para componentes ReactJS

Hoy platicaremos de una librería con la que he estado jugando hace algún tiempo y a la cual le he tomado cierto cariño, así como un programador se lo tomaría a su taza de cafe.

Cualquier parecido con la realidad es mera coincidencia

Qué es Radium… para qué sirve… y así…

Podemos describir a Radium como un conjunto de herramientas para escribir estilos en línea sin la utilización de CSS tradicional, pensado especialmente para componentes de React.

Pero ¿qué ventajas tenemos al utilizarlo frente al CSS tradicional? ¿En qué panorama podría utilizarlo? En mi opinión, si estás escribiendo React para componentes pequeños, aplicaciones que no requieren grandes implicaciones o aquellas en las que tu componente no vivirá en sitios de terceros que puedan afectar los estilos de tu aplicación, entonces no necesitas utilizar Radium, ya que todo eso lo podrías bien resolver con un CSS tradicional, aunque sabemos que, como buen dev, te aventurarás a jugar con él.

Por el contrario, si estás escribiendo componentes que requieren vivir en sitios de terceros, si necesitas hacer cambios en real time que también se reflejen en su forma de verse o que dependan los estilos de la data que incorporamos a nuestros componentes, e incluso del manejo de multiples temas por componentes, entonces Radium podría ayudarte en este arduo trabajo.

Algunas de las ventajas expresivas que tenemos al trabajar con Radium son la facilidad de operación matemática, la concatenación, las condicionales, expresiones regulares, funciones en javascript, etc. que están a nuestra disposición para llevar nuestros estilos al siguiente nivel; además de pseudo Css, browser states, media queries, keyframes, soporte para ES6 class y un vendor de prefixing para nuestros navegadores.

Es momento de jugar un rato… manos a la obra

Imagina que tienes una aplicación donde se pueden utilizar distintos temas, los cuales en un futuro serán alimentados por un end point, para ello dejaremos preparado el todo el camino.

Para nuestro pequeño demo utilizaremos lo siguiente:
ReactJs, Radium, ES6, babel, browser-sync, webpack, eslint, lodash, npm.
Si no conoces alguna de ellas te invito que sigas nuestro blog donde hablaremos de ellas.

Puedes descargar el siguiente repo en el cual encontrarás dos ramas, una master donde está el demo completo y otra develop donde está el setup para que tú puedas jugar.

Arrancamos…

Comenzamos escribiendo nuestro primer componente para llamar a RootApp.

import React from 'react';
import { render } from 'react-dom';
import RootApp from './root.jsx';const MyApp = () =>
<RootApp />;
render(
<MyApp />,
document.getElementById('root')
);

RootApp será nuestro componente más alto, ahí tendremos los estilos de nuestra implementación a la cual le pasaremos un scopeSelector como prop, que nos sirve para que todos nuestros estilos se apliquen sólo a los que están por debajo de este parámetro. También incluye un reset, el cual renderearemos en el head del documento a través de nuestro ReactDOMServer. también importamos un MainComponent el cual contiene nuestro layout.

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import ResetStyles from '../components/ResetStyles';
import MainComponent from '../components/MainComponent';
import ImplementationStyles from '../components/ImplementationStyles.jsx';
const RootApp = () => {
const styles = ReactDOMServer.renderToStaticMarkup(<ResetStyles />);
document.head.insertAdjacentHTML('beforeEnd', styles);
return (
<div className="app-Module">
<ImplementationStyles scopeSelector=".app-Module" />
<MainComponent />
</div>
);
};
export default RootApp;

Definimos nuestros reset, los cuales haremos componente después. Toma en cuenta que para escribir cada una de las reglas utilizaremos camelCase.

export default {
'html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video': {
margin: 0,
padding: 0,
border: 0,
fontSize: '100%',
font: 'inherit',
verticalAlign: 'baseline',
},
'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section': {
display: 'block'
},
body: {
lineHeight: 1
},
'ol, ul': {
listStyle: 'none'
},
'blockquote, q': {
quotes: 'none'
},
'blockquote:before, blockquote:after, q:before, q:after': {
content: 'none'
},
table: {
borderCollapse: 'collapse',
borderSpacing: 0
}
};

Definimos nuestro componente reset al cual le pasamos las reglas que definimos en el punto anterior y un radiumConfig que se rendereará en los navegadores al pasarle un userAgent.

import React from 'react';
import { Style } from 'radium';
import resetRules from './resetRules';const ResetStyles = () =>
<Style
radiumConfig={{ userAgent: 'all' }}
rules={resetRules}
/>;
export default ResetStyles;

Ahora definiremos nuestros estilos de implementaciones, aquí llega el scopeSelector que enviamos en RootApp y pasa como parámetro a nuestro componente; cambiamos el userAgent para que tome sólo los que necesita por navegador y así hacer más ligera la carga

import React, { PropTypes } from 'react';
import { Style } from 'radium';
const propTypes = {
scopeSelector: PropTypes.string.isRequired
};
const ImplementationStyles = ({ scopeSelector }) => (
<Style
scopeSelector={scopeSelector}
radiumConfig={{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36'
}}
rules={{
'': {
flex: '1',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflowY: 'scroll',
background: 'linear-gradient(45deg, hsla(264, 98%, 46%, 1) 0%, hsla(264, 98%, 46%, 0) 70%),'+
'linear-gradient(135deg, hsla(14, 99%, 45%, 1) 10%, hsla(14, 99%, 45%, 0) 80%),'+
'linear-gradient(225deg, hsla(191, 93%, 42%, 1) 10%, hsla(191, 93%, 42%, 0) 80%),'+
'linear-gradient(315deg, hsla(187, 97%, 47%, 1) 100%, hsla(187, 97%, 47%, 0) 70%);'
}
}}
/>
);
ImplementationStyles.propTypes = propTypes;export default ImplementationStyles;

Ahora escribiremos nuestro componente MainComponent, éste tendrá un state para cambiar entra cada uno de los temas por lo cual mandaremos por props al header una función que cambiará el estado de todos los componentes hijos (header, card) de nuestra aplicación. También definiremos un helper que se encargará de construir nuestros estilos.

import React, { Component } from 'react';
import Header from '../Header';
import Card from '../Card';
import BuilderStyles from '../../helpers/styles/builderStyles.jsx';
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
themeSelect: 'theme1'
};
this.handler = this.handler.bind(this);
this.scopeSelector = 'mainComponents';
}
handler(theme) {
this.setState({
themeSelect: theme
});
}
render() {
const { themeSelect } = this.state;
return (
<div className="mainComponents">
<BuilderStyles
scopeSelector={`.${this.scopeSelector}`}
theme={themeSelect}
/>
<Header handler={this.handler} />
<Card />
</div>
);
}
}
export default MainComponent;

Vamos a definir nuestro tema por default, primero un object de colores y ahora los estilos que utilizaremos en nuestros componentes header y card; nuestras llaves principales son colors y myApp, en la cual tenemos el fontFamily que será para toda nuestra app, así como el header y la card, cada cual con sus objects y estilos que ocuparemos más adelante.

export default () => {
const colorVariables = {
primary: '#ffffff',
secondary: '#000000',
lightPrimary: '#ffffff',
darkSecondary: '#514646',
primary35: 'rgba(195, 195, 195, 0.35)',
theme1: '#dadada',
theme2: '#d4377e',
theme3: '#44b3cc'
};
return {
colors: colorVariables,
myApp: {
fontFamily: '"Roboto Condensed", sans-serif',
header: {
position: 'absolute',
top: 0,
width: '100%',
padding: 15,
borderRadius: '0 3px 0 3px',
margin: '0px',
border: 'none',
background: colorVariables.lightPrimary,
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)',
title: {
color: colorVariables.secondary,
},
themes: {
item: {
width: 30,
height: 30
},
boxShadow: `0px 2px 5px 2px ${colorVariables.primary35}`,
},
},
card: {
width: '50%',
padding: 0,
borderRadius: '5px',
margin: '0px',
border: 'none',
background: colorVariables.primary35,
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)',
header: {
width: '100%',
padding: 15,
borderRadius: '5px',
margin: '0px',
border: 'none',
color: colorVariables.secondary,
fontWeight: 700,
background: colorVariables.primary35,
textShadow: '1px 1px 2px rgba(150, 150, 150, 1)',
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)'
},
info: {
width: '100%',
padding: 15,
color: colorVariables.darkSecondary,
textShadow: '1px 1px 2px rgba(150, 150, 150, 1)',
img: {
width: 50,
height: 50,
margin: '0 10px 0 0',
border: 'none',
borderRadius: '5px'
}
}
}
}
};
};

Ahora vamos a escribir nuestros estilos de la función MainComponent, la cual recibe nuestro scopeSelector y un tema. Extraemos del tema el fontFamily para aplicarlo a toda la app.

export default (scopeSelector, theme) => {
const {
fontFamily,
} = theme.myApp;
return {
[`${scopeSelector}`]: {
width: '100%',
height: 'inherit',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
fontFamily: fontFamily
}
};
};

Bien, ahora vamos con el header: Recordemos que mainComponent manda un handler que detonaremos mediante un evento click, éste cambiará entre los tres temas que definiremos.

import React, { PropTypes } from 'react';const propTypes = {
handler: PropTypes.func.isRequired
};
const Header = ({ handler }) => (
<div className="header">
<div className="header__title">
<img src="http://www.karmapulse.com/assets/img/logo.svg" alt="Logo" />
<h1> Demo | Radium</h1>
</div>
<div className="header__themes">
<div
className="header__themes__item theme1"
onClick={() => handler('theme1')}
>
</div>
<div
className="header__themes__item theme2"
onClick={() => handler('theme2')}
>
</div>
<div
className="header__themes__item theme3"
onClick={() => handler('theme3')}
>
</div>
</div>
</div>
);
Header.propTypes = propTypes;
export default Header;

Teniendo ya nuestro componente, vamos a los estilos. Aquí también tomamos nuestro scopeSelector y nuestro tema, del cual traeremos el header y los colores para plancharlo a nuestro header; como verás, los estilos que no deben cambiar (como los modelos de las cajas) son estáticos aquí, y los que podemos cambiar son dinámicos.

export default (scopeSelector, theme) => {
const {
colors,
myApp: { header },
} = theme;
return {
[`${scopeSelector} .header`]: {
position: header.position,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
top: header.top,
width: header.width,
height: 'auto',
boxSizing: 'border-box',
padding: header.padding,
borderRadius: header.borderRadius,
margin: header.margin,
background: header.background,
border: header.border,
boxShadow: header.boxShadow,
transitionProperty: 'all',
transitionDuration: '.4s',
},
[`${scopeSelector} .header__title`]: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
flex: '1 0 320px',
},
[`${scopeSelector} .header__title img`]: {
maxWidth: 200,
height: 'auto',
marginRight: 50
},
[`${scopeSelector} .header__title h1`]: {
fontWeight: 600,
color: header.title.color,
transitionProperty: 'all',
transitionDuration: '.4s',
},
[`${scopeSelector} .header__themes`]: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
flex: '1'
},
[`${scopeSelector} .header__themes__item`]: {
width: header.themes.item.width,
height: header.themes.item.height,
margin: '0 15px',
boxShadow: header.themes.boxShadow,
},
[`${scopeSelector} .header__themes .theme1`]: {
backgroundColor: colors.theme1
},
[`${scopeSelector} .header__themes .theme2`]: {
backgroundColor: colors.theme2
},
[`${scopeSelector} .header__themes .theme3`]: {
backgroundColor: colors.theme3
},
};
};

Bien, ahora vamos con nuestro card; un componente muy sencillo que constará de un header, una imagen y textos.

import React from 'react';const MainComponent = () => (
<div className="card">
<div className="card__header">
<h1>KarmaPulse</h1>
</div>
<div className="card__info">
<figure className="card__info__img">
<img src="https://pbs.twimg.com/profile_images/509141833864065024/C9luH5QO.jpeg" alt="" />
</figure>
<div className="card__info__text">
<h1>Daniel Venegas</h1>
<h2>front end developer</h2>
</div>
</div>
</div>
);
export default MainComponent;

Vamos a sus estilos: Así como con header, nos llegan dos parámetros que son scopeSelector y theme, del cual extraemos el card para plancharlo con nuestros estilos.

export default (scopeSelector, theme) => {
const {
card,
} = theme.myApp;
return {
[`${scopeSelector} .card`]: {
width: card.width,
height: 'auto',
boxSizing: 'border-box',
padding: card.padding,
borderRadius: card.borderRadius,
margin: card.margin,
background: card.background,
border: card.border,
boxShadow: card.boxShadow,
overflow: 'hidden',
transitionProperty: 'all',
transitionDuration: '.4s',
},
[`${scopeSelector} .card__header`]: {
width: card.header.width,
height: 'auto',
boxSizing: 'border-box',
textAlign: 'center',
padding: card.header.padding,
background: card.header.background,
border: card.header.border,
fontWeight: card.header.fontWeight,
textShadow: card.header.textShadow,
boxShadow: card.boxShadow,
color: card.header.color,
transitionProperty: 'all',
transitionDuration: '.4s',
},
[`${scopeSelector} .card__info`]: {
width: card.info.width,
height: 'auto',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
boxSizing: 'border-box',
padding: card.info.padding,
color: card.info.color,
transitionProperty: 'all',
transitionDuration: '.4s',
},
[`${scopeSelector} .card__info__img`]: {
width: card.info.img.width,
height: card.info.img.height,
margin: card.info.img.margin,
border: card.info.img.border,
borderRadius: card.info.img.borderRadius,
overflow: 'hidden',
},
[`${scopeSelector} .card__info__img img`]: {
maxWidth: '100%',
height: 'auto',
},
};
};

Vamos a definir otro tema, uno azul.

export default () => {
const colorVariables = {
primary: '#44b3cc',
secondary: '#565656',
lightPrimary: '#baf2ff',
darkSecondary: '#afafaf',
primary35: 'rgba(68, 179, 204, 0.35)',
theme1: '#dadada',
theme2: '#d4377e',
theme3: '#44b3cc'
};
return {
colors: colorVariables,
myApp: {
fontFamily: '"Roboto Condensed", sans-serif',
header: {
position: 'absolute',
top: 0,
width: '100%',
padding: 15,
borderRadius: '0 3px 0 3px',
margin: '0px',
border: 'none',
background: colorVariables.lightPrimary,
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)',
title: {
color: colorVariables.secondary,
},
themes: {
item: {
width: 30,
height: 30
},
boxShadow: `0px 2px 5px 2px ${colorVariables.primary35}`,
},
},
card: {
width: '30%',
padding: 0,
borderRadius: '5px',
margin: '0px',
border: 'none',
background: colorVariables.primary35,
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)',
header: {
width: '100%',
padding: 15,
borderRadius: '5px',
margin: '0px',
border: 'none',
color: colorVariables.secondary,
fontWeight: 700,
background: colorVariables.primary35,
textShadow: '1px 1px 2px rgba(150, 150, 150, 1)',
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)'
},
info: {
width: '100%',
padding: '100px 15px',
color: colorVariables.darkSecondary,
textShadow: '1px 1px 2px rgba(150, 150, 150, 1)',
img: {
width: 50,
height: 50,
margin: '0 10px 0 0',
border: 'none',
borderRadius: '5px'
}
}
}
}
};
};

Ahora uno rosa y con eso tendremos tres temas listos.

export default () => {
const colorVariables = {
primary: '#d4377e',
secondary: '#ffffff',
lightPrimary: '#ec96bd',
darkSecondary: '#afafaf',
primary35: 'rgba(212, 55, 126, 0.35)',
theme1: '#dadada',
theme2: '#d4377e',
theme3: '#44b3cc'
};
return {
colors: colorVariables,
myApp: {
fontFamily: '"Roboto Condensed", sans-serif',
header: {
position: 'absolute',
top: 0,
width: '100%',
padding: 15,
borderRadius: '0 3px 0 3px',
margin: '0px',
border: 'none',
background: colorVariables.lightPrimary,
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)',
title: {
color: colorVariables.secondary,
},
themes: {
item: {
width: 30,
height: 30
},
boxShadow: `0px 2px 5px 2px ${colorVariables.primary35}`,
},
},
card: {
width: '70%',
padding: 0,
borderRadius: '5px',
margin: '0px',
border: 'none',
background: colorVariables.primary35,
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)',
header: {
width: '100%',
padding: 15,
borderRadius: '5px',
margin: '0px',
border: 'none',
color: colorVariables.secondary,
fontWeight: 700,
background: colorVariables.primary35,
textShadow: '1px 1px 2px rgba(150, 150, 150, 1)',
boxShadow: '0px 2px 5px 2px rgba(0, 0, 0, 0.1)'
},
info: {
width: '100%',
padding: 35,
color: colorVariables.darkSecondary,
textShadow: '1px 1px 2px rgba(150, 150, 150, 1)',
img: {
width: 50,
height: 50,
margin: '0 10px 0 0',
border: 'none',
borderRadius: '5px'
}
}
}
}
};
};

Pasamos al builerStyles, que se encargará de unir todos los estilos y temas, hacer un match entre ellos, unirlos y por último, mandar a renderearlos. Importamos nuestros temas y nuestros estilos de los componentes, creamos una función que se encargará de mandar el tema seleccionado. Con ayuda de lodash { merge } agrupamos todas las reglas de los estilos en uno solo, el cual mandamos a nuestro componente de style para renderear.

import { Style } from 'radium';
import React, { PropTypes } from 'react';
import { merge } from 'lodash';
import themeDefault from './themeDefault';
import themeSelectPink from './themePink';
import themeSelectBlue from './themeBlue';
import cardStyles from '../../components/Card/CardStyles.jsx';
import headerStyles from '../../components/Header/HeaderStyles.jsx';
import mainComponentStyles from '../../components/MainComponent/MainComponentStyles.jsx';
const propTypes = {
scopeSelector: PropTypes.string.isRequired,
theme: PropTypes.string,
};
const selectTheme = (theme) => {
const select = {
theme1: themeDefault,
theme2: themeSelectPink,
theme3: themeSelectBlue,
};
return select[theme]();
};
const BuilderStyles = ({ scopeSelector, theme }) => {
const themeSelect = selectTheme(theme);
const margeStyles = merge(
mainComponentStyles(scopeSelector, themeSelect),
headerStyles(scopeSelector, themeSelect),
cardStyles(scopeSelector, themeSelect)
);
return (
<Style
radiumConfig={{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36'
}}
rules={margeStyles}
/>
);
};
BuilderStyles.propTypes = propTypes;export default BuilderStyles;

Con esto tenemos todos los archivos que necesitamos para correr un demo que se vera como éste:

demo radium, react

No olvides bajar el repo desde aquí;
“npm install” para instalar las dependencias,
“npm start” para correr el proyecto,
branch “master” proyecto terminado,
y branch “develop” el proyecto semilla.

Nos vemos en la siguiente nota, y recuerda no dejes de jugar XD.

--

--

Daniel Venegas
KarmaPulse Developers

Front-end Developer, adicto al anime, cine y series, jugador de fifa y amante del whisky