Slinky doing React the Scala way

Motivation

I just wanted to know what React is all about. So I did the really good Intro Tutorial from React. The main reason to try React, was that there are at least two libraries, that allows you to write React Apps in Scala:

So what are the differences to using React with Javascript. Is it worth to add another abstraction?

Idea

Image for post
Image for post

Just follow the Tutorial and translate it to Slinky / Scala. And write the findings in a blog post.

You find the source for slinky-react-turorial on Github. I made a commit for each chapter of the Tutorial.

Setup the Project

First create a Slinky project:

sbt new shadaj/create-react-scala-app.g8

Now we adjust our project to get to the starting point of the tutorial. The main part is the App.scala where the code for the tutorial is.

A component in React:

The same in Slinky:

So my first impression was, that there is more information and still less code in Slinky. How is this possible?

  • Slinky uses a macro annotation @react that reduces the boilerplate. See Technical Overview.
  • Props and States must be defined in Slinky with types or case classes. We will see some examples along the way. In the Javascript you have to check the whole Component to figure out what Props are needed.

Another point is that Slinky uses its own Tag API. So there is always a translation involved when coming from HTML.

Here is the only adjustment I made, as I am a lazy person:

<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>

I changed in my Scala code to:

for (r <- 0 to 2)
yield div(className := "board-row")(
for (c <- 0 to 2)
yield renderSquare(r * 3 + c)
)

See all changes in the Commit.

Passing Data Through Props

As mentioned above, we need to define our Props class.

@react class Square extends StatelessComponent {
case class Props(value: Int)
...

Thanks to macro annotation, creating the component looks natural (Props must not be created):

Square(squareValue)

This looks actually better than using JSX (IMHO):

<Square value={i} />

See all changes in the Commit.

Making an Interactive Component

Let’s start with the constructor of the React version:

constructor(props) {
super(props);
this.state = {
value: null,
};
}

Quite some code, with not so much information.

  • What are the Props?
  • What is the type of value?

In Slinky there is a bit more code involved, but also a lot more information.

The StatelessComponent gets new a Component, which requires the initialState function. All the missing information of the Javascript version is here. Again some magic hidden by the @react annotation.

<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>

This mix of JSX-tags and JavaScript code makes it a bit harder to read.

button(
className := "square",
onClick := (_ => setState(State("X")))
) (state.value)

With Slinky you have to replace always the State object. The value is in its own attribute list — which is a bit strange in the context of HTML. The cluttering this is not required in Scala.

See all changes in the Commit.

Lifting State Up

Image for post
Image for post
this.state = {      
squares: Array(9).fill(null),
};

Ok null in Scala that is not an option😏. Let’s just use an empty String for now (Spoiler: an Option is in the air):

case class State(squares: Seq[String])def initialState: State = State(List.fill(9)(""))

Also interesting is the onClick function. In Javascript you do not have to care about the types. In Scala you do, because you have to define them in the Props class. You must try until the compiler is happy😬.

case class Props(value: String, onClick: () => ())

See all changes in the Commit.

Why Immutability Is Important

As a Scala developer you are most certainly already convinced, that immutability is a good thing. As this is an important Scala Idiom.

Let’s see how this affects the code. An example of the last chapter:

const squares = this.state.squares.slice();    
squares[i] = 'X';
this.setState({squares: squares});

In Scala the immutable collections API provides functions to update them directly — so you can skip the step to make a copy first (why is it called slice?):

val squares = state.squares.updated(squareIndex, “X”) setState(State(squares))

So again clearer to read with less code and not to forget type safe. For example this.setState({square: squares}); would not tell you that you have missed an ‘s’. It just does not update the state of the squares!

Image for post
Image for post

Taking Turns

Not much new stuff to discuss here.

this.state.xIsNext ? ‘X’ : ‘O’;

Well, this construct does not exist in Scala, but you can write it in this (more readable) way:

if (state.xIsNext) “X” else “O”

See all changes in the Commit.

Declaring a Winner

function calculateWinner(squares) {
...
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}

Here we find two points that are not really Scala-like:

