App Desktop com React + Electron + Sqlite | MS TO-DO clone
Tags: react, typescript, sql, microsoft
Published: 2021-08-31 01:01:00

Eu acho incrível toda essa ideia de podermos utilizar tecnologias web em diversos lugares, e é super interessante a versatilidade que essas coisas nos proporciona 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.

> Esse post foi atualizado e finalizado (a parte que falta do SQLite) e [está disponível aqui](https://tuliocalil.com/desktop-apps-with-electron-react-and-sqlite/).

Boa parte desses apps utilizam o [Electron](https://www.electronjs.org/) 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](https://todo.microsoft.com/tasks/pt-br/), então pensei que seria uma boa tentarmos criar um clone funcional dele durante essa serie de postagens.
![MS Todo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/krv8zxfof74mhiutzjkc.png)
 

Vamos utilizar como banco de dados o [Sqlite](https://www.sqlite.org/index.html), 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 varias configurações que vão nos ajudar e muito a poupar recursos da maquina.

Vamos iniciar clonando o [repositório do boilerplate](https://www.sqlite.org/index.html) e dando um nome para o nosso projeto:
```sh
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](https://dev.to/tuliocalil/react-no-windows-com-wsl-3951), e que já tenha o `yarn` instalado, caso ainda não tenha, instale com `npm install -g yarn` e agora podemos continuar:
```sh
cd ms-todo-clone
yarn
```
Dependências instaladas, podemos rodar o nosso projeto:
```
yarn start
```

![Electron React](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2rjc6vgeytb9ybqr314i.png)
 
No nosso projeto, temos a seguinte estrutura:
![React Electron](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0xzzza84wr1741fr9iti.png)

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](https://www.electron.build/tutorials/two-package-structure.html).
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bl1l11qxiponltbn99au.png)
Nosso arquivo de rotas:
![Componentes de rota](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lbrj99625bmny1zk8908.png)
Nosso arquivo index.tsx:
![Componente da tela](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hk2leom225om2u3x5fua.png)
 
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:

```jsx
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:

```jsx
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 por o seguinte conteúdo:
```css
@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:

```css
@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`:

```typescript
import React from 'react';

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

export default function Logo() {
  return <h1 className={styles.logo}>TODO Clone</h1>;
}
```

```css
@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`:

```typescript
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxvmjoj1hb80dlol5z4s.png)


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

```typescript
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:
```css
@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:
```typescript
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:
```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:
```typescript
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:
```css
.container {
  display: flex;
  flex-direction: row;
}
```

Com isso, já temos nossa UI exibindo as tarefas e marcando ou desmarcando como "feita". 
![Prévia](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u05jpznammpgm8r2s6po.png)

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
 
[Post atualizado com a parte final](https://tuliocalil.com/desktop-apps-with-electron-react-and-sqlite/)

[Post sobre Electron + SQLite e Autenticação](https://tuliocalil.com/offline-auth-with-electron-sqlite-react/)