Criando uma tela simples ReactJs + Material UI em 10 minutos

Matheus Pacheco
#LocalizaLabs
Published in
10 min readJul 18, 2024

Recentemente, me envolvi em um projeto pessoal no qual utilizei ReactJS para aprofundar meus conhecimentos em programação para o Front End. O Material-UI, por sua vez, permitiu que eu criasse rapidamente interfaces, formulários e outros elementos simples.

O objetivo deste projeto é desenvolver um sistema de gerenciamento para um restaurante. Este sistema incluirá funcionalidades de CRUD (Criação, Leitura, Atualização e Exclusão) para as mesas e o cardápio. Além disso, contará com uma interface dedicada para o gerenciamento de pedidos em andamento, bem como uma interface separada para a gestão das vendas do restaurante. O projeto ainda está em progresso. No entanto, durante a fase inicial, utilizando ReactJs + Material Ui construí a primeira interface de gerenciamento de mesas. Esta experiência de aprendizado foi muito gratificante e rápida, então decidi compartilhá-la neste artigo.

Funcionalidade básica da página de gestão de mesas do restaurante

O primeiro passo dessa jornada foi instalar o Node atualizado para criar um projeto em ReactJs. Para isso, utilizei a página oficial https://nodejs.org/en/download/prebuilt-installer. Após realizada a instalação, bastou abrir o prompt de comando e executar:

npx create-react-app my-app

Após alguns minutos é criado um projeto com vários arquivos. Particularmente achei desnecessário a maioria deles, então resolvi remover e manter o projeto apenas com uma estrutura mais simples:

Basicamente 3 arquivos principais

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);

src/App.js

function App() {
return (
<div className="App">
<h1>Hello World</h1>
</div>
);
}

export default App;

Para executar o projeto, basta utilizar o prompt de comando aberto e rodar os seguintes comandos:

cd my-app/
npm start

O resultado deverá ser uma tela bem simples, como esta

Com isso, concluímos a inicialização do projeto e estamos prontos para iniciar de fato o trabalho proposto neste artigo

Para prosseguir, será necessário instalar o Material UI, conforme mencionado anteriormente. O Material UI é uma biblioteca que oferece uma lista de componentes prontos para auxiliar na construção do layout. Para fazer isso, utilizei o prompt de comando novamente, dentro da pasta do projeto, e executei os comandos abaixo. Isso permitiu que os pacotes sejam instalados e adicionados como dependências do projeto:

npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material

Agora que temos tudo instalado, trabalhei no desenho básico da estrutura da aplicação, organizada em componentes, algo característico do ReactJS. Para a tela de gerenciamento de mesas, criei três níveis de componentes: um independente para transições de páginas, a própria página (que também é um componente) e, dentro dela, dois componentes de layout. Essa abordagem modular facilita a manutenção e escalabilidade da aplicação:

Vamos aprofundando um pouco em cada componente e principalmente em como ele foi criado.

Página principal

A pagina principal é onde colocamos os componentes criados e toda a estrutura da aplicação Front End. Aqui ela ficou responsável pelos seguintes funcionalidades:

  • Inserir o Menu da aplicação
  • Inserir a página correspondente a opção selecionada no menu da aplicação
  • Determinar o tema padrão da aplicação
  • Fornecer método para alertas de sucesso e erro para a aplicação, que será utilizado por todas as páginas e componentes

Segue o código desenvolvido para a página principal da aplicação:

index.js

import React from 'react';
import { createRoot } from 'react-dom/client';

import { ThemeProvider } from '@emotion/react';
import { createTheme } from '@mui/material';

import App from './App';

const theme = createTheme({
typography: {
fontFamily: 'sans-serif',
color: 'inherit',
textDecoration: 'none'
}
});

const root = createRoot(document.getElementById("root"));
root.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
);

No index.js basicamente foi definido um tema simples, já que o objetivo principal do projeto não é trabalhar com layout, mas sim conhecer a estrutura do ReactJs. Além disso, renderizar o App principal (definido no App.js) para a página aberta

App.js

import React, { useState } from 'react';
import { Box, Grid, Snackbar, Alert } from '@mui/material';

import AppBarComponent from './components/AppBarComponent';

