Berkenalan dengan Remix - 1
Remix, framework fullstack baru dari tim pembuat React Router. Secara fungsional mirip NextJS (SSR, routing, dll), tapi banyak yang bilang kalo framework ini juga bisa dibandingin sama Rails, Laravel, dan sejenisnya karena aplikasi webnya bisa jalan tanpa JavaScript.
Disclaimer: Materi dalam tulisan ini saya ambil dari video tutorial di Youtube buatan Traversy Media di bawah ini. Bagian-bagian yang menurut saya nggak esensial saya lewati biar nggak kepanjangan.
Apa itu Remix?
Remix adalah fullstack framework berbasis React yang punya fitur-fitur standar seperti:
- SSR
- Routing berbasis dokumen/fail
- Routing bersarang (nested)
- Fungsi-fungsi server ( Loader & Action )
- Akses mudah ke elemen
<head>
dan dokumen HTML - Penanganan error
- Dukungan untuk TypeScript
- Dukungan untuk cookies & session
Semua itu udah built-in, jadi kita nggak perlu instal paket NPM sendiri.
Instalasi & Struktur Direktori
Untuk instalasi kita jalanin perintah:
npx create-remix@latest
Skrip instalasinya nanti kasih kita beberapa opsi. Untuk direktori terserah mau dikasih nama apa tapi di sini saya ikuti yang ada di video, remix-blog
. Untuk pilihan deploynya samain juga, Remix App Server
.

Bahasa pilih JavaScript & langsung jalanin npm install
.


Tunggu sampe selesai terus CD ke direktori aplikasi.

Sekarang coba jalanin dev server & buka localhost:3000
npm run dev


Di package.json
bisa kita liat beberapa paket yang jadi dependency & skrip npm.
{
"private": true,
"name": "remix-app-template-js",
"description": "",
"license": "",
"scripts": {
"build": "remix build",
"dev": "remix dev",
"postinstall": "remix setup node",
"start": "remix-serve build"
},
"dependencies": {
"@remix-run/react": "^1.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remix": "^1.1.1",
"@remix-run/serve": "^1.1.1"
},
"devDependencies": {
"@remix-run/dev": "^1.1.1"
},
"engines": {
"node": ">=14"
},
"sideEffects": false
}
Skrip build
dipake untuk bikin production-build. Skrip ini kalo dijalanin akan mengkompail kode kita & men-generate fail output direktori /build
yang ada di dua tempat – satu di direktori proyek, satu lagi di direktori /public
. Fail yang di direktori proyek untuk dipake di server, yang di /public
dipake di klien (browser).

Skrip start
dipake untuk jalanin production-server. Dijalanin sesudah deploy ke server. Kode aplikasi kita tempatnya di mana? Di direktori /app
.

Fail entry.client.jsx
& entry.server.jsx
, sesuai namanya masing-masing adalah skrip utama yang dijalanin di klien & server. root.jsx
adalah komponen utama (root) aplikasi kita.
Modul Route Utama
Buka root.jsx
, hapus semua isinya. Ganti dengan ini:
export default function App(){
return (
<h1>My App</h1>
)
}
Refresh browsernya.

Kalo kita view-source, isi dokumen HTML-nya ya cuma h1
itu. Nggak ada elemen lainnya.

Kita bisa tambahin elemen lainnya langsung di komponen ini.
import { Outlet } from "remix";
export default function App(){
return (
<html lang="en">
<head>
<title>My Remix Blog</title>
</head>
<body>
<Outlet />
</body>
</html>
)
}

Fungsi komponen <Outlet>
adalah untuk menampilkan route berdasarkan path. Kalo path-nya /
komponen ini menampilkan app/routes/index.jsx
(2).
Live Reload
Kalo kita perhatikan, setiap kali kita ngerubah kode halaman webnya nggak otomatis terupdate. Kita harus refresh halamannya. Untuk mengaktifkan fitur autoreload / livereload, kita harus pake komponen <LiveReload>
.
import { Outlet, LiveReload } from "remix";
export default function App(){
return (
<html lang="en">
<head>
<title>My Remix Blog</title>
</head>
<body>
<Outlet />
{ process.env.NODE_ENV === 'development' ? <LiveReload /> : null }
</body>
</html>
)
}
Kita masih perlu refresh manual sekali lagi tapi setelah itu jadi otomatis.
Remix nggak mendukung HMR, jadi kalo fitur itu lebih penting buat Anda sebaiknya pake NextJS aja karena dia punya fitur Fast Refresh.
Komponen Document & Layout
Struktur komponen App
bisa kita "rapihin". Kita buat komponen Document
yang isinya tag HTML dasar. Di komponen ini hanya title
nya yang bisa kita kustom.
function Document({children, title}){
return (
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
{children}
{ process.env.NODE_ENV === 'development' ? <LiveReload /> : null }
</body>
</html>
)
}
Satu komponen lagi di bawah Document
yang nanti isinya struktur/layout halaman. Sementara isinya hanya navigasi aja.
function Layout({children}){
return (
<>
<nav className="navbar">
<Link to="/" className="logo">Remix</Link>
<ul>
<li>
<Link to="/posts">Posts</Link>
</li>
</ul>
</nav>
<div className="container">
{children}
</div>
</>
)
}
import { Outlet, LiveReload, Link } from "remix";
export default function App(){
return <Document title="Remix Blog">
<Layout>
<Outlet />
</Layout>
</Document>
}
function Document({children, title}){
return (
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
{children}
{ process.env.NODE_ENV === 'development' ? <LiveReload /> : null }
</body>
</html>
)
}
function Layout({children}){
return (
<>
<nav className="navbar">
<Link to="/" className="logo">Remix</Link>
<ul>
<li>
<Link to="/posts">Posts</Link>
</li>
</ul>
</nav>
<div className="container">
{children}
</div>
</>
)
}
KomponenDocument
&Layout
ini opsional aja. Nggak pake juga nggak apa-apa. Tapi kalo pake struktur seperti di atas, kita bisa memisahkan komponen berdasarkan fungsinya – istilahnya "Separation of concerns".
Style & Links
Kita buat global.css
di direktori app/styles/
. Isinya begini (kopas dari repo tutorial di YT):
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400&display=swap');
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Poppins', sans-serif;
}
a {
text-decoration: none;
color: #000;
}
ul {
list-style: none;
}
p {
margin: 10px 0;
}
.container {
width: 100%;
max-width: 960px;
margin: auto;
padding: 0 30px;
}
.logo {
font-size: x-large;
text-transform: uppercase;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
background-color: #f4f4f4;
padding: 10px 30px;
text-transform: uppercase;
}
.navbar ul {
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar li {
margin-left: 20px;
}
.navbar ul li a {
color: #000;
text-transform: uppercase;
font-size: 14px;
font-weight: bold;
}
.navbar ul li a:hover {
color: #333;
border-bottom: 1px solid #333;
}
.btn {
display: inline-block;
background: #000;
color: #fff;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
font-size: 15px;
font-family: inherit;
}
.btn-reverse {
background: #fff;
color: #000;
border-color: #000;
}
.btn:focus {
outline: none;
}
.btn:active {
transform: scale(0.98);
}
.btn-block {
display: block;
width: 100%;
}
.btn-delete {
background: darkred;
color: #fff;
border-color: darkred;
font-size: 13px;
padding: 5px 10px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.page-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 30px;
}
.form-control {
margin: 20px 0;
}
.form-control label {
display: block;
}
.form-control input,
.form-control textarea {
width: 100%;
height: 40px;
margin: 5px;
padding: 3px 7px;
font-size: 17px;
}
.form-control textarea {
height: 200px;
}
.form-control-check {
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-check label {
flex: 1;
}
.form-control-check input {
flex: 2;
height: 20px;
}
.posts-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
justify-content: space-between;
align-items: flex-start;
}
.posts-list li {
padding: 20px;
border: 1px #333 solid;
border-radius: 5px;
}
.posts-list li h3 {
margin-bottom: 5px;
}
.error {
color: darkred;
}
.auth-container {
max-width: 600px;
margin: auto;
}
.auth-container h1 {
text-align: center;
}
.auth-container .pageHeader {
display: block;
}
.auth-container fieldset {
padding: 15px;
border-radius: 5px;
}
.auth-container fieldset label {
margin-right: 10px;
}
Kembali ke root.jsx
. Kita buat fungsi links()
yang balikannya adalah array
berisi objek yang dipake oleh Remix untuk men-generate <link>
.
import { Outlet, LiveReload, Link, Links } from "remix";
import globalStyleUrl from './styles/global.css'
export const links = () => ([
{
rel:'stylesheet',
href: globalStyleUrl
}
])
// kode yang lain
Di atas kita impor komponen baru namanya Links
(pake s
). Komponen ini kita pake di komponen Document
di bagian <head>
. Komponen ini otomatis mengambil balikan fungsi links()
di atas & membuat <link>
.
function Document({children, title}){
return (
<html lang="en">
<head>
<title>{title}</title>
<Links />
</head>
<body>
{/* kode yang lain*/}
</body>
</html>
)
}

Meta
Selain fungis links()
ada juga fungsi meta()
. Dari namanya udah jelas ya kalo fungsi ini nanti dipake Remix untuk men-generate tag <meta>
. Balikan fungsi ini adalah objek dengan key description
& keywords
. Untuk memasukkannya ke HTML kita pake komponen Meta
.
import { Outlet, LiveReload, Link, Links, Meta } from "remix";
import globalStyleUrl from './styles/global.css'
export const links = () => ([
// ... kode lainnya
])
export const meta = ()=>{
return {
description: 'Blog built with Remix',
keywords: 'remix, react, javascript'
}
}
// ... kode lainnya
function Document({children, title}){
return (
<html lang="en">
<head>
<title>{title}</title>
<Meta />
<Links />
</head>
<body>
{/* kode lainnya */}
</body>
</html>
)
}

Routing Berbasis Fail
Mirip NextJS, Remix mendukung routing berbasis fail. app/routes/index.jsx
disebut default route atau index route.
Remix pake React Router di internalnya, jadi kalo ada konsep atau istilah yang asing / susah dipahami silakan baca-baca ini dulu: React Router v6 - Concepts.
Di bagian navbar, ada link ke /posts
yang kalo kita buka munculnya 404 karena belum ada route-nya. Kita buat fail app/routes/posts.jsx
yang isinya:
import React from 'react';
function Posts() {
return (
<div>
<h1>Ini Route Posts</h1>
</div>
)
};
export default Posts;
Sekarang kalo kita buka localhost:3000/posts
, halamannya udah nggak 404 lagi:

Nested Routes
// file: app/routes/posts.jsx
import {Outlet} from 'remix';
function Posts() {
return (
<div>
<h1>Ini Route Posts</h1>
<Outlet />
</div>
)
}
export default Posts
// file: app/routes/posts/new.jsx
function NewPost() {
return (
<div>
Ini routes/new.jsx
</div>
)
}
export default NewPost
localhost:3000/posts/new

Kita bisa kosongin isi routes/posts.jsx
hanya ninggalin Outlet
aja. Sedangkan isinya yang lain kita pindah ke routes/posts/index.jsx
. Jadi fungsi komponen ini berubah mirip komponen Layout
.
// file: routes/posts.jsx
import { Outlet } from "remix";
function Posts() {
return (
<div>
<Outlet />
</div>
);
}
export default Posts;
// file: routes/posts/index.jsx
function PostsIndex() {
return (
<div>
<h1>Ini Route Posts Index</h1>
</div>
);
}
export default PostsIndex;
localhost:3000/posts

Jadi hubungannya posts.jsx
, Outlet
, posts/index.jsx
& posts/new.jsx
kurang lebih begini:

Route Dinamis
Untuk route dinamis, kita bikin fail yang namanya diawali $
. Bagian yang di belakangnya jadi nama parameter balikan hook useParams
. Jadi kalo kita bikin $postId.jsx
, bagian yang dinamis itu nanti bisa kita akses lewat params.postId
.
//file: routes/posts/$postId.jsx
import { useParams } from 'remix';
function Post() {
const params = useParams();
return (
<div>
<h1>Post id: {params.postId}</h1>
</div>
)
}
export default Post;
localhost:3000/posts/123

Bagian 1 cukup sampe sini dulu karena udah lumayan panjang. Nanti lanjut di bagian 2, kita bahas tentang Loader & Action.