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
}
}
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
}
}
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
This allows for clean and easy methods to update and read the required state.
updatedClickedState += 1 // update the value by 1
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
}
}
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)
}
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
}
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)
Time to break it down a little:
internal
Prevents the setState
function being exposed as part of the overall modules API.
inline
inline
optimises functions by inlining the lambda expressions for a reduction in runtime overhead.
<T : RState>
This defines the type of oldState
and newState
. Extending RState
gives us the certainty this will be a state
.
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.)
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.
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.
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
}
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
to get the latest updates and strategies with Kotlin and Multiplatform for your business or personal needs.
Top comments (0)