import SalesPage from './pages/SalesPage';
import TablesPage from './pages/Tables/index';
import OrdersPage from './pages/OrdersPage';
import MenuPage from './pages/MenuPage';

function renderPage(page, openSnackBar){
switch (page){
case 'Sales': return <SalesPage></SalesPage>;
case 'Tables': return <TablesPage openSnackBar={openSnackBar}></TablesPage>
case 'Menu': return <MenuPage></MenuPage>;
case 'Orders': return <OrdersPage></OrdersPage>;
default: return <SalesPage></SalesPage>;
}
}

function App() {
const [activePage, setActivePage] = useState('Sales');
const [snack, setSnack] = useState({ open: false, message: '', type: '' });

const pages = ["Tables", "Menu", "Orders", "Sales"];

const openSnackBar = (messageSnackBar, typeSnackBar) => {
setSnack({ open: true, message: messageSnackBar, type: typeSnackBar });
};

const closeSnackBar = () => {
setSnack({ open: false, message: '', type: '' });
};

function handleChangePage(page){
setActivePage(page);
}

return (
<>
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<AppBarComponent onClickMenuButton={handleChangePage} pages={pages} />
</Grid>
<Grid item xs={12}>
{renderPage(activePage, openSnackBar)}
</Grid>
</Grid>
</Box>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={snack.open} autoHideDuration={2000} onClose={closeSnackBar}>
<Alert
onClose={closeSnackBar}
severity={snack.type}
variant="filled"
sx={{ width: '100%' }}
>
{snack.message}
</Alert>
</Snackbar>
</>
);
}
export default App;

Neste arquivo App.js foi desenvolvida a funcionalidade de alternar entre as páginas, utilizando useState para definir qual HTML é renderizado ao selecionar uma opção do menu. O método handleChangePage criado neste componente é responsável por mudar o valor do estado activePage para alterar a pagina abaixo do menu, de acordo com a opção selecionada no componente de menu.

Além disso neste arquivo da página principal foi criado também o método openSnackBar para exibir uma mensagem de alerta customizado, para que seja transmitido para qualquer página ou componente que necessite alertar algo ao usuário, como uma mensagem de sucesso ou erro.

As páginas são transmitidas como propriedades para o componente menu, de forma que seja renderizada todas as opções disponíveis e retornadas para o handleChangePage aquela opção que o usuário selecionou.

1º Componente — Menu da aplicação

AppBar usado para menu de aplicativos

Como este é um componente independente, ele foi colocado fora de qualquer página, a nível do App principal, de modo que consiga alterar o conteúdo HTML abaixo deste componente. Para isso, cada um dos botões deste componente possui uma lógica para renderizar a página correspondente. Segue o código necessário para construção deste componente:

AppBarComponent.js

import * as React from 'react';
import { AppBar, Box, Toolbar, Typography, Container, Button } from '@mui/material';
import { RestaurantMenu } from '@mui/icons-material';

function AppBarComponent({onClickMenuButton, pages}) {

return (
<>
<AppBar position="static" color="primary">
<Container maxWidth="xl">
<Toolbar disableGutters>
<RestaurantMenu sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
<Typography
variant="h6"
noWrap
href="#app-bar-with-responsive-menu"
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontWeight: 700,
letterSpacing: '.1rem'
}}
>Chef Suite</Typography>

<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map((page) => (
<Button
key={page}
onClick={() => onClickMenuButton(page)}
sx={{ my: 2, color: 'inherit', display: 'block' }}
>
{page}
</Button>
))}
</Box>
</Toolbar>
</Container>
</AppBar>
</>
);
}
export default AppBarComponent;

Este componente recebe como propriedades a lista de páginas, que serão as opções exibidas para que o usuário selecione e um método que será executado passando a opção selecionada. Dessa forma a página principal, responsável por implementar este método, saberá qual opção foi selecionada e dará o comando para renderizar a página correspondente.

2º Componente — Página de gestão de Mesas

A página de gestão das mesas foi responsável por renderizar 2 componentes principais para este trabalho, uma tabela com a lista de mesas já cadastradas e um formulário para criação de novas mesas. Como a princípio não temos uma API para fornecer as informações, foi simulado um valor inicial, que é exibido ao carregar a tela. Nesta página foram implementadas as seguintes funções

  • Inserção do componente de listagem das mesas
  • Inserção do componente de criação das mesas
  • Gerenciamento dos dados das mesas cadastradas para permitir que o formulário atualize em tempo real a lista de mesas ao realizar um cadastro com sucesso.

O código deste componente ficou da seguinte forma

Tables/index.js

import React, { useState, lazy, Suspense } from 'react';
import { Box, Grid } from '@mui/material';

import NewTableForm from "./NewTableForm";
const TableList = lazy(() => import('./TableList'));

// data fake to display on page
const tables = [
{ id: '1', size: '4'},
{ id: '2', size: '2'},
{ id: '3', size: '6'},
{ id: '4', size: '4'},
{ id: '5', size: '6'},
{ id: '6', size: '10'},
{ id: '7', size: '4'},
{ id: '8', size: '4'},
{ id: '9', size: '4'},
{ id: '10', size: '2'}
]

export default function TablesPage({openSnackBar}){
const [tablesList, setTablesList] = useState(tables);
console.log(openSnackBar);

function addNewTable(newTableId, newTableSize) {
setTablesList([...tablesList, { id: newTableId, size: newTableSize }]);
openSnackBar('Table ' + newTableId + ' registered successfully', 'success');
}

function removeTable(tableId) {
setTablesList(t => t.filter(table => table.id !== tableId));
openSnackBar('Table ' + tableId + ' successfully removed', 'success');
}

function validateNewTable(newTableId) {
if (tablesList.some(table => table.id === newTableId)){
openSnackBar('Already existing Table ' + newTableId, 'error');
return false;
}

return true;
}

return (
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2}>
<Grid item xs={1}/>
<Grid item xs={3}>
<NewTableForm addNewTable={addNewTable} validateNewTable={validateNewTable}/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={6}>
<Suspense fallback={<div>Loading...</div>}>
<TableList tablesList={tablesList} removeTable={removeTable} />
</Suspense>
</Grid>
<Grid item xs={1}/>
</Grid>
</Box>
);
}

3º Componente — Tabela de listagem das mesas

Tabela de exibição das mesas cadastradas

Este componente é o mais complexo da aplicação, porém bastante simples de estruturar graças ao Material UI. Basicamente é uma tabela agrupada, em que temos uma lista de capacidades disponíveis das mesas, e para cada capacidade, podemos expandir para visualizar quais mesas possuem aquela capacidade específica, como no exemplo acima.

Aqui estamos considerando que uma API vai retornar uma lista de mesas e suas capacidades, portanto o front ficou como responsável por fazer esse agrupamento.

Este componente, além de listar mesas, tem a funcionalidade de remover mesas cadastradas, ao clicar no ícone de remoção. Ao remover todas as mesas de uma determinada capacidade, a tabela se adequa para não mais exibir a capacidade em questão. O código dessa aplicação segue abaixo:

Tables/TableList.js

import React from 'react';
import { Box, Collapse, IconButton, Typography, Paper } from '@mui/material';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';

import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import DeleteIcon from '@mui/icons-material/Delete';

function groupTablesBySize(tablesList){
let groupedTable = tablesList.reduce((tablesBySize, { size, id }) => {
if (!tablesBySize[size])
tablesBySize[size] = [];

tablesBySize[size].push(id);
return tablesBySize;
}, {});

var result = [];
for (var val in groupedTable){
result.push( {
size: val,
tables: groupedTable[val]
} )
}

return result;
}

