/// <summary>
        /// Creates a reaction that operates on data of type T.
        /// </summary>
        /// <typeparam name="T">The type the reaction operates on.</typeparam>
        /// <param name="sharedState">The shared state to use.</param>
        /// <param name="expression">The expression that delivers a value.</param>
        /// <param name="effect">The effect that is executed when the value changes.</param>
        /// <param name="reactionOptions">The options to use for the reaction.</param>
        /// <returns>An <see cref="IDisposable"/> instance that can be used to stop the reaction.</returns>
        public static IDisposable Reaction <T>(this ISharedState sharedState, Func <Reaction, T> expression, Action <T, Reaction> effect, ReactionOptions <T> reactionOptions = null)
        {
            if (sharedState is null)
            {
                throw new ArgumentNullException(nameof(sharedState));
            }

            if (expression is null)
            {
                throw new ArgumentNullException(nameof(expression));
            }

            if (effect is null)
            {
                throw new ArgumentNullException(nameof(effect));
            }

            if (reactionOptions is null)
            {
                reactionOptions = new ReactionOptions <T>();
            }

            var name   = reactionOptions.Name ?? $"Reaction@{sharedState.GetUniqueId()}";
            var action = sharedState.CreateAction(
                name + "effect",
                reactionOptions.Context,
                reactionOptions.ErrorHandler != null ? WrapErrorHandler(reactionOptions.ErrorHandler, effect) : effect);
            var runSync = reactionOptions.Scheduler == null && reactionOptions.Delay == 0;

            var      firstTime   = true;
            var      isScheduled = false;
            T        value       = default;
            Reaction reaction    = null;

            var equals =
                reactionOptions.EqualityComparer != null ?
                new Func <T, T, bool>(reactionOptions.EqualityComparer.Equals) :
                new Func <T, T, bool>((x, y) => Equals(x, y));

            var scheduler = CreateSchedulerFromOptions(reactionOptions, ReactionRunner);

#pragma warning disable CA2000  // Dispose objects before losing scope
#pragma warning disable IDE0067 // Dispose objects before losing scope
            reaction = new Reaction(
                sharedState,
                name,
                () =>
            {
                if (firstTime || runSync)
                {
                    ReactionRunner().GetAwaiter().GetResult();
                }
                else if (!isScheduled)
                {
                    isScheduled = true;
                    scheduler().GetAwaiter().GetResult();
                }
            }, reactionOptions.ErrorHandler);
#pragma warning restore IDE0067 // Dispose objects before losing scope
#pragma warning restore CA2000  // Dispose objects before losing scope

            Task ReactionRunner()
            {
                isScheduled = false; // Q: move into reaction runner?
                if (reaction.IsDisposed)
                {
                    return(Task.CompletedTask);
                }

                var changed = false;

                reaction.Track(() =>
                {
                    var nextValue = expression(reaction);
                    changed       = firstTime || !equals(value, nextValue);
                    value         = nextValue;
                });
                if (firstTime && reactionOptions.FireImmediately)
                {
                    action(value, reaction);
                }

                if (!firstTime && changed)
                {
                    action(value, reaction);
                }

                if (firstTime)
                {
                    firstTime = false;
                }

                return(Task.CompletedTask);
            }

            reaction.Schedule();
            return(new DisposableDelegate(() => reaction.Dispose()));
        }
        /// <summary>
        /// Creates a reaction that operates on data of type T.
        /// </summary>
        /// <typeparam name="T">The type the reaction operates on.</typeparam>
        /// <param name="sharedState">The shared state to use.</param>
        /// <param name="expression">The expression that delivers a value.</param>
        /// <param name="effect">The effect that is executed when the value changes.</param>
        /// <param name="reactionOptions">The options to use for the reaction.</param>
        /// <returns>An <see cref="IDisposable"/> instance that can be used to stop the reaction.</returns>
        /// <remarks>Only pass asynchronous effect functions that wrap state modifications in actions.</remarks>
        public static IDisposable Reaction <T>(this ISharedState sharedState, Func <Reaction, T> expression, Func <T, Reaction, Task> effect, ReactionOptions <T> reactionOptions = null)
        {
            if (sharedState is null)
            {
                throw new ArgumentNullException(nameof(sharedState));
            }

            if (expression is null)
            {
                throw new ArgumentNullException(nameof(expression));
            }

            if (effect is null)
            {
                throw new ArgumentNullException(nameof(effect));
            }

            if (reactionOptions is null)
            {
                reactionOptions = new ReactionOptions <T>();
            }

            var name   = reactionOptions.Name ?? $"Reaction@{sharedState.GetUniqueId()}";
            var action = reactionOptions.ErrorHandler != null?WrapErrorHandler(reactionOptions.ErrorHandler, effect) : effect;

            var runSync = reactionOptions.Scheduler == null && reactionOptions.Delay == 0;

            var      firstTime   = true;
            var      isScheduled = false;
            T        value       = default;
            Reaction reaction    = null;

            var equals =
                reactionOptions.EqualityComparer != null ?
                new Func <T, T, bool>(reactionOptions.EqualityComparer.Equals) :
                new Func <T, T, bool>((x, y) => Equals(x, y));

            var scheduler = CreateSchedulerFromOptions(reactionOptions, async() =>
            {
                await ReactionRunner().ConfigureAwait(true);
            });

#pragma warning disable CA2000  // Dispose objects before losing scope
#pragma warning disable IDE0067 // Dispose objects before losing scope
            reaction = new Reaction(
                sharedState,
                name,
                () =>
            {
                if (firstTime || runSync)
                {
                    var taskScheduler = sharedState.GetTaskScheduler();
                    isScheduled       = true;
                    Task.Factory.StartNew(
                        ReactionRunner,
                        CancellationToken.None,
                        TaskCreationOptions.DenyChildAttach,
                        taskScheduler).Unwrap().ContinueWith(
                        t =>
                    {
                        if (t.Exception != null)
                        {
                            reaction.ReportExceptionInReaction(t.Exception);
                        }
                    }, taskScheduler);
                }
                else if (!isScheduled)
                {
                    var taskScheduler = sharedState.GetTaskScheduler();
                    isScheduled       = true;
                    Task.Factory.StartNew(
                        scheduler,
                        CancellationToken.None,
                        TaskCreationOptions.DenyChildAttach,
                        taskScheduler).Unwrap().ContinueWith(
                        t =>
                    {
                        if (t.Exception != null)
                        {
                            reaction.ReportExceptionInReaction(t.Exception);
                        }
                    }, taskScheduler);
                }
            }, reactionOptions.ErrorHandler);
#pragma warning restore IDE0067 // Dispose objects before losing scope
#pragma warning restore CA2000  // Dispose objects before losing scope

            async Task ReactionRunner()
            {
                isScheduled = false; // Q: move into reaction runner?
                if (reaction.IsDisposed)
                {
                    return;
                }

                var changed = false;

                reaction.Track(() =>
                {
                    var nextValue = expression(reaction);
                    changed       = firstTime || !equals(value, nextValue);
                    value         = nextValue;
                });
                if (firstTime && reactionOptions.FireImmediately)
                {
                    await action(value, reaction).ConfigureAwait(true);
                }

                if (!firstTime && changed)
                {
                    await action(value, reaction).ConfigureAwait(true);
                }

                if (firstTime)
                {
                    firstTime = false;
                }
            }

            reaction.Schedule();
            return(new DisposableDelegate(() => reaction.Dispose()));
        }