diff --git a/docs/faq.md b/docs/faq.md index 74e734982..77284f853 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -15,3 +15,98 @@ Caused by: java.lang.IllegalStateException: Transition already happened. This is ### Callback is already defined. TODO.. + +### After evaluation finished +If a `callback` or `eventCallback` is called after the Formula evaluation is finished, you will see +this exception. +``` +Caused by: java.lang.IllegalStateException: Cannot call this after evaluation finished. +``` +This can happen for a number of reasons. Likely, you are creating a `callback` or `eventCallback` +within the `onEvent` or `events` method of your `updates` lambda for the given Formula. This can +cause your callbacks to be scoped to a stale state instance. Instead, you should create your callbacks +within the `evaluate` function itself, passing the data you might be using from the `onEvent` into +the `State` defined for that Formula. For example, instead of: +``` +class TaskDetailFormula @Inject constructor( + private val repo: TasksRepo, +) : Formula { + + data class Input( + val taskId: String + ) + + data class State( + val task: TaskDetailRenderModel? = null + ) + + override fun initialState(input: Input) = State() + + override fun evaluate( + input: Input, + state: State, + context: FormulaContext + ): Evaluation { + return Evaluation( + output = state.task, + updates = context.updates { + RxStream.fromObservable { repo.fetchTask(input.taskId) }.onEvent { task -> + val renderModel = TaskDetailRenderModel( + title = task.title, + // Don't do: calling context.callback within "onEvent" will cause a crash described above + onDeleteSelected = context.callback { + ... + } + ) + transition(state.copy(task = renderModel)) + } + } + ) + } +} +``` +which the render model and then stores it in the `State`, we would store the fetched task from the RxStream in +the state and then construct the render model in the `evaluation` function itself: +``` +class TaskDetailFormula @Inject constructor( + private val repo: TasksRepo, +) : Formula { + + data class Input( + val taskId: String + ) + + data class State( + val task: Task? = null + ) + + override fun initialState(input: Input) = State() + + override fun evaluate( + input: Input, + state: State, + context: FormulaContext + ): Evaluation { + // Note that this is correct because the render model and therefore callback is constructed + // within `evaluate` instead of within `onEvent` + val renderModel = state.task?.let { + TaskDetailRenderModel( + title = it.title, + onDeleteSelected = context.callback { + ... + } + ) + } + return Evaluation( + output = renderModel, + updates = context.updates { + RxStream.fromObservable { repo.fetchTask(input.taskId) }.onEvent { task -> + transition(state.copy(task = renderModel)) + } + } + ) + } +} +``` +Notice that the render model is no longer stored in the state, but instead constructed on each +call to `evaluate` so that the callbacks are never stale. \ No newline at end of file diff --git a/formula/src/main/java/com/instacart/formula/internal/FormulaContextImpl.kt b/formula/src/main/java/com/instacart/formula/internal/FormulaContextImpl.kt index 90384c7f2..967e8640d 100644 --- a/formula/src/main/java/com/instacart/formula/internal/FormulaContextImpl.kt +++ b/formula/src/main/java/com/instacart/formula/internal/FormulaContextImpl.kt @@ -42,7 +42,7 @@ class FormulaContextImpl internal constructor( private fun ensureNotRunning() { if (transitionCallback.running) { - throw IllegalStateException("cannot call this after evaluation finished.") + throw IllegalStateException("Cannot call this transition after evaluation finished. See https://instacart.github.io/formula/faq/#after-evaluation-finished") } } } diff --git a/formula/src/main/java/com/instacart/formula/internal/ScopedCallbacks.kt b/formula/src/main/java/com/instacart/formula/internal/ScopedCallbacks.kt index 010bbffd0..c2e2d332d 100644 --- a/formula/src/main/java/com/instacart/formula/internal/ScopedCallbacks.kt +++ b/formula/src/main/java/com/instacart/formula/internal/ScopedCallbacks.kt @@ -101,7 +101,7 @@ internal class ScopedCallbacks private constructor( private fun ensureNotRunning() { if (!enabled) { - throw java.lang.IllegalStateException("cannot call this after evaluation finished.") + throw java.lang.IllegalStateException("Cannot call this callback after evaluation finished. See https://instacart.github.io/formula/faq/#after-evaluation-finished") } } }