function Row(props) {
const { row, removeTable } = props;
const [open, setOpen] = React.useState(false);

return (
<React.Fragment>
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }}>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell align="center">Tables for {row.size} people</TableCell>
<TableCell align="center">{row.tables.length} tables</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6} align="center">
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
List of Tables
</Typography>
<Table size="small" aria-label="purchases">
<TableBody>
{row.tables.map((table) => (
<TableRow key={table}>
<TableCell component="th" scope="row" align="center">
Table {table}
</TableCell>
<TableCell component="th" scope="row" align="center">
<IconButton aria-label="delete" onClick={() => removeTable(table)}><DeleteIcon /></IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
}

export default function TableList({tablesList, removeTable}){

var groupTables = groupTablesBySize(tablesList);

return (
<TableContainer component={Paper}>
<Table aria-label="collapsible table">
<TableHead>
<TableRow>
<TableCell />
<TableCell align="center">Table size</TableCell>
<TableCell align="center">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{
groupTables.map((row) => (
<Row key={row.size} row={row} removeTable={removeTable} />
))}
</TableBody>
</Table>
</TableContainer>
);
}

4º Componente — Formulário de cadastro de novas mesas

Formulário para criação de mesas

O formulário de criação de mesas ficou bem simples, porém com algumas pequenas validações e animações em seu comportamento. Ao abrir a tela de mesas, os dois botões se iniciam desabilitados, de modo que só permitam ser clicados quando realmente puderem executar as ações programadas, ou seja, o botão “Clear” só habilita se algum campo do formulário estiver preenchido. Dessa forma, ao ser clicado, os campos são limpos e os botões voltam a ficar desabilitados. Da mesma forma, o botão “Save” só é habilitado quando os dois campos possuem algum valor preenchido. Por fim, ao salvar a informação, existe uma verificação para saber se o número da mesa cadastrada já existe. Se sim, é exibido um alerta de erro informando que a mesa já existe. Se não, é exibida uma mensagem de sucesso e a nova mesa é listada no componente responsável por mostrar as mesas criadas. Segue o código deste componente:

Tables/NewTableForm.js

import React, { useState, memo } from 'react';
import { Box, TextField, Button, Grid, Typography } from '@mui/material';

import ClearAllRoundedIcon from '@mui/icons-material/ClearAllRounded';
import DoneOutlineRoundedIcon from '@mui/icons-material/DoneOutlineRounded';

const NewTableForm = memo(({validateNewTable, addNewTable}) => {
const [formData, setFormData] = useState({ newTable_tableId: '', newTable_tableSize: '' });

const handleClearForm = () => {
setFormData({
newTable_tableId: '',
newTable_tableSize: ''
});
};

const handleChangeForm = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};

const handleSubmitButton = () => {

if (!validateNewTable(formData.newTable_tableId))
return;

addNewTable(formData.newTable_tableId, formData.newTable_tableSize);
handleClearForm();
};

return (
<Box sx={{ flexGrow: 1 }}>
<form>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h5" gutterBottom align="center"
sx={{
letterSpacing: '.1rem',
}}
>New Table</Typography>
</Grid>
<Grid item xs={12}>
<TextField
required
id="newTable_tableId"
name="newTable_tableId"
label="Table ID"
variant="standard"
value={formData.newTable_tableId}
onChange={handleChangeForm}
InputLabelProps={{
shrink: true,
}}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<TextField
required
id="newTable_tableSize"
name="newTable_tableSize"
label="Table Size"
type="number"
variant="standard"
value={formData.newTable_tableSize}
onChange={handleChangeForm}
InputLabelProps={{
shrink: true,
}}
fullWidth
/>
</Grid>
<Grid item xs={12} container justifyContent="flex-end" spacing={2}>
<Grid item>
<Button
type="button"
variant="outlined"
startIcon={<ClearAllRoundedIcon />}
onClick={handleClearForm}
disabled={formData.newTable_tableSize==='' && formData.newTable_tableId===''}>

Clear
</Button>
</Grid>
<Grid item>
<Button
type="button"
variant="contained"
startIcon={<DoneOutlineRoundedIcon />}
onClick={handleSubmitButton}
disabled={formData.newTable_tableSize==='' || formData.newTable_tableId===''}>

Save
</Button>
</Grid>
</Grid>
</Grid>
</form>
</Box>
);
});

export default NewTableForm;

Estrutura final do projeto

A estrutura do projeto após implementação destes componentes ficou assim:

Organização das pastas do projeto

Executando o comando npm start, será possível ver o funcionamento da tela criada. Caso você queira baixar o código desta tela, ele se encontra neste link:
https://github.com/MatheusAntunesPacheco/chef-suite/tree/main/chef-suite-react

Espero que tenham gostado, e se ficou alguma dúvida não deixem de entrar em contato.

--

--