DEV Community

Cover image for KotlinJS and MULTIPLE State Hooks (Part two)
skalable-dev
skalable-dev

Posted on

KotlinJS and MULTIPLE State Hooks (Part two)

At sKalable we are just in love with Kotlin! We really strive to make all things Kotlin simple, fun, and a breeze to work with :D <3 KotlinJS is no exception to our mission. ๐Ÿ˜ƒ โค๏ธ

Following on from our Part 1 tutorial of KotlinJS and State Hooks , that covers State as a singular, Hooks and the best practices for working with functional components, we want to take things further and delve into using multiple State Hooks or State values in our code. Using multiple State Hooks can be advantageous as you can split them for different uses, better manage properties that change independently of each other... but with certain caveats...

Helping to define the structure and improve the readability of our code so much more!

(Before delving deeper into multiple State Hooks, feel free to take a look at Part 1 of this article as a refresher ๐Ÿ˜Š )

Let's give it a go!

Multiple States in action

Check out this diagram where we can see multiple states in action!

The problem with Fixed State interfaces

Below is an example looking at some issues of setting an interface object as a useState type value.

/**  
 * EnterWordStateOriginal is used as a State Object * 
 * 
 * @property word is the word that is updated when the input changes.  
 * @property updateClicked is the property that is updated when the button gets clicked.  
 * @property updatedWord the new word that has been updated.  
 */
 external interface EnterWordStateOriginal {  
  var word: String  
  var updateClicked: Int  
  var updatedWord: String  
}

/**  
 * enterWord is a functional component that renders an input, a button and a label. 
 */
