combine
- parallel merging of fs2.Streams
combine
takes a name of class, which companion has apply
method (e.g. tuple or case class) and builds a stream of values
of this class, built from the most recent values of each stream.
This is done by converting all streams to Signal
using holdOption
,
merging them using Applicative[Signal]
which does parallel joining,
removing all None
values and mapping the resulting output. The method
itself is a varargs blackbox macro, allowing for any arity of
constructor function, and is based on withLatestFrom
in various Rx
libraries.
The call looks like this:
import com.olegpy.shironeko.util.combine
case class State(timestamp: String, count: Int, name: String)
val stream1: fs2.Stream[F, String] = fs2.Stream("1st")
val stream2: fs2.Stream[F, Int] = fs2.Stream(2)
val stream3: fs2.Stream[F, String] = fs2.Stream("3rd")
val states: fs2.Stream[F, State] =
combine[State].from(
// ^ --- ^ type parameter is mandatory there
stream1, // <- streams will be checked to match expected param type
stream2, // after macro expansion
stream3
)
The call to combine
will only compile if:
- There’s
Concurrent[F]
instance in implicit scope - Streams are provided in an order such that types of their elements
correspond to those in
apply
of type being constructed (here it’s autogeneratedState.apply
). - Number of streams provided is same as number of arguments of that
apply
The previous example will compile if you swap stream1
and stream3
,
but won’t if you swap stream1
with stream2
, nor if you add or remove
a stream.
No attempt is made to match field names, or warn about possible order mismatches.
shift
- using hooks in any React component
React hooks cannot be used with regular class-based components, and separating parts of state or using regular context API can be annoying. Shironeko Containers also have no place to specify an extra local state, which might be desirable in several scenarios.
Shift is a simple functional component that takes a (by-name) body and renders it directly. This body, however, might use any hooks. This can be used in containers, or in plain class-based components, e.g. in this class from todo-mvc example:
@react class NewTodoInput extends StatelessComponent {
case class Props(onCommit: String => Unit)
override def render(): ReactElement = shift {
val (text, setText) = Hooks.useState("")
input(
className := "new-todo",
placeholder := "What needs to be done?",
autoFocus := true,
value := text,
onChange := { e => setText(e.target.value) },
onKeyPress := { e =>
if (e.key == "Enter") {
props.onCommit(text)
setText("")
}
}
)
}
}
Cache
- low-boilerplate cache for reducing allocations
Cache
is a macro-based polymorphic cache. It uses
call position in the code as a primary key.
class Foo extends StatelessComponent {
type Props = Unit
private val cache = new Cache
def render: ReactElement = {
div(
onMouseEnter := cache { () => println("Hovered") },
onClick := cache { () => println("Clicked") }
)(cache("Click me!")) // not useful here
}
}
Its primary purpose is to avoid allocating lambdas in render
method, but it can be used for anything else (it might be confusing
though).
It also has support for adding a key and a list of dependencies to watch. You must use key in loops, and add any values your lambda captures that are subject to change to a dependency list
class Foo extends StatelessComponent {
type Props = Int
private val cache = new Cache
def render: ReactElement = {
val displayValue = props + 1
div(
List("foo", "bar", "baz").map { s =>
// cache different funcs for the same position
div(onClick := cache(s)(() => println(s)))
},
div(onClick := cache.dependent("clicker", Seq(displayValue)) { () =>
println(s"Current value is $displayValue")
})(displayValue)
)
}
}
Note that if you’re caching a lambda, you don’t need to add state
or
props
as dependencies in Slinky, as those are methods and will always
resolve to most recent state.