Criando Componentes Dinâmicos com React

/me

William Correa

@wilcorrea

-> Agenda

  • Motor de renderização
  • Diagramação
  • Componentes dinâmicos
  • Abstrações básicas

Cenas fortes de abstração

Pergunte o que e quando quiser

-> Motor de renderização

  • Virtual DOM
  • Criando elementos
  • JSX

-> Virtual DOM

  • Reagir à mudanças e aplicar ao DOM
  • DOM é lento em tarefas individuais
  • É rápido em tarefas em lote
  • Criar nós em memória e aplicar várias
  • Detectar mudanças de forma eficiente

-> Virtual DOM

  • O mundo v-dom é eficiente
  • Virtual DOM não é "dev friendly"
  • Buscar formas de simplificar isso

-> Criando elementos

  • O esquecido "hello world"
class Hello extends React.Component {
  render() {
    return React.createElement('h1', null, `Hello ${this.props.toWhat}`);
  }
}

ReactDOM.render(
  React.createElement(Hello, {toWhat: 'World'}, null),
  document.getElementById('root')
);

-> Criando elementos

  • O "hello world" mais comum
class Hello extends React.Component {
  render() {
    return <h1>Hello {this.props.toWhat}</h1>;
  }
}

ReactDOM.render(
  <Hello toWhat="World" />,
  document.getElementById('root')
);

-> JSX

-> Para levar para casa

  • Renderização no cliente vai demandar "tooling"
  • Complexidade inerente não é "o problema"

-> Diagramação

  • Distribuição de elementos
  • Sistemas de grids
  • CSS Grid Layout

-> Distribuição de elementos

  • Elementos precisam de organização

-> Distribuição de elementos

  • HTML Box
  • CSS

-> Distribuição de elementos

  • Responsividade

-> Sistemas de grids

  • Float + porcentagem
  • Flex layout

-> Sistemas de grids

  • Float + porcentagem
  • Flex layout

-> Sistemas de grids

  • Aninhamento
<div class="container-fluid">
  <div class="row">
    <div class="col-9">
      <div class="row">
        <div class="col-9">
          I'm the first element in the nested grid
        </div>
        <div class="col-3">
          I'm the second element in the nested grid
        </div>
      </div>
      <div class="row">
        <div class="col-12">
          I'm the second row in the nested grid
        </div>
       </div>
    </div>
    <div class="col-3">
      I'm the second element in the first grid
    </div>
  </div>
</div>

-> CSS Grid Layout

  • Resolve problemas tradicionais nativamente
<div class="container">
  <div class="col-9">
    I'm the first element in the nested grid
  </div>
  <div class="col-3">
    I'm the second element in the nested grid
  </div>
  <div class="col-3 height-2">
    I'm the second element in the first grid
  </div>
  <div class="col-12">
    I'm the second row in the nested grid
  </div>
</div>
.container {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  grid-gap: 2px;
}

.container > div {
  border: 1px solid #D5CFDE;
  border-radius: 2px;
  background-color: #E7E3EC;
  padding: 10px;
}

.col-3 {
  grid-column: auto / span 3;
}

.col-9 {
  grid-column: auto / span 9;
}

.col-12 {
  grid-column: auto / span 12;
}

.height-1 {
  grid-row: auto / span 1;
}

.height-2 {
  grid-row: auto / span 2;
}

-> CSS Grid Layout

-> CSS Grid Layout

-> Para levar para casa

  • Lógica e apresentação caminham de mãos dadas
  • Conhecer CSS te dá liberdade

-> Componentes dinâmicos

  • Componentes agnósticos
  • Usando JSX
  • Usando createElement

-> Componentes agnósticos

  • Componentes simples
  • Parametrização
<AppFormField
  label='Nome'
  name='name'
  value={this.state.filters.name}
  onChange={this.handleChange}
  placeholder='Informe um nome...'
/>

-> Componentes agnósticos

  • DRY < SOLID
  • SOLID < KISS
  return (
    <div className="form-group">
      { label ? labelComponent : '' }
      <AppInput valid={valid} invalid={invalid} { ...attrs }  />
      { hintText }
      { errorText }
    </div>
  );

-> Componentes agnósticos

import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';

