react4j.arez.ReactArezComponent Maven / Gradle / Ivy
package react4j.arez;
import arez.Arez;
import arez.ArezContext;
import arez.Disposable;
import arez.Observer;
import arez.Priority;
import arez.annotations.Action;
import arez.annotations.ComponentId;
import arez.annotations.ContextRef;
import arez.annotations.ObserverRef;
import arez.annotations.OnDepsChanged;
import arez.annotations.Track;
import arez.spy.ObservableValueInfo;
import elemental2.core.JsObject;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import jsinterop.base.Js;
import jsinterop.base.JsPropertyMap;
import react4j.Component;
import react4j.Procedure;
import react4j.ReactNode;
import react4j.annotations.Prop;
import static org.realityforge.braincheck.Guards.*;
/**
* 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
{
/**
* Key used to store the arez data in state.
*/
private static final String AREZ_STATE_KEY = "arez";
private static int c_nextComponentId = 1;
private final int _arezComponentId;
private boolean _renderDepsChanged;
private boolean _unmounted;
protected ReactArezComponent()
{
_arezComponentId = c_nextComponentId++;
}
/**
* {@inheritDoc}
*/
@Override
protected final void scheduleStateUpdate( @Nonnull final SetStateCallback callback,
@Nullable final Procedure onStateUpdateComplete )
{
fail( () -> "Attempted to schedule state update on ReactArezComponent subclass. Use Arez @Observable or @Computed properties instead." );
}
/**
* 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;
}
/**
* Hook used by Arez to notify component that it needs to be re-rendered.
*/
@OnDepsChanged
protected final void onRenderDepsChanged()
{
if ( !_renderDepsChanged )
{
_renderDepsChanged = true;
if ( !_unmounted )
{
scheduleRender( true );
}
}
}
/**
* 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.
* This method is invoked by the code generated by the Arez annotation processor.
*
* @return the unique identifier of component according to Arez.
*/
@ComponentId
protected final int getArezComponentId()
{
return _arezComponentId;
}
/**
* Return the Observer associated with the render tracker method.
*
* @return the Observer associated with the render tracker method.
*/
@ObserverRef
@Nonnull
protected abstract Observer getRenderObserver();
/**
* {@inheritDoc}
*/
@Override
protected final ReactNode performRender()
{
return Disposable.isDisposed( this ) ? null : trackRender();
}
/**
* Return true if any prop is an ArezComponent that has been disposed.
* This is used to guard against rendering a react component that has invalid props.
*
* @return true if any prop is an ArezComponent that has been disposed.
*/
protected boolean anyPropsDisposed()
{
return false;
}
/**
* This method is the method enhanced by arez that performs render and tracks dependencies.
* This SHOULD NOT be merged with {@link #performRender()} as then the isDisposed check will be present
* in every instance of render method which can result in unnecessary code bloat.
*
* @return the result of rendering.
*/
@Track( name = "render", priority = Priority.LOW, observeLowerPriorityDependencies = true )
@Nullable
protected ReactNode trackRender()
{
_renderDepsChanged = false;
if ( anyPropsDisposed() )
{
return null;
}
else
{
final ReactNode result = super.performRender();
if ( Arez.shouldCheckInvariants() && Arez.areSpiesEnabled() )
{
final List dependencies =
getContext().getSpy().asObserverInfo( getRenderObserver() ).getDependencies();
invariant( () -> !dependencies.isEmpty(),
() -> "ReactArezComponent render completed on '" + this + "' but the component does not " +
"have any Arez dependencies. This component should extend react4j.Component instead." );
}
return result;
}
}
/**
* {@inheritDoc}
*/
@SuppressWarnings( "SimplifiableIfStatement" )
@Override
protected boolean shouldComponentUpdate( @Nullable final JsPropertyMap