From 891b5b33e4704ba4df62eb3a3f96f256dae8eb15 Mon Sep 17 00:00:00 2001 From: Eli Knebel Date: Mon, 27 May 2024 22:49:54 -0400 Subject: [PATCH] cleanup api by replacing hook triggers with just a dep list --- src/sprocket/context.gleam | 8 +--- src/sprocket/hooks.gleam | 67 +++++++++++++--------------------- src/sprocket/runtime.gleam | 61 +++++++++---------------------- test/sprocket/hooks_test.gleam | 8 ++-- 4 files changed, 49 insertions(+), 95 deletions(-) diff --git a/src/sprocket/context.gleam b/src/sprocket/context.gleam index 5a36110..3f752c2 100644 --- a/src/sprocket/context.gleam +++ b/src/sprocket/context.gleam @@ -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) @@ -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) diff --git a/src/sprocket/hooks.gleam b/src/sprocket/hooks.gleam index dbb4b6e..39f8347 100644 --- a/src/sprocket/hooks.gleam +++ b/src/sprocket/hooks.gleam @@ -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 @@ -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) = @@ -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, @@ -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)) } } @@ -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) } @@ -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) = @@ -185,7 +170,7 @@ pub fn memo( let #(memoized, deps) = maybe_trigger_update( - trigger, + deps, prev |> option.then(fn(prev) { prev.deps }), current_memoized, diff --git a/src/sprocket/runtime.gleam b/src/sprocket/runtime.gleam index 2d2957a..ed26c3e 100644 --- a/src/sprocket/runtime.gleam +++ b/src/sprocket/runtime.gleam @@ -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, @@ -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 } @@ -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)) } } } diff --git a/test/sprocket/hooks_test.gleam b/test/sprocket/hooks_test.gleam index 4a80766..1f8d8e7 100644 --- a/test/sprocket/hooks_test.gleam +++ b/test/sprocket/hooks_test.gleam @@ -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} @@ -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) @@ -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("")) @@ -139,7 +139,7 @@ fn inc_reset_on_button_click_counter(ctx: Context, _props) { dispatch(UpdateCount(count + 1)) None }, - WithDeps([]), + [], ) // Define event handlers