reactea

package module
v0.5.1-0...-056a159 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 5 Imported by: 0

README

Reactea

Latest build Codecov Go Reference Go Report Card

With the release of bubbletea v2 I'm archiving this project - it's not actively maintained anymore

Rather simple Bubbletea companion for handling hierarchy, support for lifting state up.
It Reactifies Bubbletea philosophy and makes it especially easy to work with in bigger projects.

For me, personally - It's a must in project with multiple pages and component communication

Check our quickstart right here or other examples here!

go get -u github.com/Hayao0819/reactea

General info

The goal is to create components that are

  • dimensions-aware (especially unify all setSize conventions)
  • scallable
  • more robust
  • easier to code
  • all of that without code duplication

Extreme performance is not main goal of this package, however it should not be that far off actual Bubbletea which is already blazing fast. Most info is currently in source code so I suggest checking it out

Always return reactea.Destroy instead of tea.Quit in order to follow our convention (that way Destroy() will be called on your components)

Quickstart

Reactea unlike Bubbletea implements two-way communication, very React-like communication.
If you have experience with React you are gonna love Reactea straight away!

In this tutorial we are going to make application that consists of 2 pages.

  • The /input (aka index, in reactea default) page for inputting your name
  • The /displayname page for displaying your name
Lifecycle

More detailed docs about component lifecycle can be found here, we are only gonna go through basics.

Reactea component lifecycle consists of 4 methods (while Bubbletea only 3)

Method Purpose
Init() tea.Cmd It's called first. All critical stuff should happen here. It also supports IO through tea.Cmd
Update(tea.Msg) tea.Cmd It reacts to Bubbletea IO and updates state accordingly
Render(int, int) string It renders the UI. The two arguments are width and height, they should be calculated by parent
Destroy() It's called whenever Component is about to end it's lifecycle. Please note that it's parent's responsibility to call Destroy()

Let's get to work!

The /input page

/pages/input/input.go

type Component struct {
    reactea.BasicComponent                // It implements all reactea's core functionalities

    // Props
    SetText func(string)

    textinput textinput.Model             // Input for inputting name
}

func New() *Component {
    return &Component{textinput: textinput.New()}
}

func (c *Component) Init() tea.Cmd {
    return c.textinput.Focus()
}

func (c *Component) Update(msg tea.Msg) tea.Cmd {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        if msg.Type == tea.KeyEnter {
            // Lifted state power! Woohooo
            c.SetText(c.textinput.Value())

            // Navigate to displayname, please
            reactea.SetRoute("/displayname")
            return nil
        }
    }

    var cmd tea.Cmd
    c.textinput, cmd = c.textinput.Update(msg)
    return cmd
}

// Here we are not using width and height, but you can!
func (c *Component) Render(int, int) string {
    return fmt.Sprintf("Enter your name: %s\nAnd press [ Enter ]", c.textinput.View())
}
The /displayname page

/pages/displayname/displayname.go

import (
 "fmt"
)

// Our prop(s) is a string itself!
type Props = string

// Stateless components?!?!
func Render(text Props, width, height int) string {
    return fmt.Sprintf("OMG! Hello %s!", text)
}
Main component

/app/app.go

type Component struct {
    reactea.BasicComponent                // It implements all reactea's core functionalities

    mainRouter *router.Component

    text string // The name
}

func New() *Component {
    c := &Component{}

    // Does it remind you of something? react-router!
    c.mainRouter = router.NewWithRoutes(router.Routes{
        "default": func(router.Params) reactea.Component {
			component := input.New()

			component.SetText = c.setText

			return component
		},
		"/displayname": func(router.Params) reactea.Component {
            // RouteInitializer requires Component so we have to convert
            // Stateless component (renderer) to Component
			return reactea.Componentify(displayname.Render, c.text)
		},
    })

    return c
}

func (c *Component) Init() tea.Cmd {
    return c.mainRouter.Init()
}

func (c *Component) Update(msg tea.Msg) tea.Cmd {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        // ctrl+c support 
        if msg.String() == "ctrl+c" {
            return reactea.Destroy
        }
    }

    return c.mainRouter.Update(msg)
}

func (c *Component) Render(width, height int) string {
    return c.mainRouter.Render(width, height)
}

func (c *Component) setText(text string) {
    c.text = text
}
Main

