Skip to content

Commit

Permalink
cleanup api by replacing hook triggers with just a dep list
Browse files Browse the repository at this point in the history
  • Loading branch information
eliknebel committed May 28, 2024
1 parent 546482e commit 891b5b3
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 95 deletions.
8 changes: 1 addition & 7 deletions src/sprocket/context.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ pub type ComponentHooks =
pub type HookDependencies =
List(Dynamic)

pub type HookTrigger {
OnMount
OnUpdate
WithDeps(deps: HookDependencies)
}

pub type EffectCleanup =
Option(fn() -> Nil)

Expand Down Expand Up @@ -96,7 +90,7 @@ pub type Hook {
Effect(
id: Unique,
effect: fn() -> EffectCleanup,
trigger: HookTrigger,
deps: HookDependencies,
prev: Option(EffectResult),
)
Handler(id: Unique, handler_fn: HandlerFn)
Expand Down
67 changes: 26 additions & 41 deletions src/sprocket/hooks.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import sprocket/internal/constants.{call_timeout}
import sprocket/context.{
type Attribute, type ClientDispatcher, type ClientEventHandler, type Context,
type EffectCleanup, type Element, type HandlerFn, type HookDependencies,
type HookTrigger, type IdentifiableHandler, Callback, CallbackResult, Changed,
Client, ClientHook, Context, Effect, Handler, IdentifiableHandler, OnMount,
OnUpdate, Unchanged, WithDeps, compare_deps,
type IdentifiableHandler, Callback, CallbackResult, Changed, Client,
ClientHook, Context, Effect, Handler, IdentifiableHandler, Unchanged,
compare_deps,
}
import sprocket/internal/exceptions.{throw_on_unexpected_hook_result}
import sprocket/internal/utils/unique
Expand All @@ -19,14 +19,14 @@ import sprocket/internal/logger
/// Callback Hook
/// -------------
/// Creates a callback hook that will return a cached version of a given callback
/// function and only recompute the callback as specified by the trigger.
/// function and only recompute the callback when specified dependencies change.
///
/// This hook is useful for when a component needs to use a callback function that
/// is referenced as a dependency by another hook, such as an effect hook.
pub fn callback(
ctx: Context,
callback_fn: fn() -> Nil,
trigger: HookTrigger,
deps: HookDependencies,
cb: fn(Context, fn() -> Nil) -> #(ctx, Element),
) -> #(ctx, Element) {
let assert #(ctx, Callback(id, current_callback_fn, prev), index) =
Expand All @@ -36,7 +36,7 @@ pub fn callback(

let #(callback_fn, deps) =
maybe_trigger_update(
trigger,
deps,
prev
|> option.then(fn(prev) { prev.deps }),
current_callback_fn,
Expand All @@ -54,36 +54,21 @@ pub fn callback(
}

fn maybe_trigger_update(
trigger: HookTrigger,
deps: HookDependencies,
prev: Option(HookDependencies),
value: a,
updater: fn() -> a,
) -> #(a, Option(HookDependencies)) {
case trigger {
// Only compute callback on the initial render. This is a convience for WithDeps([]).
OnMount -> {
#(updater(), Some([]))
}

// Recompute callback on every update
OnUpdate -> {
#(updater(), None)
}

// Only compute callback on the initial render and when the dependencies change
WithDeps(deps) -> {
case prev {
Some(prev_deps) -> {
case compare_deps(prev_deps, deps) {
Changed(new_deps) -> #(updater(), Some(new_deps))
Unchanged -> #(value, prev)
}
}

// initial render
None -> #(updater(), Some(deps))
case prev {
Some(prev_deps) -> {
case compare_deps(prev_deps, deps) {
Changed(new_deps) -> #(updater(), Some(new_deps))
Unchanged -> #(value, prev)
}
}

// initial render
None -> #(updater(), Some(deps))
}
}

Expand Down Expand Up @@ -120,26 +105,26 @@ pub fn client(

/// Effect Hook
/// -----------
/// Creates an effect hook that will run the given effect function when the hook is
/// triggered. The effect function is memoized and recomputed based on the trigger type.
/// Creates an effect hook that will run the given effect function when the deps change. The effect
/// function is memoized and recomputed when the deps change. The effect function can return a cleanup
/// function that will be called when the effect is removed.
pub fn effect(
ctx: Context,
effect_fn: fn() -> EffectCleanup,
trigger: HookTrigger,
deps: HookDependencies,
cb: fn(Context) -> #(Context, Element),
) -> #(Context, Element) {
// define the initial effect function that will only run when the hook is first created
let init = fn() {
Effect(unique.cuid(ctx.cuid_channel), effect_fn, trigger, None)
Effect(unique.cuid(ctx.cuid_channel), effect_fn, deps, None)
}

// get the previous effect result, if one exists
let assert #(ctx, Effect(id, _effect_fn, _trigger, prev), index) =
let assert #(ctx, Effect(id, _effect_fn, _deps, prev), index) =
context.fetch_or_init_hook(ctx, init)

// update the effect hook, combining with the previous result
let ctx =
context.update_hook(ctx, Effect(id, effect_fn, trigger, prev), index)
let ctx = context.update_hook(ctx, Effect(id, effect_fn, deps, prev), index)

cb(ctx)
}
Expand Down Expand Up @@ -167,15 +152,15 @@ pub fn handler(
/// Memo Hook
/// ---------
/// Creates a memo hook that can be used to memoize the result of a function. The memo
/// hook will return the result of the function and will only recompute the result as
/// specified by the trigger.
/// hook will return the result of the function and will only recompute the result when
/// the dependencies change.
///
/// This hook is useful for optimizing performance by memoizing the result of an
/// expensive function between renders.
pub fn memo(
ctx: Context,
memo_fn: fn() -> a,
trigger: HookTrigger,
deps: HookDependencies,
cb: fn(Context, a) -> #(Context, Element),
) -> #(Context, Element) {
let assert #(ctx, context.Memo(id, current_memoized, prev), index) =
Expand All @@ -185,7 +170,7 @@ pub fn memo(

let #(memoized, deps) =
maybe_trigger_update(
trigger,
deps,
prev
|> option.then(fn(prev) { prev.deps }),
current_memoized,
Expand Down
61 changes: 18 additions & 43 deletions src/sprocket/runtime.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import sprocket/internal/constants.{call_timeout}
import sprocket/context.{
type ComponentHooks, type Context, type Dispatcher, type EffectCleanup,
type EffectResult, type Element, type Hook, type HookDependencies,
type HookTrigger, type IdentifiableHandler, type Updater, Callback, Changed,
Client, Context, Dispatcher, Effect, EffectResult, Handler,
IdentifiableHandler, Memo, OnMount, OnUpdate, Reducer, Unchanged, Updater,
WithDeps, callback_param_from_string, compare_deps,
type IdentifiableHandler, type Updater, Callback, Changed, Client, Context,
Dispatcher, Effect, EffectResult, Handler, IdentifiableHandler, Memo, Reducer,
Unchanged, Updater, callback_param_from_string, compare_deps,
}
import sprocket/internal/reconcile.{
type ReconciledElement, ReconciledComponent, ReconciledElement,
Expand Down Expand Up @@ -513,10 +512,10 @@ fn build_hooks_map(
fn run_effects(rendered: ReconciledElement) -> ReconciledElement {
process_state_hooks(rendered, fn(hook) {
case hook {
Effect(id, effect_fn, trigger, prev) -> {
let result = run_effect(effect_fn, trigger, prev)
Effect(id, effect_fn, deps, prev) -> {
let result = run_effect(effect_fn, deps, prev)

Effect(id, effect_fn, trigger, Some(result))
Effect(id, effect_fn, deps, Some(result))
}
other -> other
}
Expand All @@ -525,48 +524,24 @@ fn run_effects(rendered: ReconciledElement) -> ReconciledElement {

fn run_effect(
effect_fn: fn() -> EffectCleanup,
trigger: HookTrigger,
deps: HookDependencies,
prev: Option(EffectResult),
) -> EffectResult {
case trigger {
// Only compute callback on the first render. This is a convience that is equivalent to WithDeps([]).
OnMount -> {
case prev {
Some(prev_effect_result) -> {
prev_effect_result
}

None -> EffectResult(effect_fn(), Some([]))
// only trigger the update on the first render and when the dependencies change
case prev {
Some(EffectResult(cleanup, Some(prev_deps))) -> {
case compare_deps(prev_deps, deps) {
Changed(_) ->
maybe_cleanup_and_rerun_effect(cleanup, effect_fn, Some(deps))
Unchanged -> EffectResult(cleanup, Some(deps))
}
}

// trigger effect on every update
OnUpdate -> {
case prev {
Some(EffectResult(cleanup: cleanup, ..)) ->
maybe_cleanup_and_rerun_effect(cleanup, effect_fn, None)
_ -> EffectResult(effect_fn(), None)
}
}
None -> maybe_cleanup_and_rerun_effect(None, effect_fn, Some(deps))

// only trigger the update on the first render and when the dependencies change
WithDeps(deps) -> {
case prev {
Some(EffectResult(cleanup, Some(prev_deps))) -> {
case compare_deps(prev_deps, deps) {
Changed(_) ->
maybe_cleanup_and_rerun_effect(cleanup, effect_fn, Some(deps))
Unchanged -> EffectResult(cleanup, Some(deps))
}
}

None -> maybe_cleanup_and_rerun_effect(None, effect_fn, Some(deps))

_ -> {
// this should never occur and means that a hook was dynamically added
throw_on_unexpected_hook_result(#("handle_effect", prev))
}
}
_ -> {
// this should never occur and means that a hook was dynamically added
throw_on_unexpected_hook_result(#("handle_effect", prev))
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions test/sprocket/hooks_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import gleam/string
import gleam/option.{None}
import gleeunit/should
import gleam/erlang/process.{type Subject}
import sprocket/context.{type Context, OnMount, OnUpdate, WithDeps}
import sprocket/context.{type Context, dep}
import sprocket/component.{component}
import sprocket/html/elements.{button, fragment, text}
import sprocket/html/attributes.{id, on_click}
Expand Down Expand Up @@ -50,7 +50,7 @@ fn inc_initial_render_counter(ctx: Context, _props) {
dispatch(UpdateCount(count + 1))
None
},
OnMount,
[],
)

let current_count = int.to_string(count)
Expand Down Expand Up @@ -93,7 +93,7 @@ fn inc_on_every_update_counter(ctx: Context, props: IncEveryUpdateCounterProps)
tally_counter.increment(props.tally)
None
},
OnUpdate,
[dep(ctx)],
)

component.render(ctx, text(""))
Expand Down Expand Up @@ -139,7 +139,7 @@ fn inc_reset_on_button_click_counter(ctx: Context, _props) {
dispatch(UpdateCount(count + 1))
None
},
WithDeps([]),
[],
)

// Define event handlers
Expand Down

0 comments on commit 891b5b3

Please sign in to comment.