react4j.arez.ReactArezComponent Maven / Gradle / Ivy
package react4j.arez;
import arez.Arez;
import arez.ArezContext;
import arez.Disposable;
import arez.Observer;
import arez.annotations.Action;
import arez.annotations.ComponentIdRef;
import arez.annotations.ComponentNameRef;
import arez.annotations.ContextRef;
import arez.annotations.ObserverRef;
import arez.annotations.PreDispose;
import arez.spy.ObservableValueInfo;
import arez.spy.ObserverInfo;
import elemental2.promise.Promise;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import jsinterop.base.Js;
import jsinterop.base.JsPropertyMap;
import react4j.Component;
/**
* A base class for all Arez enabled components. This class makes the component
* rendering reactive and it will schedule a re-render any time any of the observable
* entities accessed within the scope of the render method are changed.
*
* To achieve this goal, the props and state of the component are converted into
* observable properties. This of course means they must be accessed within the scope
* of an Arez transaction. (Typically this means it needs to be accessed within the
* scope of a {@link Action} annotated method or within the scope of the render method.
*/
public abstract class ReactArezComponent
extends Component
{
/**
* A non-null lock that will be released in the next micro-task which will schedule any renders required.
*/
@Nullable
private static Disposable c_schedulerLock;
private boolean _renderDepsChanged;
private boolean _unmounted;
protected ReactArezComponent()
{
}
/**
* After construction of the object. Schedule any autoruns attached to component.
*/
@Override
protected final void performPostConstruct()
{
super.performPostConstruct();
triggerScheduler();
}
/**
* Template method overridden by annotation processor if there are autoruns to schedule.
*/
protected void triggerScheduler()
{
}
/**
* Return true if the render dependencies have been marked as changed and component has yet to be re-rendered.
*
* @return true if render dependencies changed, false otherwise.
*/
protected final boolean hasRenderDepsChanged()
{
return _renderDepsChanged;
}
/**
* Helper method invoked when it has detected a dependency of the render method has changed.
*
* @param componentHasObservableProps flag indicating whether the component has any observable props.
*/
protected final void onRenderDepsChange( final boolean componentHasObservableProps )
{
if ( !_renderDepsChanged )
{
_renderDepsChanged = true;
if ( !_unmounted )
{
scheduleRender( !componentHasObservableProps );
}
}
}
protected final void clearRenderDepsChanged()
{
_renderDepsChanged = false;
}
/**
* Return the arez context that this component is associated with.
* The component is associated with the context that was active when it was created
* and can only react to observables associated with the same context.
*
* @return the arez context that this component is associated with.
*/
@ContextRef
@Nonnull
protected abstract ArezContext getContext();
/**
* Return the unique identifier of component according to Arez.
*
* @return the unique identifier of component according to Arez.
*/
@ComponentIdRef
protected abstract int getArezComponentId();
/**
* Return the name of the component according to Arez.
*
* @return the name of the component according to Arez.
*/
@ComponentNameRef
protected abstract String getArezComponentName();
/**
* Return the Observer associated with the render tracker method.
*
* @return the Observer associated with the render tracker method.
*/
@ObserverRef
@Nonnull
protected abstract Observer getRenderObserver();
/**
* The first time an arez component is rendered it will lock the Arez scheduler and release
* the lock in the micro-task immediately following the task that prompted the render. If this
* is not done it is possible that Arez can re-trigger a component render when the scheduler is
* triggered after the tracked render completes but before the render method has returned to the
* react runtime. This results in error message from react as a setState()/forceRender() was invoked
* while still within a render() method.
*
* NOTE: While render methods are read-only transactions, they can un-observe components with
* disposeOnDeactivate=true
that would result in the component being disposed and
* triggering an update that would mark particular React Components/Observers as STALE and trigger
* a re-render of that component.
*/
protected final void pauseArezSchedulerUntilRenderLoopComplete()
{
if ( null == c_schedulerLock )
{
c_schedulerLock = getContext().pauseScheduler();
// Use a hack of an empty promise that immediately resolves to
// schedule the block immediately after this call stack pops.
Promise.resolve( (Object) null ).then( ignored -> {
c_schedulerLock.dispose();
c_schedulerLock = null;
return null;
} );
}
}
@PreDispose
protected void preDispose()
{
_unmounted = true;
}
/**
* {@inheritDoc}
*/
@Override
protected final void populateDebugData( @Nonnull final JsPropertyMap