main.go

// reactea.NewProgram initializes program with
// "translation layer", so Reactea components work
program := reactea.NewProgram(app.New())

if _, err := program.Run(); err != nil {
    panic(err)
}

Component lifecycle

Component lifecycle image

Reactea component lifecycle consists of 4 methods (while Bubbletea only 3)

Method Purpose
Init() tea.Cmd It's called first. All critical stuff should happen here. It also supports IO through tea.Cmd
Update(tea.Msg) tea.Cmd It reacts to Bubbletea IO and updates state accordingly
Render(int, int) string It renders the UI. The two arguments are width and height, they should be calculated by parent
Destroy() It's called whenever Component is about to end it's lifecycle. Please note that it's parent's responsibility to call Destroy()

Reactea takes pointer approach for components making state modifiable in any lifecycle method\

Notes

Update() IS NOT guaranteed to be called on first-run, Init() for most part is, and critical logic should be there

Stateless components

Stateless components are represented by following function types

Renderer[TProps any] ProplessRenderer DumbRenderer
Properties
Dimensions
Arguments TProps, int, int int, int

There are many utility functions for transforming stateless into stateful components or for rendering any component without knowing its type (reactea.RenderAny)

Routes API

Routes API allows developers for easy development of multi-page apps. They are kind of substitute for window.Location inside Bubbletea

reactea.CurrentRoute() Route

Returns current route

reactea.LastRoute() Route

Returns last route

reactea.WasRouteChanged() bool

returns LastRoute() != CurrentRoute()

Reactea Routes now support params

Params have been introduced in order to allow routes like: /teams/123/player/4

Params have to follow regex ^:.*$
^ being beginning of current path level (/^level/)
$being end of current path level (/level$/)

Note that params support wildcards with single :, like /teams/:/player. /teams/123/player, /teams/456/player etc will be matched no matter what and param will be ignored in param map.

Router Component

router.Component is a basic router. Give it routes with router.NewWithRoutes(router.Routes{...}) or the Routes field on New().

It matches route placeholders, including the params and wildcards described above. When more than one placeholder matches, the most specific wins: literal segments beat params, which beat catch-alls. Relative navigation is a separate concern, handled by reactea.Navigate.

router.Routes

router.Routes is map[string]RouteInitializer keyed by route placeholder.

RouteInitializer is a func(router.Params) reactea.Component that builds the component for a matched route.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CurrentRoute

func CurrentRoute() string

func Destroy

func Destroy() tea.Msg

Destroys app before quiting

func LastRoute

func LastRoute() string
func Navigate(target string) tea.Cmd

func NewProgram

func NewProgram(root Component, options ...tea.ProgramOption) *tea.Program

Note: Return type is *tea.Program, Reactea doesn't have it's own wrapper (reactea.Program) type, yet (?)

func RenderAny

func RenderAny[TProps any, TRenderer AnyRenderer[TProps]](renderer TRenderer, props TProps, width, height int) (result string)

Renders all AnyRenderers in one function

Note: If you are using ProplessRenderer/DumbRenderer just pass reactea.NoProps{} or struct{}{}

Note: Using named return type for 100% coverage

func RenderDumb

func RenderDumb[TRenderer AnyProplessRenderer](renderer TRenderer, width, height int) (result string)

Handles rendering of all AnyProplessRenderers in one function

Note: Using named return type for 100% coverage

func Rerender

func Rerender() tea.Msg

Utility tea.Cmd for requesting rerender

func RouteMatchesPlaceholder

func RouteMatchesPlaceholder(route string, placeholder string) (map[string]string, bool)

Checks whether route (e.g. /teams/123/12) matches placeholder (e.g /teams/:teamId/:playerId) Returns map of found params and if it matches Params have to follow regex ^:.*$ ^ being beginning of current path level (/^level/) $ being end of current path level (/level$/)

Note: Entire matched route can be accessed with key "$" Note: Placeholders can be optional => /foo/?:/?: will match foo/bar and foo and /foo/bar/baz Note: The most outside placeholders can be optional recursive => /foo/+?: will match /foo/bar and foo and /foo/bar/baz Note: It allows for defining wildcards with /foo/:/bar Note: Duplicate params will result in overwrite of first param

func SetRoute

func SetRoute(target string) tea.Cmd

func WasRouteChanged

