App Desktop com React + Electron + Sqlite | MS TO-DO clone

Aprenda a criar aplicativos de desktop eficientes e poderosos usando Electron, React e SQLite. Descubra os benefícios dessa combinação vencedora e melhore sua produtividade hoje.

App Desktop com React + Electron + Sqlite | MS TO-DO clone
Esse post foi refeito e finalizado nessa outra postagem aqui, embora esse aqui tenha um pouco mais de detalhe, você encontrará a parte do SQLite no post mais recente.

Eu acho incrível toda essa ideia de podermos utilizar tecnologias web em diversos lugares, e é super interessante a versatilidade que essas coisas nos proporcionam também.
Caso você ainda não saiba, é possível criar aplicações para dekstop com tecnologias web (html, js e css), e isso é tão comum que acaba passando despercebido por muita gente, temos apps no nosso dia a dia que são com essas tecnologias e nem parece, como é o caso do Visual Studio Code, Skype, Discord e vários outros.
Boa parte desses apps utilizam o Electron um framework focado em criação de apps desktop multiplataforma com tecnologias web. E como eu disse antes, todo esse ecossistema é super flexível, a ponto de conseguir usar o React junto ao electron, e é isso que vamos ver agora!

Overview do projeto

Nesse post, eu queria sintetizar algumas vontades que já tenho a um tempo e não consigo trazer: Um post de "React iniciante" e um sobre o Electron. Por isso, esse post vai ser dividido em partes e será o mais detalhado que eu conseguir.

Um aplicativo que eu tenho usado bastante ultimamente é o Microsoft To Do, então pensei que seria uma boa tentarmos criar um clone funcional dele durante essa série de postagens.

MS Todo

Vamos utilizar como banco de dados o Sqlite, que é um banco SQL relacional super leve e portátil, perfeito para nosso projeto.

Iniciando com o projeto

Podemos iniciar o projeto como um projeto Electron normal e depois adicionar o React, porém, essa não é a melhor forma nem a mais performática de fazer isso.
O Electron não é exatamente elogiado pelos seus Builds enxutos ou seu baixo consumo de memória ram, então, vamos utilizar um Boilerplate, que nada mais é do que um esqueleto pronto com essa integração que precisamos e com várias configurações que vão nos ajudar e muito a poupar recursos da máquina.

Vamos iniciar clonando o repositório do boilerplate e dando um nome para o nosso projeto:

git clone --depth 1 --branch main https://github.com/electron-react-boilerplate/electron-react-boilerplate.git ms-todo-clone

Após clonado, vamos navegar até a pasta do nosso projeto e instalar as dependências dele, aqui, estou levando em conta que você já tenha um ambiente Nodejs configurado, e que já tenha o yarn instalado, caso ainda não tenha, instale com npm install -g yarn e agora podemos continuar:

cd ms-todo-clone
yarn

Dependências instaladas, podemos rodar o nosso projeto:

yarn start
Electron React

No nosso projeto, temos a seguinte estrutura:

React Electron

O que podemos observar logo de cara é que temos dois package.json em pastas diferentes, e isso é por que o boilerplate separa dependências de desenvolvimento na raiz e dependências da aplicação dentro do app. Você pode ler detalhadamente sobre essa escolha aqui.
Outro ponto interessante e que temos o CSS Modules, React Router Dom e o testing-library já configurado no projeto.

Iniciando a UI

Para iniciar nosso projeto, vamos criar uma pasta views, styles e components dentro de src/renderer, vamos copiar as rotas dentro do arquivo App.tsx e criar um arquivo chamado routes.tsx e colar o conteúdo das rotas(a função que esta sendo exportada e os imports do react router dom), em seguida podemos deletar o arquivo App.tsx, agora mova o arquivo App.global.css para a pasta de styles. Dentro da pasta de views vamos criar uma pasta chamada Home, dentro dela vamos criar um arquivo chamado index.tsx e um home.module.tsx, importe esse componente para o seu arquivo de rota e use na rota "/".
Vamos precisar fazer algumas alterações no nosso index.tsx que fica em renderer, primeiro vamos corrigir a importação do componente App para apontar para nossas rotas, em seguida vamos importar nosso CSS global que movemos para a pasta styles.
Nossa estrutura ficará dessa forma:

Estrutura de pastas


Nosso arquivo de rotas:

Componentes de rota


Nosso arquivo index.tsx:

Componente da tela

Com toda nossa estrutura configurada, vamos começar nossa interface, vamos começar pela sidebar, então dentro da pasta components cria uma pasta chamada Sidebar com os arquivos index.tsx e Sidebar.module.css dentro dela, vamos inicialmente adicionar o seguinte código a esse componente:

import React from 'react';

import styles from './Sidebar.module.css';

export default function Sidebar() {
  return (
    <div>
      <a href="#">Meu dia</a>
      <a href="#">Importante</a>
      <a href="#">Planejado</a>
      <a href="#">Trabalho</a>
    </div>
  );
}

Importe o componente na Home, ficando dessa forma por enquanto:

import React from 'react';

import Sidebar from '../../components/Sidebar';

export default function Home() {
  return <Sidebar />;
}

Vamos criar um arquivo de tema, para centralizar nossos estilos. Na pasta styles crie uma pasta chamada themes e crie um arquivo chamado default.css e nele vamos pôr o seguinte conteúdo:

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300&display=swap');

:root {
  --primary-color: #788cde;
  --secondary-color: #323232;
  --background-color: #282828;
  --alternate-background-color: #1e1e1e;

  --text-color: #e1e1e1;
  --text-color-light: #777676bb;
  --font: Roboto;

  --text-cancel-color: #dd2a2c;

  --link-color: #e1e1e1;
  --link-color--hover: #543fd7;
}

Agora vamos estilizar nossa Sidebar, para isso abra o arquivo Sidebar.module.css e vamos colocar o seguinte:

@import '../../styles/themes/default.css';

.sidenav {
  width: 240px;
  height: 100vh;
  background: var(--background-color);
  overflow-x: hidden;
  padding-left: 10px;
}

.sidenav a {
  padding: 10px;
  text-decoration: none;
  font-family: var(--font);
  font-size: 1.1rem;
  color: var(--link-color);
  display: block;
}

.sidenav a:hover {
  background-color: var(--alternate-background-color);
}

Agora vamos criar nosso componente de logo. Na pasta components crie a pasta Logo e dentro dela index.tsx e Logo.module.css:

import React from 'react';

import styles from './Logo.module.css';

export default function Logo() {
  return <h1 className={styles.logo}>TODO Clone</h1>;
}
@import '../../styles/themes/default.css';

.logo {
  color: var(--primary-color);
  margin: 20px 0px;
  font-family: var(--font);
  font-weight: 800;
}

Importe o componente de logo na nossa Sidebar:

import React from 'react';

import Logo from '../Logo';

import styles from './Sidebar.module.css';

export default function Sidebar() {
  return (
    <div className={styles.sidenav}>
      <Logo />
      <a href="#">Meu dia</a>
      <a href="#">Importante</a>
      <a href="#">Planejado</a>
      <a href="#">Trabalho</a>
    </div>
  );
}

Como resultado, teremos o seguinte até agora:

Previa

Crie duas novas pastas em components: TaskArea e TaskItem.
TaskItem é o componente que representará nossa tarefa, no arquivo index.tsx vamos incluir o seguinte:

import React from 'react';
import { format } from 'date-fns';
import styles from './TaskItem.module.css';

export type TaskItemType = {
  label: string;
  date: string;
  id: number;
  checked: boolean;
  onChange: (id: number) => void;
};

export default function TaskItem({
  date,
  label,
  id,
  checked,
  onChange,
}: TaskItemType) {
  function handleCheck() {
    onChange(id);
  }

  return (
    <div
      className={`${styles.container} ${checked ? styles['task-finish'] : ''}`}
      id={`${id}`}
    >
      <input
        className={styles.checkbox}
        type="checkbox"
        checked={checked}
        onChange={handleCheck}
      />
      <div className="col">
        <div className="row">
          <p className={styles['task-label']}>{label}</p>
        </div>
        <div className="row">
          <p className={styles['task-date']}>
            {format(new Date(date), "E., dd 'de' MMM")}
          </p>
        </div>
      </div>
    </div>
  );
}
Perceba que você precisará instalar o date-fns.
E o CSS dele fica da seguinte forma:
@import '../../styles/themes/default.css';

