- Design
- Quickstart
- React hooks in scalajs-react
- New hooks provided by scalajs-react
shouldComponentUpdate
- Hooks with dependencies
- Hooks and PropsChildren
- Custom hooks
- Custom hook composition
- Using third-party JavaScript hooks
- API extensions
- Escape hatches
One of the core goals of scalajs-react is that if your code compiles, it will work as expected at runtime. As a consequence, most translation of React JS looks different in scalajs-react specifically because we want to turn runtime errors into compile-time errors. Hooks is no different.
The main difference you'll notice is that instead of just creating hooks imperatively, scalajs-react provides a builder-like DSL. The reason for this difference is that it allows us to enforce at compile-time, the React JS rule that the same hooks must always be created, and in the same order, even if they aren't used in a given render pass. In a plain JS world, the onus is on the user to have read the documentation and know that they have to avoid many very natural types of code, else they'll get a runtime error (or worse, no runtime error but undetected bugs). There is a mitigation in that JS users can use a linter that uses AST reflection to try to detect when a user is misusing hooks. In scalajs-react the DSL enforces these rules at compile-time without the need for an AST-inspecting macro.
If you have a spare 9 hours (!), you can watch the livestreamed coding sessions (part 1, part 2) and see how the design gradually evolved into what it (conceptually) is today.
Let's translate this JS component...
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [fruit, setFruit] = useState("banana");
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<p>Your favourite fruit is a {fruit}!</p>
</div>
);
}
The above JS component can be written in scalajs-react in two very similar ways.
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom.document
object Example {
val Component = ScalaFnComponent.withHooks[Unit]
.useState(0)
.useEffectBy((props, count) => Callback {
document.title = s"You clicked ${count.value} times"
})
.useState("banana")
.render((props, count, fruit) =>
<.div(
<.p(s"You clicked ${count.value} times"),
<.button(
^.onClick --> count.modState(_ + 1),
"Click me"
),
<.p(s"Your favourite fruit is a ${fruit.value}!")
)
)
}
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom.document
object Example {
val Component = ScalaFnComponent.withHooks[Unit]
.useState(0)
.useEffectBy($ => Callback {
document.title = s"You clicked ${$.hook1.value} times"
})
.useState("banana")
.render($ =>
<.div(
<.p(s"You clicked ${$.hook1.value} times"),
<.button(
^.onClick --> $.hook1.modState(_ + 1),
"Click me"
),
<.p(s"Your favourite fruit is a ${$.hook2.value}!")
)
)
}
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom.document
object Example {
val Component = ScalaFnComponent.withHooks[Unit]
.useState(0)
- .useEffectBy((props, count) => Callback {
- document.title = s"You clicked ${count.value} times"
+ .useEffectBy($ => Callback {
+ document.title = s"You clicked ${$.hook1.value} times"
})
.useState("banana")
- .render((props, count, fruit) =>
+ .render($ =>
<.div(
- <.p(s"You clicked ${count.value} times"),
+ <.p(s"You clicked ${$.hook1.value} times"),
<.button(
- ^.onClick --> count.modState(_ + 1),
+ ^.onClick --> $.hook1.modState(_ + 1),
"Click me"
),
- <.p(s"Your favourite fruit is a ${fruit.value}!")
+ <.p(s"Your favourite fruit is a ${$.hook2.value}!")
)
)
}
JavaScript | scalajs-react |
---|---|
useCallback(c) |
.useCallback(c) |
useCallback(c, [deps]) |
.useCallbackWithDeps((deps))(_ => c) |
useCallback(f([deps]), [deps]) |
.useCallbackWithDeps((deps))(f) |
useContext(c) |
.useContext(c) |
useDebugValue(desc) |
.useDebugValue(desc) |
useDebugValue(a, f) |
.useDebugValue(f(a)) |
useEffect(e) |
.useEffect(e) |
useEffect(e, []) |
.useEffectOnMount(e) |
useEffect(e, [deps]) |
.useEffectWithDeps((deps))(_ => e) |
useEffect(f([deps]), [deps]) |
.useEffectWithDeps((deps))(f) |
useLayoutEffect(e) |
.useLayoutEffect(e) |
useLayoutEffect(e, []) |
.useLayoutEffectOnMount(e) |
useLayoutEffect(e, [deps]) |
.useLayoutEffectWithDeps((deps))(_ => e) |
useLayoutEffect(f([deps]), [deps]) |
.useLayoutEffectWithDeps((deps))(f) |
useMemo(() => a, [deps]) |
`.useMemo((deps))(_ => a) |
useMemo(() => f([deps]), [deps]) |
`.useMemo((deps))(f) |
useReducer(f, s) |
.useReducer(f, s) |
useReducer(f, a, i) |
.useReducer(f, i(a)) (Note: i(a) is actually (=> i(a)) and isn't evaluated immediately) |
useRef() |
.useRefToAnyVdom .useRefToVdom[DomType] .useRefToScalaComponent(component) .useRefToScalaComponent[P, S, B] .useRefToJsComponent(component) .useRefToJsComponent[P, S] .useRefToJsComponentWithMountedFacade[P, S, F] |
useRef(initialValue) |
.useRef(initialValue) |
useState(initialState) useState(() => initialState) |
.useState(initialState) |
Custom hook useBlah(i) |
.custom(useBlah(i)) |
Note: The reason that [deps]
on the JS side becomes (deps)
on the Scala side,
is that in JS you'd use an array but in Scala you'd use a tuple.
So [dep1, dep2]
becomes (dep1, dep2)
; and [dep1]
becomes just dep1
which is the same as
(dep1)
.
Hook | Description |
---|---|
.localLazyVal(a) |
Creates a new lazy val on each render. |
.localVal(a) |
Creates a new val on each render. |
.localVar(a) |
Creates a new var on each render. |
.useForceUpdate |
Provides a Reusable[Callback] then when invoked, forces a re-render of the component. |
.useStateSnapshot(initialState) (Requires import japgolly.scalajs.react.extra._) |
Same as .useState except you get a StateSnapshot (which accepts callbacks on set updates). |
.useStateSnapshotWithReuse(initialState) (Requires import japgolly.scalajs.react.extra._) |
Same as .useState except you get a StateSnapshot (which accepts callbacks on set updates) with state Reusability . |
.useStateWithReuse(initialState) |
Conceptually useState + shouldComponentUpdate . Same as useState except that updates are dropped according to Reusability . |
Instead of calling render
, you can call one of the following to get shouldComponentUpdate
behaviour just like classes have.
renderWithReuse(f: Ctx => VdomNode)(implicit r: Reusability[Ctx])
renderWithReuseBy[A: Reusability](reusableInputs: Ctx => A)(f: A => VdomNode)
renderReusable(f: Ctx => Reusable[VdomNode])
Sometimes hooks are initialised using props and/or the output of other hooks,
(which scalajs-react refers to as "context").
Each hook that has a return type that's not Unit
,
becomes available in subsequent contexts.
In order to get access to this context, append a By
suffix to the hook method
of your choice, and change the arguments to functions that take the context.
There are two ways to do this.
val comp = ScalaFnComponent.withHooks[Int]
.useStateBy(props => props - 1) // initialise state according to props
.useEffectBy((props, hook1) => Callback.log(s"Props: $props, State: ${hook1.value}"))
val comp = ScalaFnComponent.withHooks[Int]
.useStateBy(props => props - 1) // initialise state according to props
.useEffectBy(c => Callback.log(s"Props: ${c.props}, State: ${c.hook1.value}"))
val comp = ScalaFnComponent.withHooks[Int]
// The result of this hook becomes "hook1"
.useStateBy(props => props - 1)
// The result of useEffect is Unit and doesn't appear in context
.useEffectBy(c => Callback.log(s"Props: ${c.props}, State: ${c.hook1.value}"))
// The result of this hook becomes "hook2"
.useState(123)
.render((props, hook1, hook2) =>
<.div(
<.div("State 1 = ", hook1.value),
<.div("State 2 = ", hook2.value),
)
)
In order to get access to PropsChildren
, call .withPropsChildren
as the first step in your DSL.
It will then become available...
- as argument #2 after
props
in multi-arg fns (eg..render((props, propsChildren, hook1, hook2, ...) =>
) - as
.propsChildren
from context objects (eg..render($ => $.propsChildren)
)
Example:
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
object Example {
final case class Props(name: String)
val Component = ScalaFnComponent.withHooks[Props]
.withPropsChildren
.useState(0)
.render((props, propsChildren, counter) =>
<.div(
<.p(s"Hello ${props.name}."),
<.p(s"You clicked ${counter.value} times."),
<.button("Click me", ^.onClick --> counter.modState(_ + 1)),
<.div(propsChildren)
)
)
}
A custom hook has the type CustomHook[I, O]
where
I
is the input type (or Unit
if your custom hook doesn't take an input),
and O
is the output type (or Unit
if your custom hook doesn't return an output),
To create a custom hook, the API is nearly identical to building a component with hooks.
- Start with
CustomHook[I]
instead ofScalaFnComponent.withHooks[P]
- Complete your hook with
.buildReturning(ctx => O)
, or just.build
if you don't need to return a value.
Example:
import japgolly.scalajs.react._
import org.scalajs.dom.document
object ExampleHook {
val useTitleCounter = CustomHook[Unit]
.useState(0)
.useEffectBy((_, count) => Callback {
document.title = s"You clicked ${count.value} times"
})
.buildReturning(_.hook1)
}
and to use it:
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
object Example {
val Component = ScalaFnComponent.withHooks[Unit]
.custom(ExampleHook.useTitleCounter) // <--- usage
.render((_, count) =>
<.div(
<.p(s"You clicked ${count.value} times"),
<.button(
^.onClick --> count.modState(_ + 1),
"Click me")))
}
In order to provide the hook directly via .custom
the input type of the hook must be one of the following...
Unit
- same as the
Props
type PropsChildren
If the custom hook has any other kind of type, simply provide it to the hook directly. Example:
val someCustomHook: CustomHook[Int, Unit] = ???
final case class Props(someInt: Int)
val Component = ScalaFnComponent.withHooks[Props]
.custom(someCustomHook(123)) // provide a constant Int arg
.customBy($ => someCustomHook($.props.someInt)) // or use a dynamic value
CustomHooks can be composed by calling ++
.
The input/output type of the result will be the "natural" result according to these rules:
A ++ Unit
orUnit ++ A
becomesA
A ++ A
becomesA
if it's in the input positionA ++ A
becomesA
if it's in the output position andA <: scala.Singleton
- otherwise
A ++ B
becomes(A, B)
Examples:
object Example1 {
val hook1: CustomHook[Int, Unit] = ???
val hook2: CustomHook[Int, Unit] = ???
val hooks: CustomHook[Int, Unit] = hook1 ++ hook2
}
object Example2 {
val hook1: CustomHook[Unit, Int] = ???
val hook2: CustomHook[Unit, Int] = ???
val hooks: CustomHook[Unit, (Int, Int)] = hook1 ++ hook2
}
object Example3 {
val hook1: CustomHook[Long, Boolean] = ???
val hook2: CustomHook[String, Int] = ???
val hooks: CustomHook[(Long, String), (Boolean, Int)] = hook1 ++ hook2
}
object Example4 {
val hook1: CustomHook[Unit, Boolean] = ???
val hook2: CustomHook[String, Unit] = ???
val hooks: CustomHook[String, Boolean] = hook1 ++ hook2
}
Using a third-party JavaScript hook is as simple as wrapping it in CustomHook.unchecked
.
// Declare your JS facade as normal. Type should be a subtype of js.Function.
// I is the type of the hook's inputs (use a tuple or case class for multiple args)
// O is the type of the hook's output (or Unit if none)
val jsHook = CustomHook.unchecked[I, O](i => JsHookFacade(i))
Then to use it, either...
- simply call
.custom(jsHook)
from your component - or create an API extension as shown below
You can also provide your own implicit extensions to the hook API.
Unfortunately it involves a bit of boilerplate. Copy and customise one of the following templates:
import japgolly.scalajs.react._
object MyCustomHook {
// TODO: Replace
val hook = CustomHook[String]
.useEffectOnMountBy(name => Callback.log(s"HELLO $name"))
.buildReturning(name => name)
object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
// TODO: Change hook name, input args/type(s), and output type
final def useMyCustomHook(name: String)(implicit step: Step): step.Next[String] =
// TODO: Change hook name
useMyCustomHookBy(_ => name)
// TODO: Change hook name, input args/type(s), and output type
final def useMyCustomHookBy(name: Ctx => String)(implicit step: Step): step.Next[String] =
api.customBy(ctx => hook(name(ctx)))
}
final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary[Ctx, Step](api) {
// TODO: Change hook name, input args/type(s), and output type
def useMyCustomHookBy(name: CtxFn[String])(implicit step: Step): step.Next[String] =
// TODO: Change hook name, squash each parameter
// useMyCustomHookBy(step.squash(arg1)(_), step.squash(arg2)(_), ...)
useMyCustomHookBy(step.squash(name)(_))
}
}
trait HooksApiExt {
import HooksApiExt._
// TODO: Change hook name so that it won't conflict with other custom hooks
implicit def hooksExtMyCustomHook1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] =
new Primary(api)
// TODO: Change hook name so that it won't conflict with other custom hooks
implicit def hooksExtMyCustomHook2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] =
new Secondary(api)
}
object Implicits extends HooksApiExt
}
import japgolly.scalajs.react._
object MyCustomHook {
// TODO: Replace
val hook = CustomHook[String]
.useEffectOnMountBy(name => Callback.log(s"HELLO $name"))
.build
object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
// TODO: Change hook name, input args/type(s), and output type
final def useMyCustomHook(name: String)(implicit step: Step): step.Self =
// TODO: Change hook name
useMyCustomHookBy(_ => name)
// TODO: Change hook name, input args/type(s), and output type
final def useMyCustomHookBy(name: Ctx => String)(implicit step: Step): step.Self =
api.customBy(ctx => hook(name(ctx)))
}
final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary[Ctx, Step](api) {
// TODO: Change hook name, input args/type(s), and output type
def useMyCustomHookBy(name: CtxFn[String])(implicit step: Step): step.Self =
// TODO: Change hook name, squash each parameter
// useMyCustomHookBy(step.squash(arg1)(_), step.squash(arg2)(_), ...)
useMyCustomHookBy(step.squash(name)(_))
}
}
trait HooksApiExt {
import HooksApiExt._
// TODO: Change hook name so that it won't conflict with other custom hooks
implicit def hooksExtMyCustomHook1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] =
new Primary(api)
// TODO: Change hook name so that it won't conflict with other custom hooks
implicit def hooksExtMyCustomHook2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] =
new Secondary(api)
}
object Implicits extends HooksApiExt
}
import japgolly.scalajs.react._
object MyCustomHook {
// TODO: Replace
val hook = CustomHook[Unit]
.useEffectOnMount(Callback.log("HELLO!"))
.buildReturning(_ => 123)
object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
// TODO: Change hook name, and output type
final def useMyCustomHook(implicit step: Step): step.Next[Int] =
api.custom(hook)
}
}
trait HooksApiExt {
import HooksApiExt._
// TODO: Change hook name so that it won't conflict with other custom hooks
implicit def hooksExtMyCustomHook[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] =
new Primary(api)
}
object Implicits extends HooksApiExt
}
import japgolly.scalajs.react._
object MyCustomHook {
// TODO: Replace
val hook = CustomHook[Unit]
.useEffectOnMount(Callback.log("HELLO!"))
.build
object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
// TODO: Change hook name
final def useMyCustomHook(implicit step: Step): step.Self =
api.custom(hook)
}
}
trait HooksApiExt {
import HooksApiExt._
// TODO: Change hook name so that it won't conflict with other custom hooks
implicit def hooksExtMyCustomHook[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] =
new Primary(api)
}
object Implicits extends HooksApiExt
}
By importing MyCustomHook.Implicits._
users will be able to use your custom hook directly from the hooks API.
Example:
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import MyCustomHook.Implicits._
object Example {
val Component = ScalaFnComponent.withHooks[Unit]
.useMyCustomHook // Implicitly available
.render($ =>
<.div("MyCustomHook: ", $.hook1)
)
}
If you really, really want to work with JS-style imperative hooks, you can! But it's important to note that the onus is on you to ensure you use hooks correctly without violating React's rules. If you use the escape hatch, scalajs-react won't be able to check that your code will always work.
In the hooks API, there's .unchecked(body)
and .uncheckedBy(ctx => body)
that you can use as an escape hatch,
and create hooks using React directly instead of using scalajs-react's hooks API.
Example:
package japgolly.scalajs.react.core
import japgolly.scalajs.react._
import japgolly.scalajs.react.facade.{React => ReactJs}
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom.document
object Example {
val Component = ScalaFnComponent.withHooks[Unit]
.unchecked {
val count = ReactJs.useState[Int](0)
ReactJs.useEffect(() => {
document.title = s"You clicked ${count._1} times"
})
count
}
.render { (_, countHook) =>
val count = countHook._1
val setCount = countHook._2
<.div(
<.p(s"You clicked $count times"),
<.button(
^.onClick --> Callback(setCount(count + 1)),
"Click me"
),
)
}
}