Skip to content

Nexus.R

Miu edited this page Dec 2, 2023 · 20 revisions

React Native + Svelte for Discord Bots. Nexus.R (React) is a Kotlin-only feature that enables developers to respond to slash commands more intuitively akin to web development. In addition, Nexus.R supports a simple and minimal state system (writable) that enables you to update the Discord message by simply changing a writable's value.

Features

  • Write-once, Use Everywhere
    • Nexus.R allows you to easily reuse responses whether it was written for slash commands or for messages with hardly any changes.
  • States in Discord Bot
    • Nexus.R introduces the infamous states of the JavaScript world into Discord bots, allowing you to create messages that will re-render itself upon different state changes, just like a reactive website!
  • Webdev-like Feel
    • In order to make Discord bot development more accessible to even web developers, we've made the feature feels like writing web code (JSX) but simpler!

Use-cases

Nexus.R is best to use when you want the following:

  1. Incredible Developer Experience: Nexus.R is incredibly simple and powerful, allowing you to do many things easily with ease.
  2. Simple Reactivity: Paginations, and things that are reactive are best powered by Nexus.R, in fact, here's an example of a simple pagination system written with Nexus.R.

Learn by Code

Nexus.R is simple enough for most developers that you can simply read the code and you'll understand it. If you prefer to learn by reading examples, we recommend reading the examples.

Learn by Wiki

In this wiki page, we'll try to go in-depth into Nexus.R and how we can use it effectively to build a beautiful and reactive Discord bot. Nexus.R is easier to understand for web developers who are familiar with JSX or Svelte as this essentially is the same but for Kotlin Discord bots and that's what makes it beautiful, but before we begin with our guide, we have to understand one thing and that is: Nexus.R will never be as performant and memory efficient as manually doing things yourself, but it will be more easier on the developer experience.

To expand upon the above statement, it's important to acknowledge that Nexus.R breaks the traditional philosophy of Nexus in which abstractions are kept at a minimal. Nexus.R builds many abstractions upon the Javacord way in order to optimize the most for developer experience, which in turn, means it creates a lot of objects, which takes a really tiny bit more memory and time.

According to our testing though, Nexus.R doesn't impact a lot as the code is designed enough that it doesn't create enough objects that it slows down things, in fact, most rendering completes in about 1-5 milliseconds and that's very tiny. Now that we know such fact, if you still want to use Nexus.R to favor better developer experience, then let's start reading!

Rendering with Nexus.R

Nexus.R supports practically everything where you can create a message whether it'd be an Interaction, a User, a Text Channel or anything that can be messaged. In this example, we'll be looking at things more towards Slash Commands, but you can read the examples to see how it'd look for other methods.

You can start the journey by entering into Nexus.R (React) scope by calling the R method.

fun onEvent(event: NexusCommandEvent) {
    event.R { }
}

In this scope, we can start defining the message by creating how it looks inside the render function. How about we create a simple embed that tells people that this message was rendered with Nexus.R, it should also be yellow in color and have a title and a timestamp that should be when the message was sent?

fun onEvent(event: NexusCommandEvent) {
    event.R {
        render {
            Embed {
                Title("Rendered with Nexus.R")
                SpacedBody(
                    p("This message was rendered with Nexus.R.")
                )
                Color(java.awt.Color.YELLOW)
                Timestamp(Instant.now())
            }
        }
    }
}

With that, the entire command is ready to go and you can simply execute the command and it should render the following message:

Rendered Message Example

But that's pretty simple though, how about we go even further and dive into reactivity and buttons?

Reactivity

Nexus.R can help you build incredibly reactive bots, and one way that it does this is by supporting its own state mechanism. For those familiar with web development i.e. Svelte or React, this will click with you easily, but for those who didn't, states are basically properties that can signal things when the value has changed, in this case, Nexus.R listens to changes in the value to re-render the message (with a debounce of React.debounceMillis, by default 25 milliseconds, to ensure any other states changed in that time is also batched to prevent unnecessary re-renders).

You can create states in Nexus.R by creating a writable and there are two ways to go about this:

  1. Use the writable method inside the R scope (includes a subscription that re-renders the message whenever the state changes).
  2. Create a new React.Writable using the React.Writable constructor (doesn't re-render the message unless you add the subscription needed).

We'll be showing both ways in this guide and how we can allow re-rendering in the second way. But to begin, how about we expand our original example and include a button that increments a number every click, the traditional example of reactivity?

fun onEvent(event: NexusCommandEvent) {
    event.R {
        var clicks by writable(0)
        render {
            Embed {
                Title("Rendered with Nexus.R")
                SpacedBody(
                    p("This message was rendered with Nexus.R."),
                    p("The button has been clicked ") + bold("$clicks times.")
                )
                Color(java.awt.Color.YELLOW)
                Timestamp(Instant.now())
            }
            Button(label = "Click me!") {
                it.buttonInteraction.acknowledge()
                clicks += 1
            }
        }
    }
}
2023-10-18.00-39-29.mp4

In the above example, we can see three new things:

  1. clicks variable that is delegated to writable(0) using the by operator of Kotlin.
  2. a new p text that shows us how many clicks were made.
  3. a Button that increments clicks by one for each click made.

As we can see from the example, we define states by using the writable(initialValue) method that is only available in the R scope. The method itself simply creates a new React.Writable using its public constructor and then uses the expand(Writable) function inside the R scope to add the subscription needed to re-render.

In order to understand this though, we have to understand the internal working of a Writable. Simply, a Writable is basically an AtomicReference but with the setter also executing subscriptions asynchronously. A Subscription is simply a listener that is waiting to be executed whenever the value of the Writable changes. That's the simplest way to explain a Writable and you can see this when you exit out of delegation where you'll see methods such as get, set, update and subscribe.

This is also important to know as there is a limitation in delegated Writable and that is that we cannot pass them to other functions outside of our R scope simply because for several reasons:

  1. Kotlin doesn't support mutable function arguments.
  2. Delegated properties passes the value they get from getValue which means we don't get the Writable instance.

To know how to resolve this, it's recommended to simply read the code example which shows everything in as much detail as possible:

Anyhow, we've derailed a bit, going back, to create a state, you simply create a Writable but there is also two ways that you can do so and that is by creating a variable with the value of a Writable or by using Kotlin's delegation.

Kotlin's Delegation

var clicks by writable(0)

clicks = 1 // 1
clicks += 1 // 2

println(1 + clicks) // 3

Variable with a Writable value

val clicks = writable(0)

// Primitives such as numerical types, lists and strings have these operator overloads
// to help you make things feel native.
clicks set 1 // 1
clicks += 1 // 2

println(1 + clicks.get()) // 3

// Pre-operator overloads, this is still how you should do it when you have a type 
// that isn't supported with operator overloading.
clicks.set(1) // 1
clicks.update { it + 1 } // 2

println(1 + clicks.get()) // 3

What's the difference between the two? In the first way, we use Kotlin's delegation method which means that we get to use the native setter and getter operators as the delegation overrides the set() and get() of the property while the second way is basically just a Writable instance and nothing more than that.

As mentioned earlier, we cannot pass the delegated one easily due to Kotlin restrictions, therefore, generally, when you need to pass a state from one to another and still have the native setter and getter operators, you have to create the Writable first then another variable that delegates to that. As an example, how about we have an increment function that increments the clicks state?

val clicksDelegate = writable(0)
var clicks by clicksDelegate

fun increment(clicks: React.Writable<Int>) {
   clicks++
}

Now that we understand the fundamentals of state in Nexus.R, there is still something that one has to know and that is never create a state inside render and that is primarily because render function is called every time a state changes, therefore, any re-rendering will effectively delete that state. This is important to know as we proceed to our next topic, Components.

Components

Having reusable code is always an important key factor to any program, and this also applies to Discord bots, as such, knowing how to create components in Nexus.R is equally as important. To build components in Nexus.R, you simply create an extension function over React.Component and start rendering what you like. Do note that as this is a function, the rule of how to pass state over to functions is applied and also the rule of never create a state inside render.

As an example, how about we make our previous example into a component?

fun React.Component.ClickMe(clicksDelegate: React.Writable<Int>) {
    var clicks by clicksDelegate
    
    Embed {
        Title("Rendered with Nexus.R")
        SpacedBody(
            p("This message was rendered with Nexus.R."),
            p("The button has been clicked ") + bold("$clicks times.")
        )
        Color(java.awt.Color.YELLOW)
        Timestamp(Instant.now())
    }
    Button(label = "Click me!") {
        it.buttonInteraction.acknowledge()
        clicks += 1
    }
}

fun onEvent(event: NexusCommandEvent) {
    event.R {
        val clicksDelegate = writable(0)
        render {
            ClickMe(clicksDelegate)
        }
    }
}

As we can see, we are applying the rule of how to pass state to another function and also applying the rule of not creating a state inside render itself (which is where Components are called). As render itself has a parameter that is a receiver function of React.Component, that means we can simply access any extension function or function under React.Component and that becomes the way how we build components.

You can then use the component on message commands, User private message or anywhere that Nexus.R can be used and it'll render the same way as any other.

Derivatives

Nexus.R supports derivating a state's value from another state using the derive function. Although, there isn't many use-cases to this, but it can be helpful in some specific scenarios.

val number = writable(1) 
val multiplied = parent.derive { it * 2 }

Data Fetching

Nexus.R provides two methods that can be used to render things:

  1. onInitialRender: calls the task on the very first time the message is rendered (this is executed before render itself)
  2. onRender: calls the task on the first time and every succeeding renders (this is executed before render itself).

Additionally, you can also simply execute functions under the R scope which happens to just be a function. To understand more about the three ways, simply read the example itself which best demonstrates this topic.

Lifetimes

An important part of memory management of Nexus.R is lifetimes. Unlike browser frameworks, we have to worry a lot more about the garbage collectibility of objects as our memory usage isn't just per-user, it's a per-bot (or per-node, if scaling via multi-node), which means that we have to clean up memory as soon as possible to prevent an Out Of Memory issue.

In a case such as Nexus.R, a lot of times, we do not know how long this reactivity should last. Do we expect the user to use it again after some time, do we intend to have this message be reactive for as long as the bot live, but if we do that, what about our memory usage? There are a lot of what-ifs when it comes to this, but it is very important if we want to build a cost-effective and efficient bot.

Nexus.R has two destruction mechanisms and those are:

  1. Lifetime
  2. Message Delete

When you have the right intent enabled (GUILD_MESSAGES and PRIVATE_MESSAGES, not to be confused with MESSAGE_CONTENT), Nexus will listen to MessageDelete events for the associated message and once it detects that the message associated with Nexus.R is deleted, it will conduct self-destruction.

At the same time, Nexus.R will also start a countdown up until a specific lifetime where the instance will self-destruct. This lifetime can be configured right on the instance that you call Nexus.R itself, for example:

fun on(event: NexusCommandEvent) {
  event.R(lifetime = 1.hours) {
    // todo
  }
}

By default, it has a lifetime of an hour, which is more than enough for most users to be already done with the reactivity. If you want to configure it so that it doesn't self-destruct unless you deliberately call for destroy to happen, you can use the Duration.INFINITE instead which will tell Nexus.R to not schedule a self-destruction.

A self-destruction is Nexus.R's way of clearing all references to it and its children. It will unsubscribe all states associated with it, remove all listeners associated, empty the React.Components associated, and throw Nexus.R into a practically clean state that allows it to be garbage-collectible. As such, this self-destruction must happen.

As stated earlier, you can also call destroy yourself which will initiate the self-destruction process. This is recommended when you know a specific moment where you can freely self-destruct, this also allows you to free memory even earlier.

To get started with Nexus, we recommend reading the following in chronological:

  1. Installation & Preparing Nexus
  2. Designing Commands
  3. Command Interceptors
  4. Additional Features (Subcommand Router, Option Validation)
  5. Context Menus
  6. Command Synchronization

You may want to read a specific part of handling command and middleware responses:

You can also read about additional features of Nexus:

You can read about synchronizing commands to Discord:

For more additional performance:

Additional configurations:

Clone this wiki locally