.container {
  display: flex;
  align-items: center;
  background-color: var(--secondary-color);
  padding: 10px 20px;
  margin: 1px 0px;
  color: var(--text-color);
  font-family: var(--font);
  border-radius: 6px;
}

.container > :nth-child(1) {
  margin-right: 15px;
}

.task-label {
  font-size: 0.85rem;
  color: var(--text-color);
}

.task-date {
  font-size: 0.85rem;
  color: var(--text-cancel-color);
  font-weight: bold;
}

.task-finish .task-label {
  text-decoration: line-through;
}

input[type='checkbox'] {
  -webkit-appearance: none;
  appearance: none;
  background-color: var(--alternate-background-color);
  margin: 0;
  font: inherit;
  color: currentColor;
  width: 1.35em;
  height: 1.35em;
  border: 0.15em solid var(--background-color);
  border-radius: 50px;
  transform: translateY(-0.075em);
  display: grid;
  place-content: center;
}

input[type='checkbox']::before {
  content: '';
  width: 0.55em;
  height: 0.55em;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
  border-radius: 50px;
  transform: scale(0);
  transform-origin: bottom left;
  transition: 120ms transform ease-in-out;
  box-shadow: inset 1em 1em var(--background-color);
  background-color: var(--background-color);
}

input[type='checkbox']:checked::before {
  transform: scale(1);
}

input[type='checkbox']:checked {
  background-color: var(--primary-color);
}

input[type='checkbox']:focus {
  outline: max(2px, 0.15em) solid currentColor;
  outline-offset: max(2px, 0.15em);
}

input[type='checkbox']:disabled {
  color: var(--primary-color);
  cursor: not-allowed;
}

TaskArea será o container que irá gerenciar quais tasks serão exibidas. O código dele fica assim:

import React, { useState } from 'react';

import TaskItem from '../TaskItem';

import styles from './TaskArea.module.css';

export default function TaskArea() {
  const [tasks, setTasks] = useState([
    {
      id: 1,
      label: 'Teste de task',
      date: new Date().toDateString(),
      checked: false,
    },
    {
      id: 2,
      label: 'Teste de task',
      date: new Date().toDateString(),
      checked: false,
    },
    {
      id: 3,
      label: 'Teste de task',
      date: new Date().toDateString(),
      checked: false,
    },
  ]);

  const handleCheckTask = (id: number) => {
    const newState = tasks.map((task) => {
      if (task.id === id) {
        return {
          ...task,
          checked: !task.checked,
        };
      }

      return task;
    });

    setTasks(newState);
  };

  return (
    <div className={styles.container}>
      {tasks.map((task) => (
        <TaskItem
          checked={task.checked}
          date={task.date}
          label={task.label}
          key={task.id}
          id={task.id}
          onChange={handleCheckTask}
        />
      ))}
    </div>
  );
}

E o CSS:

@import '../../styles/themes/default.css';

.container {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 10px;
  background-color: var(--alternate-background-color);
}

Com isso, já podemos voltar na nossa view Home e importar o componente TaskArea e vamos importar os estilos dela também:

import React from 'react';

import Sidebar from '../../components/Sidebar';
import TaskArea from '../../components/TaskArea';

import styles from './Home.module.css';

export default function Home() {
  return (
    <div className={styles.container}>
      <Sidebar />
      <TaskArea />
    </div>
  );
}

CSS da Home:

.container {
  display: flex;
  flex-direction: row;
}

Com isso, já temos nossa UI exibindo as tarefas e marcando ou desmarcando como "feita".

Prévia

Nosso próximo passo será:

  • Criar novas tarefas
  • Editar tarefas
  • Deletar tarefas
  • Adicionar data a tarefa
  • Verificar se a tarefa está fora do prazo
  • Navegar entre os menus laterais
  • Conectar com a base de dados

Continuação:

Desktop Apps with Electron, React and SQLite
Learn how to create efficient and powerful desktop applications using Electron, React, and SQLite. Discover the benefits of this winning combination and enhance your productivity today.
Cobre a parte atual e SQLite.