private val enterWord = functionalComponent<RProps> {  
/**  
 * When we first declare the useState, the default value is set in the parenthesis. 
 * This will be held in enterWordState. 
 * 
 * To modify this we use the setEnterWord function, delegated with the [by] key. 
 * To clarify enterWord is treated as a var with a getter and a setter. 
 */
var enterWordState by useState<EnterWordStateOriginal> {  
    object : EnterWordStateOriginal {  
        override var word = ""  
        override var updateClicked = 0  
        override var updatedWord = ""  
    }
}

//... functional / render code .

/**
 * Setting a state object requires the full object to be set in functional
 * components. This can become very verbose, incredibly quickly.     
 */  
    //... HTML Input handler 
    onChangeFunction = { event -> 
         enterWordState = object : EnterWordState {  
             override var word = (event.target as HTMLInputElement).value  
             override var updateClicked = enterWordState.updateClicked 
             override var updatedWord = enterWordState.updatedWord
         }  
    }
Enter fullscreen mode Exit fullscreen mode

It might not be the most elegant looking code, but it works. When using state objects in functional components, you'll see there is no requirement to set the RState type on the component itself. This is different to how Class Components work for instance.

Unlike with Class Components, Functional Components do not have a setState {} function to map old state to new state (This is not the case for Props though) . Nor do they require knowledge of the state in their construction either.

We apply the concept of state to a functional component through React Hooks. Using hooks, the component now has the capability to handle state changes. There is a readability issue regarding this though...

Code should be clean, easy to write and read. Unfortunately, using state objects in functional components doesn't help us achieve that with the approach above.

Below, we see that in order to set state we must initialise the full object every time. This requires us to manually set the values of the previous states that don't change.

/** 
 * Setting a state object requires the full object to be set in functional 
 * components. 
 * This can become very verbose, incredibly quickly.     
 */  
 onChangeFunction = { event ->   
     enterWordState = object : EnterWordState {  
         override var word = (event.target as HTMLInputElement).value  
         override var updateClicked = enterWordState.updateClicked 
         override var updatedWord = enterWordState.updatedWord
     }  
 }
Enter fullscreen mode Exit fullscreen mode

Ughhh.... We can't be adding this everywhere each time we update the state. Ok, time to clean this up a little.

Dividing state strategies

There is no real "right" or "wrong" method of tackling division of state, its mostly down to personal preference and use case for each component (although some strategies can look ridiculous such as above).

Larger states have a different challenge than smaller states. Below we outline various strategies and how to decide which is best approach for the components needs and number of states you require.

Dividing by individual values โ€” Multi-State component

For small state interfaces that can be described as having no more than three vars in a State, prefer an individual state for each value.

/**  
 * Primitive State based on a String and an Int. The types are inferred. 
 */
var wordState by useState { props.word } // inferred String 
var updatedClickedState by useState { 0 } // inferred Int
Enter fullscreen mode Exit fullscreen mode

This allows for clean and easy methods to update and read the required state.

updatedClickedState += 1 // update the value by 1  
Enter fullscreen mode Exit fullscreen mode

What about larger states? How should we handle those?

Keeping composition / context as a Single State

If you find yourself writing a lot of repetitive code, always think of DRY Principles. We tend do repeat a lot of the state construction just to update a single value when using state as a single object. A separate function within the functional component can help resolve this issue.

Builder functions can be used to create new objects and handle the mapping of values. Kotlin has a feature called default arguments allowing parameter values to have the default value to the corresponding states value. Automatically the parameters will have the value if one has not been provided by the caller.

Applying this approach allows for cleaner code. It does require "boilerplate" in the form of a separate function for each state interface in functional components with interface states.

Though it's a better approach to mapping, it's still not ideal nor efficient when writing components.

/**  
 * When we first declare the useState, the default value is set in the parenthesis. 
 * This will be held in enterWordState. 
 * 
 * To modify this we use the setEnterWord function, delegated with the [by] key.
 * To clarify enterWord is treated as a var with a getter and a setter. 
 */
var enterWordState by useState<EnterWordStateWithBuilder> {  
    object : EnterWordStateWithBuilder {  
        override var word = ""  
        override var updateClicked = 0  
        override var updatedWord = ""  
    }  
}

/**  
 * In this approach we use utility builders within the functional component to set state as a single 
 * line when interfaces are used as state holders.
 * Using default params pointed at [enterWordState] allows for cleaner setters. 
 * 
 * @param word โ€” Has a default of the current state word  
 * @param updateClicked โ€” Has a default of the current state updateClicked  
 * @param updatedWord โ€” Has a default of the current state updatedWord  
 */
fun setWordState(  
    word: String = enterWordState.word,  
    updateClicked: Int = enterWordState.updateClicked,  
    updatedWord: String = enterWordState.updatedWord  
) {  
    enterWordState = object : EnterWordStateWithBuilder {  
        override var word = word  
        override var updateClicked = updateClicked  
        override var updatedWord = updatedWord  
    }  
}
Enter fullscreen mode Exit fullscreen mode

The result of creating a utility builder for the function state is a clean setter.

/**  
 * Setting a state object requires the full object to be set in functional 
 * components. This can become very verbose, incredibly quickly. 
 */  
onChangeFunction = { event -> 
    setWordState(word = (event.target as HTMLInputElement).value)  
}  
Enter fullscreen mode Exit fullscreen mode

There must be another option...

As the number of state values grow, they become more and more cumbersome to maintain. If we need to create large builder functions for each State object, our functional components will become more and more polluted.

Utility function to the rescue!

The thought of writing different builders for each state object is daunting. Removing the need for this and providing a clean method of updating state objects without writing builders would be perfect. Even better if it meant changing the component from functional to a class didn't require the interface to change.

To solve this we look at Kotlin itself and the incredible apply function. Using our old state and new state values together provides all the ingredients to create a new object by copying the existing values of the old state and applying the new state values atop.

Let us start by changing the state holder interface slightly.

/**  
 * EnterWordStateOriginal is used as a State Object * 
 *  
 * @property word is the word that is updated when the input changes.  
 * @property updateClicked is the property that is updated when the button gets clicked.  
 * @property updatedWord the new word that has been updated.  
 */
external interface SetStateExampleState: RState {  
    var word: String  
    var updateClicked: Int  
    var updatedWord: String  
}
Enter fullscreen mode Exit fullscreen mode

I know what you're all thinking, "What's RState doing there?!"

There is a genuine reason: earlier we mentioned maintaining cooperation of state if we change the component from functional into a class?

Extending RState achieves this, but also plays a secret second purpose.๐Ÿ‘‡

Functional setState

To prevent any regular interface being used as a state we can extend our state interface from RState. Using this as the type for our setState ensures only state objects can be used. Forcing better readability and cleaner code across our codebase naturally.

no more "What is this badly named interface for?!"

Our new utility function to handle this mapping will now provide us not just the clean setState we want, but the setState we deserve!

/**  
 * By creating a utility function to map the current state with 
 * the updated variables, it removes the need to create multiple 
 * builder functions for larger states across the project. 
 * Using this function we can keep code clean and efficient. 
 * 
 * @see T โ€” The purpose of extending RState is to keep uniformity across the code. 
 *          If we look to change the type of component we can * be guaranteed the state will work for free.
 * 
 * @param oldState โ€” The current state values  
 * @param newState โ€” The new values we would like to apply to the state  
 * 
 * @return T โ€” The values of old state plus the updated values of new state.  
 */
internal inline fun <T : RState> setState(  
    oldState: T,  
    newState: T.() -> Unit  
): T = clone(oldState).apply(newState)
Enter fullscreen mode Exit fullscreen mode

Time to break it down a little:

  1. internal

Prevents the setState function being exposed as part of the overall modules API.

  1. inline

inline optimises functions by inlining the lambda expressions for a reduction in runtime overhead.

  1. <T : RState>

This defines the type of oldState and newState. Extending RState gives us the certainty this will be a state.

  1. oldState: T

The value of the existing state. Kotlin uses "Copy by Value" for function parameters. The oldState param will then be a copy of the state we want to set. (There is some discrepancy in this statement to the values inside, as only the outlining object is copied, but that's for another time.)

  1. newState: T.() -> Unit

For those of you who don't know, this has got to be one of the most amazing features of Kotlin. It's known as a Function literals with receiver. We can set parameters of the receiver T and apply them to our clone.

  1. clone

_Ok, this might not be exactly part of the Kotlin language, but it is part of KotlinJS! It allows us to copy oldState into a new jsObject.

  1. apply(newState)

We want to return the value of the oldState with the updates from newState. Using apply allows for this. apply returns an instance of this so is ideal for returning a new copy after adding newState.

Result

Adding our brand new setState to the functional component we get a clean, readable state management.

enterWordState = setState(enterWordState) {  
    updateClicked += 1  
    updatedWord = word
}
Enter fullscreen mode Exit fullscreen mode

The best part of this approach is autocomplete and no need to define each value to set state. Our generic function infers the type of the state and gives us auto complete within the body of the lambda block while also mapping existing values that haven't changed to the new state.

Awesome right?!

The outcome is a clean state setter within a functional component that can have its interface values extended without requiring refactoring everywhere the state is set.
(As we would with the initial approach)

Closing remarks

Using large sets of values in a state object can be the most efficient way of keeping code clean and maintainable. Especially when dealing with larger state sets within components (such as forms).

As a rule of thumb, with smaller state values individual states can be used. These can lose context of "what they're for" as logic grows.

Object states address this by grouping these values into a single value. Important when improving the clarity of code, also providing a "Context" to what state is.

e.g "formValuesState' would hold the state of fields in a form.

One last tip to help avoid confusion is to make sure you include the actual word State as part of the state variable name, this is especially true with single states. i.e nameState, emailState

To help differentiate we have grouped each approach into separate examples in the project below, so you can get an overall understanding of each approach and its advantages.

Checkout it out here

@sKalable we are a Full Stack Kotlin-centric agency that create code to ensure it is consistently Maintainable, Flexible and of course, sKalable. ๐Ÿ˜Ž

We love to hear from the community, so if this helped feel free to get in touch or follow us on

Twitter

Dev.to

LinkedIn

to get the latest updates and strategies with Kotlin and Multiplatform for your business or personal needs.

Top comments (0)