const propTypes = {
  type: PropTypes.oneOf(['button', 'submit']),
  color: PropTypes.oneOf([
    'primary', 'secondary', 'success', 'danger',
    'warning', 'info', 'light', 'dark', 'link'
  ]),
  removeDefaultClasses: PropTypes.bool
};

const defaultProps = {
  type: 'button',
  color: 'default',
  removeDefaultClasses: false
};
// ...
// ...
const AppButton = (props) => {
  let { className, removeDefaultClasses, color, text, ...attrs } = props;

  const classes = classNames(
    !removeDefaultClasses ? 'btn' : false,
    color ? `btn-${color}` : false,
    className
  );

  return (
    <button {...attrs} className={classes}>{props.children}</button>
  );
}

AppButton.propTypes = propTypes;
AppButton.defaultProps = defaultProps;

export default AppButton;

-> Componentes agnósticos

const AppInput = (props) => {
  let { className, valid, invalid, bsSize, clean, ...attrs } = props;

  attrs.id = (attrs.hasOwnProperty('id') ? attrs.id : attrs.name);
  attrs.autoComplete = (attrs.hasOwnProperty('autoComplete') ?
                        attrs.autoComplete :'off');

  const classes = classNames(
    !clean ? 'form-control' : false,
    className,
    bsSize ? `form-control-${bsSize}` : false,
    valid ? `is-valid` : false,
    invalid ? `is-invalid` : false,
  );

  return (
    <input type={attrs.type} className={classes} { ...attrs } />
  );
}

-> Combinar Recursos

<div className="row">
  <div className="col-6">
    <AppFormField label='Nome' ...
      value={this.state...}
      onChange={this.handleChange}
    />
  </div>
<div className="col-3">
  <AppLabel className='d-none d-md-block'>&nbsp;</AppLabel>
  <AppButton color='primary' type='submit'>
    <i className="fas fa-search"/> {''}
    Buscar
  </AppButton>
  {
  this.state.hasFilter && (
    <AppButton color='link' ...>
      <i className="fas fa-eraser"/> {''}
      Limpar Filtros
    </AppButton>
   )
  }
  </div>

-> Componentes dinâmicos

  • JSX brilha
import React from 'react'
import './styles.css'

export default function App() {
  const Component = 'div'
  return (
    <Component className="App">
      <h1>Hello World</h1>
      <h2>My first dynamic component!</h2>
    </Component>
  )
}

-> Componentes dinâmicos

  • Notação de classes
import React from 'react'
import './styles.css'

export class App extends React.Component {
  render() {
    const Component = 'div'
    return (
      <Component className="App">
        <h1>Hello World</h1>
        <h2>My first dynamic component!</h2>
      </Component>
    )
  }
}

-> Componentes dinâmicos

  • Usando createElement
import React from 'react'
import './styles.css'
import h from './createElement'

export default class App extends React.Component {
  render() {
    const Component = 'div'
    const data = { className: 'App' }
    const children = [
      h('h1', 'Hello World'),
      h('h2', 'My first dynamic component!')
    ]
    return h(Component, data, children)
  }
}

-> Componentes dinâmicos

  • Ferramenta poderosa
const Component = components(field.form.component);
const attrs = (field.form.attrs ? field.form.attrs : {});

return (
  <div className={field.form.className}>
    <Component
      label={field.label}
      name={field.key}
      value={this.state.record[field.key]}
      onChange={this.handleChange}
      {...attrs}
    />
  </div>
)

-> Componentes dinâmicos

  • Em tempo de execução

-> Para levar para casa

  • Virtual DOM é sobre dinamismo
  • Qualquer "client render" terá "dynamic components"
  • Componentes dinâmicos são utilidade e não mágica

-> Abstrações básicas

  • Componentes contêineres
  • Padrão composite

-> Componentes contêineres

import { Component } from 'react'

export default class BaseComponent extends Component {

}
import BaseComponent from "./BaseComponent";

export default class TableComponent extends BaseComponent {
  constructor(props) {
    super(props);

    this.state = {...};
  }

  handleChange = e => {...};

  handleSubmit = e => {...};

  onClickClearFilters = e => {...};

  refreshTable = (page = 1) => {...};

  onClickPrevious = () => {...};

  onClickNext = () => {...};

  onClickPage = (e) => {...};
}
import BaseComponent from "./BaseComponent";

export default class FormComponent extends BaseComponent {

  constructor(props, schema, scope = 'create') {
    super(props);

    if (!schema) {
      console.error("Schema can't be empty or null");
    }

    if (!schema.hasOwnProperty('fields')) {
      throw new Error("Schema object must be a 'fields' object property.");
    }
    // ...
  }

  handleChange = e => {...};
}

-> Aplicando o composite

import React from 'react';
import FormComponent from '~/utils/FormComponent';
import { form } from "../settings";
import { components } from "../../../_config/settings";

class UsersFormPage extends FormComponent {
  constructor(props) {
    const scope = (props.meta ? props.meta.scope : 'create');
    const schema = form(scope);
    super(props, scope, schema);
  }
  // ...
}
class UsersFormPage extends FormComponent {
  // ...
  render() {
    return (
      <form className='app-form'>
        {
          this.state.fields.map(field => {
            const Component = this.parseComponent(field.form.component);
            const attrs = (field.form.attrs ? field.form.attrs : {});

            return (
              <div key={field.key} className={this.parseClassName(field)}>
                <Component
                  {...attrs}
                  value={this.state.record[field.key]} onChange={this.handleChange}
                />
              </div>
            )
          })
        }
      </form>
    );
  }
}

-> Renderizando tudo

const Component = this.parseComponent(field.form.component);
const attrs = (field.form.attrs ? field.form.attrs : {});

return (
  <div key={field.key} className={this.parseClassName(field)}`}>
    <Component
      label={field.label}
      name={field.key}
      value={this.state.record[field.key]}
      onChange={this.handleChange}
      {...attrs}
    />
  </div>
)
export default class FormComponent extends BaseComponent {
  // ...
  parseClassName (field) {
    return `field width-${field.form.width} height-${field.form.height}`
  }
}
import AppFormField from "~/components/app/FormWrappers/AppFormField";
import AppFormCheckbox from "~/components/app/FormWrappers/AppFormCheckbox";
import AppFormTextarea from "~/components/app/FormWrappers/AppFormTextarea";

export default class FormComponent extends BaseComponent {
  // ...
  parseComponent (component) {
    const components = {
      'input': AppFormField,
      'checkbox': AppFormCheckbox,
      'textarea': AppFormTextarea
    };

    return components[component];
  }
}

-> Esquemas como funções

import { status } from '~/components/app/AppTable/fields';
import * as defaultActions from '~/components/app/AppTable/actions';
import { COMPONENTS, SCOPES } from "../../_config/settings";

export const fields = () => ([
  {
    key: 'uuid', // ...
    table: {},
    form: {}
  },
  {
    key: 'name',
    label: 'Nome',
    scopes: [SCOPES.DEFAULT, SCOPES.CREATE, SCOPES.UPDATE], //...
  },
  {
    key: 'email' //...
  },
  {
    key: 'observations', //...
  },
  {
    key: 'status', //...
  }
]);

export const actions = () => Object.values(defaultActions);
import { fields, actions } from './schema';
import { deleteProperty, filterByScope } from "../../_config/helpers";

export const path = '/auth/users';

export const table = (scope = 'default') => ({
  // ...
  filters: { name: '', status: '' },
  rows: [],
  actions: actions().filter(action => filterByScope(action, scope)),
  fields: fields()
    .filter(field => filterByScope(field, scope))
    .map(field => deleteProperty(field, 'form'))
});

export const form = (scope) => ({
  // ...
  groups: [],
  fields: fields()
    .filter(field => filterByScope(field, scope))
    .map(field => deleteProperty(field, 'table'))
});

-> Esquemas como classes

import Schema from 'src/app/Agnostic/Schema'
import Service from 'src/domains/Example/User/Schema/UserService'

export default class UserSchema extends Schema {
  service = Service

  construct () {
    this.addField('nome')
      .fieldTableShow()
      .fieldTableWhere()
      .fieldFormAutofocus()
      .fieldFormWidth(50)
      .validationRequired()

    this.addField('email')
      .fieldTableShow()
      .fieldTableWhere()
      .fieldIsEmail()
      .fieldFormWidth(50)
    // ...
  }
}

-> Para levar pra casa

  • Componentes HTML são apresentação
  • Regra de negócio fica no JavaScript
  • Quanto mais JS mais manutenível e extensível

hora das palmas

perguntas?

-> Referências

Criando Componentes Dinâmicos com React

By William Correa

Criando Componentes Dinâmicos com React

  • 1,411