On this tutorial, we’ll discover Tauri, a contemporary, cross-platform framework for constructing desktop apps.
Contents:
For a few years, Electron was the de facto cross-platform framework for constructing desktop apps. Visible Studio Code, MongoDB Compass, and Postman are all nice examples of apps constructed with this framework. Electron is unquestionably nice, nevertheless it has some important drawbacks, which another trendy frameworks have overcome — Tauri being among the finest of them.
What’s Tauri?
Tauri is a contemporary framework that means that you can design, develop and construct cross-platform apps utilizing acquainted internet applied sciences like HTML, CSS, and JavaScript on the frontend, whereas making the most of the highly effective Rust programming language on the backend.
Tauri is framework agnostic. Which means you should utilize it with any frontend library of your selection — akin to Vue, React, Svelte, and so forth. Additionally, utilizing Rust in a Tauri-based challenge is totally elective. You need to use simply the JavaScript API supplied by Tauri to construct your whole app. This makes it simple not solely to construct a brand new app, but additionally to take the codebase of an online app you’ve already constructed and switch it right into a native desktop app whereas barely needing to change the unique code.
Let’s take a look at why we must always use Tauri as a substitute of Electron.
Tauri vs Electron: a Fast Comparability
There are three vital parts to constructing a very nice app. The app have to be small, quick, and safe. Tauri outperforms Electron in all three:
- Tauri produces a lot smaller binaries. As you’ll be able to see from the benchmarks outcomes revealed by Tauri, even a brilliant easy Whats up, World! app could be a large measurement (over 120 MB) when it’s constructed with Electron. In distinction, the binary measurement of the identical Tauri app is far smaller, lower than 2 MB. That is fairly spectacular in my view.
- Tauri apps carry out manner quicker. From the identical web page talked about above, you may also see that the reminiscence utilization of Tauri apps is likely to be practically half that of an equal Electron app.
- Tauri apps are extremely safe. On the Tauri web site, you’ll be able to examine all of the built-in safety features Tauri offers by default. However one notable function I wish to point out right here is that builders can explicitly allow or disable sure APIs. This not solely retains your app safer, but additionally reduces the binary measurement.
Constructing a Word-taking App
On this part, we’ll construct a easy note-taking app with the next options:
- add and delete notes
- rename a word’s title
- edit a word’s content material in Markdown
- preview a word’s content material in HTML
- save notes in native storage
- import and export notes to the system onerous drive
Yow will discover all of the challenge information on GitHub.
Getting began
To get began with Tauri, you first have to set up Rust and its system dependencies. They’re completely different relying on a person’s working system, so I’m not going to discover them right here. Please observe the directions on your OS within the documentation.
If you’re prepared, in a listing of your selection, run the next command:
It will information you trough the set up course of as proven beneath:
$ npm create tauri-app
We hope to assist you create one thing particular with Tauri!
You should have a selection of one of many UI frameworks supported by the larger internet tech group.
This device ought to get you shortly began. See our docs at https://tauri.app/
If you have not already, please take a second to setup your system.
It's possible you'll discover the necessities right here: https://tauri.app/v1/guides/getting-started/stipulations
Press any key to proceed...
? What's your app title? my-notes
? What ought to the window title be? My Notes
? What UI recipe would you want so as to add? create-vite (vanilla, vue, react, svelte, preact, lit) (https://vitejs.dev/information/
? Add "@tauri-apps/api" npm package deal? Sure
? Which vite template would you want to make use of? react-ts
>> Operating preliminary command(s)
Must set up the next packages:
create-vite@3.2.1
Okay to proceed? (y) y
>> Putting in any further wanted dependencies
added 87 packages, and audited 88 packages in 19s
9 packages are trying for funding
run `npm fund` for particulars
discovered 0 vulnerabilities
added 2 packages, and audited 90 packages in 7s
10 packages are trying for funding
run `npm fund` for particulars
discovered 0 vulnerabilities
>> Updating "package deal.json"
>> Operating "tauri init"
> my-notes@0.0.0 tauri
> tauri init --app-name my-notes --window-title My Notes --dist-dir ../dist --dev-path http://localhost:5173
✔ What's your frontend dev command? · npm run dev
✔ What's your frontend construct command? · npm run construct
>> Updating "tauri.conf.json"
>> Operating remaining command(s)
Your set up accomplished.
$ cd my-notes
$ npm run tauri dev
Please guarantee that your selections match those I did, that are primarily scaffolding a React app with Vite and TypeScript assist and putting in the Tauri API package deal.
Don’t run the app but. First we have to set up some further packages wanted for our challenge. Run the next instructions in your terminal:
npm set up @mantine/core @mantine/hooks @tabler/icons @emotion/react marked-react
It will set up the next packages:
Now we’re prepared to check the app, however earlier than that, let’s see how the challenge is structured:
my-notes/
├─ node_modules/
├─ public/
├─ src/
│ ├─ belongings/
│ │ └─ react.svg
│ ├─ App.css
│ ├─ App.tsx
│ ├─ index.css
│ ├─ predominant.tsx
│ └─ vite-env.d.ts
├─ src-tauri/
│ ├─ icons/
│ ├─ src/
│ ├─ .gitignore
│ ├─ construct.rs
│ ├─ Cargo.toml
│ └─ tauri.config.json
├─ .gitignore
├─ index.html
├─ package-lock.json
├─ package deal.json
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts
Crucial factor right here is that the React a part of the app is saved within the src
listing and Rust and different Tauri-specific information are saved in src-tauri
. The one file we have to contact within the Tauri listing is tauri.conf.json
, the place we are able to configure the app. Open this file and discover the allowlist
key. Change its content material with the next:
"allowlist": {
"dialog": {
"save": true,
"open": true,
"ask": true
},
"fs": {
"writeFile": true,
"readFile": true,
"scope": ["$DOCUMENT/*", "$DESKTOP/*"]
},
"path": {
"all": true
},
"notification": {
"all": true
}
},
Right here, for safety causes, as I discussed above, we allow solely the APIs we’re going to make use of in our app. We additionally prohibit the entry to the file system with solely two exceptions — the Paperwork
and Desktop
directories. It will permit customers to export their notes simply to those directories.
We have to change yet another factor earlier than closing the file. Discover the bundle
key. Beneath that key, you’ll discover the identifier
key. Change its worth to com.mynotes.dev
. That is wanted on app construct as a result of the identifier have to be distinctive.
The very last thing I wish to point out is that, within the final home windows
key, you’ll be able to arrange all window associated settings:
"home windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "My Notes",
"width": 800
}
]
As you’ll be able to see, the title
key was arrange for you in line with the worth you gave it throughout the set up.
OK, so let’s lastly begin the app. Within the my-notes
listing, run the next command:
You’ll want to attend for some time till the Tauri setup is full and all information have been compiled for the primary time. Don’t fear. Within the subsequent builds the method will likely be a lot quicker. When Tauri is prepared, it would open the app window mechanically. The picture beneath reveals what you need to see.
Word: after the app is run in improvement mode or it’s constructed, a brand new goal
listing is created inside src-tauri
, which comprises all of the compiled information. In dev mode, they’re positioned within the debug
subdirectory, and in construct mode they’re positioned within the launch
subdirectory.
OK, let’s now adapt information to our wants. First, delete the index.css
and App.css
information. Then open the predominant.tsx
file and exchange its contents with the next:
import React from 'react'
import ReactDOM from 'react-dom/consumer'
import { MantineProvider } from '@mantine/core'
import App from './App'
ReactDOM.createRoot(doc.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<MantineProvider withGlobalStyles withNormalizeCSS>
<App />
</MantineProvider>
</React.StrictMode>
)
This units up Mantine’s parts to be used.
Subsequent, open the App.tsx
file and exchange its content material with the next:
import { useState } from 'react'
import { Button } from '@mantine/core'
perform App() {
const [count, setCount] = useState(0)
return (
<div>
<Button onClick={() => setCount((depend) => depend + 1)}>depend is {depend}</Button>
</div>
)
}
export default App
Now, should you have a look within the app window you need to see the next:
Guarantee that the app is operating correctly by clicking the button. If one thing’s incorrect, you would possibly have to debug it. (See the next word.)
Word: when the app is operating in improvement mode, you’ll be able to open the DevTools by right-clicking on the app window and choosing Examine from the menu.
Creating the bottom app performance
Now let’s create the skeleton of our app. Change the contents of the App.tsx
file with the next:
import { useState } from 'react'
import Markdown from 'marked-react'
import { ThemeIcon, Button, CloseButton, Change, NavLink, Flex, Grid, Divider, Paper, Textual content, TextInput, Textarea } from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
import { IconNotebook, IconFilePlus, IconFileArrowLeft, IconFileArrowRight } from '@tabler/icons'
import { save, open, ask } from '@tauri-apps/api/dialog'
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs'
import { sendNotification } from '@tauri-apps/api/notification'
perform App() {
const [notes, setNotes] = useLocalStorage({ key: "my-notes", defaultValue: [ {
"title": "New note",
"content": ""
}] })
const [active, setActive] = useState(0)
const [title, setTitle] = useState("")
const [content, setContent] = useState("")
const [checked, setChecked] = useState(false)
const handleSelection = (title: string, content material: string, index: quantity) => {
setTitle(title)
setContent(content material)
setActive(index)
}
const addNote = () => {
notes.splice(0, 0, {title: "New word", content material: ""})
handleSelection("New word", "", 0)
setNotes([...notes])
}
const deleteNote = async (index: quantity) => {
let deleteNote = await ask("Are you certain you wish to delete this word?", {
title: "My Notes",
sort: "warning",
})
if (deleteNote) {
notes.splice(index,1)
if (energetic >= index) {
setActive(energetic >= 1 ? energetic - 1 : 0)
}
if (notes.size >= 1) {
setContent(notes[index-1].content material)
} else {
setTitle("")
setContent("")
}
setNotes([...notes])
}
}
return (
<div>
<Grid develop m={10}>
<Grid.Col span="auto">
<Flex hole="xl" justify="flex-start" align="heart" wrap="wrap">
<Flex>
<ThemeIcon measurement="lg" variant="gradient" gradient={{ from: "teal", to: "lime", deg: 90 }}>
<IconNotebook measurement={32} />
</ThemeIcon>
<Textual content colour="inexperienced" fz="xl" fw={500} ml={5}>My Notes</Textual content>
</Flex>
<Button onClick={addNote} leftIcon={<IconFilePlus />}>Add word</Button>
<Button.Group>
<Button variant="mild" leftIcon={<IconFileArrowLeft />}>Import</Button>
<Button variant="mild" leftIcon={<IconFileArrowRight />}>Export</Button>
</Button.Group>
</Flex>
<Divider my="sm" />
{notes.map((word, index) => (
<Flex key={index}>
<NavLink onClick={() => handleSelection(word.title, word.content material, index)} energetic={index === energetic} label={word.title} />
<CloseButton onClick={() => deleteNote(index)} title="Delete word" measurement="xl" iconSize={20} />
</Flex>
))}
</Grid.Col>
<Grid.Col span={2}>
<Change label="Toggle Editor / Markdown Preview" checked={checked} onChange={(occasion) => setChecked(occasion.currentTarget.checked)}/>
<Divider my="sm" />
{checked === false && (
<div>
<TextInput mb={5} />
<Textarea minRows={10} />
</div>
)}
{checked && (
<Paper shadow="lg" p={10}>
<Textual content fz="xl" fw={500} tt="capitalize">{title}</Textual content>
<Divider my="sm" />
<Markdown>{content material}</Markdown>
</Paper>
)}
</Grid.Col>
</Grid>
</div>
)
}
export default App
There’s a whole lot of code right here, so let’s discover it little by little.
Importing the required packages
To start with, we import all the required packages as follows:
- Markdown parser
- Mantine parts
- a Mantine hook
- icons
- Tauri APIs
import { useState } from 'react'
import Markdown from 'marked-react'
import { ThemeIcon, Button, CloseButton, Change, NavLink, Flex, Grid, Divider, Paper, Textual content, TextInput, Textarea } from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
import { IconNotebook, IconFilePlus, IconFileArrowLeft, IconFileArrowRight } from '@tabler/icons'
import { save, open, ask } from '@tauri-apps/api/dialog'
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs'
import { sendNotification } from '@tauri-apps/api/notification'
Organising app storage and variables
Within the subsequent half, we use the useLocalStorage
hook to arrange the storage for the notes.
We additionally set a few variables for the present word’s title and content material, and two extra for figuring out which word is chosen (energetic
) and whether or not Markdown preview is enabled (checked
).
Lastly, we create a utility perform to deal with the choice of a word. When a word is chosen, it would replace the present word’s properties accordingly:
const [notes, setNotes] = useLocalStorage({ key: "my-notes", defaultValue: [ {
"title": "New note",
"content": ""
}] })
const [active, setActive] = useState(0)
const [title, setTitle] = useState("")
const [content, setContent] = useState("")
const [checked, setChecked] = useState(false)
const handleSelection = (title: string, content material: string, index: quantity) => {
setTitle(title)
setContent(content material)
setActive(index)
}
Including add/delete word performance
The subsequent two capabilities are for including/deleting a word.
addNote()
inserts a brand new word object within the notes
array. It makes use of handleSelection()
to pick out the brand new word mechanically after including it. And eventually it updates the notes. The rationale we use the unfold operator right here is as a result of in any other case the state received’t be up to date. This fashion, we drive the state to replace and the part to rerender, so the notes will likely be displayed correctly:
const addNote = () => {
notes.splice(0, 0, {title: "New word", content material: ""})
handleSelection("New word", "", 0)
setNotes([...notes])
}
const deleteNote = async (index: quantity) => {
let deleteNote = await ask("Are you certain you wish to delete this word?", {
title: "My Notes",
sort: "warning",
})
if (deleteNote) {
notes.splice(index,1)
if (energetic >= index) {
setActive(energetic >= 1 ? energetic - 1 : 0)
}
if (notes.size >= 1) {
setContent(notes[index-1].content material)
} else {
setTitle("")
setContent("")
}
setNotes([...notes])
}
}
deleteNote()
makes use of the ask
dialog to substantiate that the person desires to delete the word and hasn’t clicked the delete button by chance. If the person confirms deletion (deleteNote = true
) then the if
assertion is executed:
- the word is faraway from the
notes
array - the
energetic
variable is up to date - the present word’s title and content material are up to date
- the
notes
array is up to date
Creating the JSX template
Within the template part, we have now two columns.
Within the first column, we create the app emblem and title, and buttons for including, importing and exporting notes. We additionally loop trough the notes
array to render the notes. Right here we use handleSelection()
once more to replace the present word’s properties correctly when a word title hyperlink is clicked:
<Grid.Col span="auto">
<Flex hole="xl" justify="flex-start" align="heart" wrap="wrap">
<Flex>
<ThemeIcon measurement="lg" variant="gradient" gradient={{ from: "teal", to: "lime", deg: 90 }}>
<IconNotebook measurement={32} />
</ThemeIcon>
<Textual content colour="inexperienced" fz="xl" fw={500} ml={5}>My Notes</Textual content>
</Flex>
<Button onClick={addNote} leftIcon={<IconFilePlus />}>Add word</Button>
<Button.Group>
<Button variant="mild" leftIcon={<IconFileArrowLeft />}>Import</Button>
<Button variant="mild" leftIcon={<IconFileArrowRight />}>Export</Button>
</Button.Group>
</Flex>
<Divider my="sm" />
{notes.map((word, index) => (
<Flex key={index}>
<NavLink onClick={() => handleSelection(word.title, word.content material, index)} energetic={index === energetic} label={word.title} />
<CloseButton onClick={() => deleteNote(index)} title="Delete word" measurement="xl" iconSize={20} />
</Flex>
))}
</Grid.Col>
Within the second column, we add a toggle button to modify between word modifying and previewing modes. In edit mode, there’s a textual content enter for the present word’s title and a textarea for the present word’s content material. In preview mode, the title is rendered by Mantine’s Textual content
part, and the content material is rendered by the marked-react
’s Markdown
part:
<Grid.Col span={2}>
<Change label="Toggle Editor / Markdown Preview" checked={checked} onChange={(occasion) => setChecked(occasion.currentTarget.checked)}/>
<Divider my="sm" />
{checked === false && (
<div>
<TextInput mb={5} />
<Textarea minRows={10} />
</div>
)}
{checked && (
<Paper shadow="lg" p={10}>
<Textual content fz="xl" fw={500} tt="capitalize">{title}</Textual content>
<Divider my="sm" />
<Markdown>{content material}</Markdown>
</Paper>
)}
</Grid.Col>
Phew! That was a whole lot of code. The picture beneath reveals what our app ought to take a look at this level.
Nice! We are able to add and delete notes now, however there’s no method to edit them. We’ll add this performance within the subsequent part.
Including a word’s title and content material updating performance
Add the next code after the deleteNote()
perform:
const updateNoteTitle = ({ goal: { worth } }: { goal: { worth: string } }) => {
notes.splice(energetic, 1, { title: worth, content material: content material })
setTitle(worth)
setNotes([...notes])
}
const updateNoteContent = ({goal: { worth } }: { goal: { worth: string } }) => {
notes.splice(energetic, 1, { title: title, content material: worth })
setContent(worth)
setNotes([...notes])
}
These two capabilities exchange the present word’s title and/or content material respectively. To make them work, we have to add them within the template:
<TextInput worth={title} onChange={updateNoteTitle} mb={5} />
<Textarea worth={content material} onChange={updateNoteContent} minRows={10} />
Now, when a word is chosen, its title and content material will likely be displayed within the enter textual content and textarea respectively. After we edit a word, its title will likely be up to date accordingly.
I’ve added a number of notes to display how the app will look. The app with a particular word and its content material is pictured beneath.
The picture beneath reveals the preview of our word.
And the following picture reveals the affirmation dialog that’s proven throughout word deletion.
Nice! The very last thing we have to make our app actually cool is so as to add performance to export and import the person’s notes to the system onerous drive.
Including performance for importing and exporting notes
Add the next code after the updateNoteContent()
perform:
const exportNotes = async () => {
const exportedNotes = JSON.stringify(notes)
const filePath = await save({
filters: [{
name: "JSON",
extensions: ["json"]
}]
})
await writeTextFile(`${filePath}`, exportedNotes)
sendNotification(`Your notes have been efficiently saved in ${filePath} file.`)
}
const importNotes = async () => {
const selectedFile = await open({
filters: [{
name: "JSON",
extensions: ["json"]
}]
})
const fileContent = await readTextFile(`${selectedFile}`)
const importedNotes = JSON.parse(fileContent)
setNotes(importedNotes)
}
Within the first perform, we convert notes to JSON. Then we use the save
dialog to save lots of the notes. Subsequent, we use the writeTextFile()
perform to put in writing the file bodily on the disk. Lastly, we use the sendNotification()
perform to tell the person that the notes have been saved efficiently and likewise the place they have been saved.
Within the second perform, we use the open
dialog to pick out a JSON file, containing notes, from the disk. Then the file is learn with the readTextFile()
perform, its JSON content material is transformed to an object, and eventually the notes storage is up to date with the brand new content material.
The very last thing we have to do is change the template to utilize the above capabilities:
<Button variant="mild" onClick={importNotes} leftIcon={<IconFileArrowLeft />}>Import</Button>
<Button variant="mild" onClick={exportNotes} leftIcon={<IconFileArrowRight />}>Export</Button>
Right here’s what the remaining App.tsx
file ought to appear to be.
Within the subsequent screenshots you’ll be able to see the Save As and Open dialogs, and the system notification showing as notes are saved.
Congrats! You’ve simply constructed a completely practical note-taking desktop app with the ability of Tauri.
Construct the app
Now, if every part works nice and also you’re proud of the top consequence, you’ll be able to construct the app and get an set up package deal on your working system. To take action, run the next command:
Conclusion
On this tutorial, we explored what Tauri is, why it’s more sensible choice for constructing native desktop apps in comparison with Electron, and eventually the way to construct a easy however absolutely practical Tauri app.
I hope you’ve loved this brief journey as a lot as I’ve. To dive deeper into Tauri world, take a look at its documentation and preserve experimenting with its highly effective options.
Associated studying: