Redraw

React opinionated bindings for Gleam. Use React directly from Gleam, with a friendly API that will never get in your way. Redraw tries to stick to React conventions, while providing idiomatic Gleam code. Write interoperable code between React and Gleam, reuse existing components, and leverage type-safe components & immutable data structures. Forget runtime errors, and write production-grade React components that just work.

Before you begin

Gleam developers come from different horizons, some from JavaScript, some from the BEAM, and the rest from various backgrounds, like Go, Python, or Java. To help all of them find their way through the language, Gleam is a user-friendly language that helps you take the right decisions. Similarly, Redraw embraces this philosophy by simplifying things as much as possible. However, some prior knowledge of frontend development is required.

Being a suite of React-bindings, Redraw assumes you know how React works and how to write an application. Redraw does not help you begin your journey in frontend development, and sometimes requires you to write JavaScript code or to understand how to connect existing React components. If you know what NPM is, what is a Single-Page Application, or how to use Vite, skip that section and jump directly to the beginning. Otherwise, there’s high chances you’ll need to learn all of this before jumping into Redraw! If you don’t feel comfortable enough with frontend development, JavaScript ecosystem, or anything related, you should take a look at Lustre. In contrast to Redraw, Lustre requires little to no knowledge of JavaScript to begin with. Lustre does also not require you to manage your runtimes like Node.js, and will help you build your dream application with HTML/CSS and other web technologies!
Lustre is user-friendly and focused on providing the best developer experience, right out-of-the-box! In case you’re hesitant between jumping into Redraw or starting Lustre, try the latter.

If, despite all of this, you still feel uncomfortable with frontend development but still need to use React (at work for example), don’t worry! Redraw will be there to support you. Start by reading the React documentation, and follow the amazing tutorials made by the React team. Before you know it, you’ll get comfortable with React, components and Redraw, and you’ll be able to build great applications!

Overview

Redraw is a package that lets you use React in a frontend-only Gleam project. By leveraging the entire JS ecosystem, Redraw helps you interop with existing existing React codebases, or allows you to build your custom codebase and cherry-pick the existing components you know and love! Redraw tries to stick with the React runtime and API, in order to let you reuse your existing React knowledge. Every skill used in React can be used in Redraw, and vice-versa. Wherever possible, Redraw also tries to stick with the Lustre API, once again to let you reuse your existing skills between Redraw and Lustre across different projects, or to help you migrate to Redraw from Lustre or from Lustre to Redraw.

Prerequisites

Redraw assumes you have node.js or equivalent as well as a modern package manager, i.e. npm, yarn, pnpm, or even bun. Redraw also assumes you’re using Vite or an equivalent as a build tool, and will not provide any interface to build your application. In the rest of this README, Vite will be used as an example. It’s up to you to use another bundler if you prefer. Redraw sticks with the modern, up-to-date frontend stack.

Getting started

Create the project, and add everything needed to make it work. Choose your preferred bundler to start. Create a Vite application, and choose to use JavaScript/TypeScript and React. Vite should bundle everything for you directly.

npm create vite@latest
yarn create vite
pnpm create vite
bun create vite

From now on, yarn will be used to illustrate the commands, it’s up to you to see how to use your desired package manager. Then, it’s time to set up the project correctly.

cd [project-name]
yarn install

# Install the Vite Gleam plugin. That plugin is required to tell Vite how to
# read Gleam files.
yarn add -D vite-gleam

# If you want to build the project on Vercel or Netlify.
# @chouquette/gleam provides a local version of the Gleam compiler installed in
# your node_modules. You can freely skip that step if you don't need to build
# your application remotely or if you're in control of the environment.
yarn add -D @chouquette/gleam

# Remove the files needed for `gleam new` to work.
mv README.md README.md.old
mv .gitignore .gitignore.old

# Set up the project.
gleam new .
gleam add redraw redraw_dom

Now that everything is set up, you need to add the vite-gleam plugin in vite.config.js. An example of a vite.config.js should look like this.

import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import gleam from "vite-gleam"

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), gleam()],
})

You’re good to go!

Writing Redraw components

Writing Redraw components is the same as writing React components, with one small difference: wrap the component in use props <- redraw.component_()!

import redraw
import redraw/dom/attribute
import redraw/dom/html

pub fn gleam_is_awesome() {
  use Nil <- redraw.component_("GleamIsAwesome")
  html.div([attribute.class("oh-yeah")], [
    html.text("Yeah, for sure")
  ])
}

While this might feel strange at first, you’ll get used to it quickly. To call the component, you’ll need to compose it in your component. In order to compose it, you need to call redraw.compose before defining a new component. This process “creates” every component in a Redraw application at startup, during what is called a “bootstrap phase”.

import redraw
import redraw/dom/attribute
import redraw/dom/client
import redraw/dom/html

pub fn main() {
  let assert Ok(root) = client.create_root("root")
  client.render(root, redraw.strict_mode([app(Nil)]))
}

fn app() {
  // Compose `gleam_is_awesome` here, before component creation,
  // during the bootstrap phase.
  use gleam_is_awesome <- redraw.compose(gleam_is_awesome())
  use Nil <- redraw.component_("Root")
  html.div([], [
    gleam_is_awesome(Nil),
  ])
}

fn gleam_is_awesome() {
  use Nil <- redraw.component_("GleamIsAwesome")
  html.div([attribute.class("oh-yeah")], [
    html.text("Yeah, for sure"),
  ])
}

And you know everything to create Redraw components!

Element, Component and passing props

React has two main built-in notions: elements and components. In React, an element is a part of the UI, and is the result of components renders. For example, a <div> is an element, but the execution of a Counter is also an element. On the other hand, a component is a function, accepting props, returning elements. Those elements can be the result of calling other components!

A component has always the same shape: it accepts some properties, or props, and returns an Element. The props can be of any nature: from a simple, basic String, to a complex record, anything is possible.

pub type CounterProps {
  CounterProps(
    count: Int,
    set_count: fn(fn(Int) -> Int) -> Nil,
  )
}

pub fn counter() {
  use props: CounterProps <- redraw.component_("Counter")
  html.button(
    [events.on_click(fn(_) { props.set_count(fn(count) { count + 1 }) })],
    [html.text("count is " <> int.to_string(props.count))],
  )
}

but also like this:

pub type CounterProps = #(Int, fn(fn(Int) -> Int) -> Nil)

pub fn counter() {
  use #(count, set_count): CounterProps <- redraw.component_("Counter")
  html.button(
    [events.on_click(fn(_) { set_count(fn(count) { count + 1 }) })],
    [html.text("count is " <> int.to_string(count))],
  )
}

or even props-less components:

pub fn counter() {
  use Nil <- redraw.component_("Counter")
  let #(count, set_count) = redraw.use_state(0)
  html.button(
    [events.on_click(fn(_) { set_count(fn(count) { count + 1 }) })],
    [html.text("count is " <> int.to_string(count))],
  )
}

React accepts mainly one shape of props: a JavaScript object. Don’t worry about it though, Redraw takes care of the hard translation work for you. When you send some Gleam props to a Redraw component, Redraw will convert the props in a suitable React format, and will convert them once again when going back in the Gleam world. Everything is done in a transparent way, and DevTools friendly: you can even see what is sent to React components directly in your browser!

Using contexts

Contexts are a powerful tool in your toolbox, and prevents what is called props drilling. In traditional React code, passing a lot of props to children components is often seen as a bad practice. In Gleam, passing arguments to function is considered as normal and absolutely usual. You should not be afraid to pass props. If you need lot of props on your component, you can implement something like a builder pattern and use your component as-is. However, sometimes you need to integrate with existing data, living outside of your props. Maybe to hold state, maybe to manage some effects outside of your current components. You can use context. To use them, all you need to do is to pass them during the bootstrap phase. Let’s take an example.

import redraw
import redraw/dom/attribute
import redraw/dom/client
import redraw/dom/html

pub fn main() {
  // Create a context.
  let assert Ok(root) = client.create_root("root")
  client.render(root, redraw.strict_mode([app(Nil)]))
}

type Shared = #(Int, fn (Int) -> Nil)

fn app() {
  // Create your context during the bootstrap phase.
  let default_state: Shared = #(0, fn(_) { Nil })
  let my_context: redraw.Context(Shared) = redraw.create_context_(default_state)
  // Pass your context during the bootstrap.
  use gleam_is_awesome <- redraw.compose(gleam_is_awesome(my_context))
  use Nil <- redraw.component_("Root")
  let #(state, set_state) = redraw.use_state(0)
  // Provide the context in the component tree to make it accesible below.
  redraw.provider(my_context, #(state, set_state), [
    html.div([], [
      gleam_is_awesome(Nil),
    ]),
  ])
}

// Component factory have to accept the context.
fn gleam_is_awesome(my_context: redraw.Context(Shared)) {
  use Nil <- redraw.component_("GleamIsAwesome")
  // Access the context inside your component.
  let #(state, set_state) = redraw.use_context(my_context)
  html.div([attribute.class("oh-yeah")], [
    html.text("Yeah, for sure"),
  ])
}

If you’re used to Erlang/OTP, you will recognise a similar pattern in gleam_otp, with named processes, where you need to instanciate your names in your main function and pass it along the way!

Some reminders on hooks

Hooks are powerful, but they can be dangerous to use. You should never use hooks outside of custom hooks (functions named use_[something]) or outside of components! This means you should never use something like use_effect or use_state outside of the body of component_. If you break that rule, while it could seem to work, it could at any time break the runtime, and explode. So keep that rule anytime: no hooks outside of custom hooks or component body.

Type-checking of hooks

You may notice that hooks in React often use a dependencies array, to determine if they have to rerun or not. This is fully supported by Redraw, and leverages Gleam’s abilities! Always pass a tuple of dependencies to hooks. No type-checking is done at this stage, and probably none will be implemented later, exactly like it’s done with React currently. Be careful to provide the correct dependencies.

import gleam/io
import redraw
import redraw/attribute
import redraw/html

fn gleam_is_awesome() {
  use <- redraw.component__("GleamIsAwesome")
  redraw.use_effect(fn() {
    io.println("Hello from component!")
  }, #()) // Passing an empty tuple here is like passing [] in JavaScript.
  html.div([attribute.class("oh-yeah")], [
    html.text("Yeah, for sure")
  ])
}

Using external components

React is widely used everywhere. It means a lot of components are already usable out-of-the-box. To do this, you’ll have to write some FFI code. The FFI code is written in usual JavaScript code, using JSX.

More to come later…

Is Redraw production-ready?

Of course! Redraw is more than production-ready: it is already used in production! While Redraw started as a side-project by a single developer, it is now developed with the help of Steerlab, and is heavily used throughout the company for various things, from single-page applications to Chrome Extensions.

Redraw supports the latest version of React, and follows its release cycle: Redraw will never break backwards-compatibility outside of the existing breaking changes of React. You can expect Redraw to not break your codebase in a suspicious way, and most new features are pushed as minor changes.

Future plans — Redraw Linter

At the moment, Redraw leverages the Gleam compiler and does not offer linter support for critical parts like hooks dependencies. In the future, a complementary linter is planned, and should bridge that gap between Gleam and React. While the Gleam compiler provides all useful information about Gleam code, the Redraw linter will focus on specific Redraw requirements.

Future plans — Fast Refresh support

More to come later…

Future plans — React Native support

React is an isolated package, and renderers can be various. Redraw has successfully been used with Raycast. You can also easily add a redraw_native package, and provide an interface for native components. Everything can be done quickly and easily, because the entire package is written with as little JavaScript code as possible. Everything should work almost out-of-the-box, because React is already working there. You should take inspiration from how redraw/html works, as it will work in the exact same way. It’s only a matter of providing the correct Element to jsx.

Why redraw.component_ instead of redraw.component?

You may see redraw.component also exists in the code. It’s because Redraw started with multiple ways to create components, and that included redraw.component, as well a redraw.element and redraw.standalone. It was merely a first try, and some exploration to make React work with Gleam. Unfortunately, it was a messy way to implement bindings, and an unsafe one on top of that. Those functions are now deprecated, and wil be remove in the future. Meanwhile, redraw.component_ has been introduced to provide support for the new component API, and will fully replace redraw.component when the existing redraw.component will be removed.

Contributing

Enjoying Redraw? Contributions are welcome! Feel free to open a PR, or open an issue!

Search Document