
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.ComponentId;
import arez.annotations.ContextRef;
import arez.annotations.ObserverRef;
import arez.annotations.OnDepsChanged;
import arez.annotations.Track;
import arez.spy.ObservableInfo;
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 org.realityforge.braincheck.Guards;
import react4j.core.BaseContext;
import react4j.core.BaseProps;
import react4j.core.BaseState;
import react4j.core.Component;
import react4j.core.Procedure;
import react4j.core.ReactNode;
import react4j.core.util.JsUtil;
/**
* 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;
protected ReactArezComponent()
{
_arezComponentId = c_nextComponentId++;
}
/**
* Method invoked when props changes.
*/
protected abstract void reportPropsChanged( @Nullable final P nextProps );
/**
* {@inheritDoc}
*/
@Override
protected final void scheduleStateUpdate( @Nonnull final SetStateCallback
callback,
@Nullable final Procedure onStateUpdateComplete )
{
Guards.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()
{
_renderDepsChanged = true;
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}
*/
@Track( name = "render" )
@Nullable
@Override
protected ReactNode performRender()
{
_renderDepsChanged = false;
return super.performRender();
}
/**
* {@inheritDoc}
*/
@Override
protected boolean shouldComponentUpdate( @Nullable final P nextProps,
@Nullable final BaseState nextState,
@Nullable final C nextContext )
{
if ( hasRenderDepsChanged() )
{
return true;
}
//noinspection SimplifiableIfStatement
if ( !Js.isTripleEqual( super.state(), nextState ) )
{
// If state is not identical then we need to re-render ...
// Previously we chose not to re-render if only AREZ_STATE_KEY that was updated but that
// meant deps in DevTools would not be update so now we just re-render anyway.
return true;
}
else
{
/*
* We just compare the props shallowly and avoid a re-render if the props have not changed.
*/
final boolean modified = JsUtil.isObjectShallowModified( super.props(), nextProps );
if ( modified )
{
reportPropsChanged( nextProps );
}
return modified;
}
}
/**
* {@inheritDoc}
*/
@Override
protected void componentDidMount()
{
storeArezDataAsState();
}
/**
* {@inheritDoc}
*/
@Override
protected void componentDidUpdate( @Nullable final P prevProps, @Nullable final BaseState prevState )
{
storeArezDataAsState();
}
/**
* {@inheritDoc}
*/
@Override
protected void componentWillUnmount()
{
/*
* Dispose of all the arez resources. Necessary particularly for the render tracker that should
* not receive notifications of updates after the component has been unmounted.
*/
Disposable.dispose( this );
}
/**
* Store arez data such as dependencies on the state of component.
* This is only done if {@link ReactArezConfig#shouldStoreArezDataAsState()} returns true and is primarily
* done to make it easy to debug from within React DevTools.
*/
private void storeArezDataAsState()
{
if ( ReactArezConfig.shouldStoreArezDataAsState() && Arez.areSpiesEnabled() )
{
final Observer renderTracker = getRenderObserver();
final List dependencies = getContext().getSpy().getDependencies( renderTracker );
final JsPropertyMap