  1. null again!
  2. Two return statements.

Ok now is definitely the time to introduce Option to our model. So our State looks now like this:

case class State(squares: Seq[Option[Char]], xIsNext: Boolean)
def initialState: State = State(List.fill(9)(None), xIsNext = true)

This gives us the following calculation:

private def calculateWinner(): Option[Char] = { 
val lines = List( (0, 1, 2), ... , (2, 4, 6) )
val squares = state.squares
lines.collectFirst {
case (a, b, c)
if squares(a).nonEmpty &&
squares(a) == squares(b) &&
squares(a) == squares(c) =>
squares(a).get } }

Also here shines the power of the collections API — as it provides for every scenario the perfect function.

With Option in your model we can now simplify our code. For example this Javascript:

const winner = calculateWinner(this.state.squares);    
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}

Is in Scala:

def status = calculateWinner() 
.map(“Winner: “ + _)
.getOrElse(s”Next player ${nextPlayer.mkString}”)

See all changes in the Commit.

Lifting State Up, Again

Image for post
Image for post

In Javascript everything is a “JSON” data structure:

history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [..]
},
// After second move
... ,
// ...
]

So you have Maps and Arrays of simple types. In Scala, the best practice to structure data are Algebraic Data Types (ADTs). So let’s make them more concrete:

case class HistoryEntry(
squares: Seq[Option[Char]] = List.fill(9)(None))
...
case class State(history: Seq[HistoryEntry], xIsNext: Boolean)
def initialState: State = State(Seq(HistoryEntry()), xIsNext = true)

The default of an HistoryEntry are nine squares that are not set (None).

See all changes in the Commit.

Showing the Past Moves

const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});

This code snippet brings up quite some questions (to a not regular-Javascript developer):

  • history is an Array of squares , what are then step and move?

step is one square, move is the index.

  • What is false of a positive integer?

— Easy 0 is false, the rest is true. How did I know? I didn’t, it’s just what it does😱.

  • step in not used, why is it there?

— No better alternative I assume.

This function in Scala:

val moves = history.indices.map(move =>
li(
button(onClick := { () => jumpTo(move) })(
if (move > 0)
s"Go to move # $move"
else
"Go to game start"
)
)
)

Not so much magic here — you need the indices, but then it is straight forward. No shortcuts, but simple readable code.

See all changes in the Commit.

Picking a Key

Well this chapter explains the warning I’ve got from the beginning:

Image for post
Image for post

This is the code causing this warning:

for (r <- 0 to 2)
yield div(className := "board-row")(
for (c <- 0 to 2)
yield renderSquare(r * 3 + c)
)

So to a div we can add it simply as the attribute key:

div(key := s"row_$r", className := "board-row")(
...)

But how about the own components? Well Slinky provides the function withKey for this:

Square(props.squares(squareIndex), () => props.onClick(squareIndex))
.withKey(s"square_$squareIndex")

See all changes in the Commit.

Implementing Time Travel

Image for post
Image for post

Nothing new here — we are done.

See all changes in the Commit.

Conclusion

It was quite easy to translate the React “Tic Tac Toe” to Slinky. I think a Javascript React developer would be productive pretty fast, delivering more robust and easier to maintain code. If this is the case for Scala developers that start with React, I hope I can answer in a future blog🙏.

Pros

  • Type Safety.
  • More Information, through defined Types.
  • Less boilerplate code thanks to macros.
  • Real immutability with Case Classes and immutable collections.
  • Powerful Collection API.
  • If your backend is in Scala, you can reuse your models and algorithms on the client.✌️

Cons

  • You have to learn an additional abstraction of React. Quite an intuitive and easy one I have to admit.
  • There is a lot of great learning material and examples for React — so you need to translate them to Slinky.
  • The Hot Loading is really good, but compared with pure React — well is still Scala😊.

References

I linked hopefully everything in the text above. Here are only the important ones listed:

https://reactjs.org/tutorial/tutorial.html

Written by

Working for finnova.com in the Banking business. Prefer to work with Scala / ScalaJS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store