func WasRouteChanged() bool

func WithRoute

func WithRoute(route string) func(*tea.Program)

func WithoutInput

func WithoutInput() func(*tea.Program)

Useful for testing on Github Actions, by default Bubbletea would try reading from /dev/tty, but on Github Actions it's restricted resulting in error

Types

type AnyProplessRenderer

type AnyProplessRenderer interface {
	ProplessRenderer | DumbRenderer
}

type AnyRenderer

type AnyRenderer[TProps any] interface {
	Renderer[TProps] | AnyProplessRenderer
}

AnyRenderer is the set of stateless renderer function types.

type BasicComponent

type BasicComponent struct{}

Basic component that implements all methods required by reactea.Component except Render(int, int)

func (*BasicComponent) Destroy

func (c *BasicComponent) Destroy()

func (*BasicComponent) Init

func (c *BasicComponent) Init() tea.Cmd

func (*BasicComponent) Update

func (c *BasicComponent) Update(msg tea.Msg) tea.Cmd

type Component

type Component interface {

	// Init() Is meant to both initialize subcomponents and run
	// long IO operations through tea.Cmd
	Init() tea.Cmd

	// It's called when component is about to be destroyed
	Destroy()

	// Typical tea.Model Update(), we handle all IO events here
	Update(tea.Msg) tea.Cmd

	// Render() is called when component should render itself
	// Provided width and height are target dimensions
	Render(int, int) string
}

func Componentify

func Componentify[TProps any, TRenderer AnyRenderer[TProps]](renderer TRenderer, props TProps) Component

Componentifies AnyRenderer Returns uninitialized component with renderer taking care of .Render()

func ComponentifyDumb

func ComponentifyDumb[TRenderer AnyProplessRenderer](renderer TRenderer) Component

Componentifies AnyProplessRenderer Returns uninitialized component with renderer taking care of .Render()

func StaticComponent

func StaticComponent(content string) Component

type DumbRenderer

type DumbRenderer = func() string

Doesn't have state, props, even scalling for target dimensions = DumbRenderer, or Stringer

type InvisibleComponent

type InvisibleComponent struct{}

Utility component for displaying empty string on Render()

func (*InvisibleComponent) Render

func (c *InvisibleComponent) Render(int, int) string

type NoProps

type NoProps = struct{}

Alias for no props

type ProplessRenderer

type ProplessRenderer = func(int, int) string

SUPEEEEEER shorthand for components

func PropfulToLess

func PropfulToLess[TProps any](renderer Renderer[TProps], props TProps) ProplessRenderer

Wraps propful into propless renderer

type Reactified

type Reactified[TModel tea.Model] struct {
	BasicComponent

	Model TModel
}

Reactifies a whole Bubbletea model into a reactea component. This is for types that satisfy tea.Model, not for bubbles widgets: a widget's Update returns its own concrete type rather than tea.Model, so it never satisfies the interface. Hold widgets as fields of your own component instead.

func Reactify

func Reactify[TModel tea.Model](model TModel) *Reactified[TModel]

Reactify wraps a tea.Model as a reactea.Component.

func (*Reactified[TModel]) Init

func (c *Reactified[TModel]) Init() tea.Cmd

func (*Reactified[TModel]) Render

func (c *Reactified[TModel]) Render(width, height int) string

func (*Reactified[TModel]) Update

func (c *Reactified[TModel]) Update(msg tea.Msg) tea.Cmd

type Renderer

type Renderer[TProps any] = func(TProps, int, int) string

Ultra shorthand for components = just renderer One could say it's a stateless component Also note that it doesn't handle any IO by itself

It's a type alias, so a Renderer[TProps] value and a plain func(TProps, int, int) string are interchangeable without a conversion.

type RerenderMsg

type RerenderMsg struct{}

type RouteUpdatedMsg

type RouteUpdatedMsg struct {
	Original string
}

type Size

type Size int

func (*Size) Remaining

func (size *Size) Remaining() Size

func (*Size) Sub

func (size *Size) Sub(n Size) *Size

func (*Size) SubUnsafe

func (size *Size) SubUnsafe(n Size) *Size

func (*Size) Take

func (size *Size) Take(n Size) Size

func (*Size) TakeUnsafe

func (size *Size) TakeUnsafe(n Size) Size

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL