State of Lit in Hilla

Hey,

I recently heard some conversation by frontend devs moving away from React to Lit for simplicity.

What is the state of Lit with Hilla?

Personally I preferred that as well.

Thanks Simon

1 Like

The frontend world has had waves of “React is too bloated – let’s use simpler web standards” for many years already while the actual usage share of React remains astronomically high.

Vaadin’s support remains the same as before. Both Lit and React are supported with React being the primary way in e.g. the official documentation. We have no plans of changing this in either direction.

Perfect!

I currently have to customers planning to use Hilla. One with React and one with Lit.

I personally prefer Lit, because I never learned React (yet) :wink:

For new projects or migrations I would consider to use React instead of Lit, because the primary focus is there, as Lief said.
What I am currently observing: some components or features are react-only. But it would be nothing that cannot be reproduced with Lit with some effort.

Also you can use Lit anytime in React, if necessary or migrating.

1 Like

I was involved in an ecommerce system a few years ago where we use Lit and the developers where happy because of the simplicity. For Java developers there was not much new to learn.

A lot of Java shops are afraid of React because of the learning curve.

1 Like

We did a couple of high-level components such as AutoGrid initially for React with plans to also do it for Lit later if there’s enough interest. Since then, there has been a little bit of interest but not enough for us to make the investment. This is in part also because we’re overall investing less in Hilla improvements after the strategy adjustment we announced last year: Merging Hilla into Flow: Embracing the Java core | Vaadin

1 Like

Yes am totally fine with this strategy.
I checked docs again: only 3 components are React-only. And the latest components like Popover, Badge or Card are fully usable in Lit :slight_smile:

As of features, I was thinking about the Signals. I didn’t use them yet, but as far as I checked the docs, it is only available for React.

“Regular” local signals can be used with Lit without anything for Vaadin using e.g. Signals – Lit.

The “full-stack” signals that are automatically synchronized with the server are only available as a prototype for React but we haven’t yet finalized that implementation.

2 Likes

I built something using FluxSink.
But I have to check out the Signals from Lit.

@Endpoint
@PermitAll
public class LockEndpoint {

    public static record Lock(String ident, Integer version, String user, Instant createdAt, Instant until) {
        public Lock(String ident, Integer version, String user) {
            this(ident, version, user, Instant.now(), Instant.now().plusSeconds(LOCK_TIMEOUT_SECONDS));
        }

        public Lock(Lock other) {
            this(other.ident, other.version, other.user, Instant.now(),
                    Instant.now().plusSeconds(LOCK_TIMEOUT_SECONDS));
        }
    }

    public enum LockChangeType {
        LOCKED, UNLOCKED;
    }

    public static record LockChange(Lock lock, LockChangeType changeType) {
    }

    public enum LockResponseType {
        CONFIRM, DENIED, NONE
    }

    public static record LockResponse(Lock lock, LockResponseType responseType) {
        public static LockResponse empty() {
            return new LockResponse(null, LockResponseType.NONE);
        }

        public static LockResponse deny(Lock lock) {
            return new LockResponse(lock, LockResponseType.DENIED);
        }

        public static LockResponse confirm(Lock lock) {
            return new LockResponse(lock, LockResponseType.CONFIRM);
        }
    }

    // contants
    private static final int LOCK_TIMEOUT_SECONDS = 60 * 15;
    private final Logger logger = LoggerFactory.getLogger(LockEndpoint.class);

    // services
    private final UserInfoService userInfoService;

    // data
    private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();

    // event bus
    private FluxSink<LockChange> emitter;
    private Flux<LockChange> events;

    public LockEndpoint(UserInfoService userInfoService) {
        this.userInfoService = userInfoService;
    }

    /*
     * 
     * Locks
     * 
     */

    public @NonNull LockResponse checkLock(@NonNull String ident) {
        var l = locks.get(ident);
        if (l == null)
            return LockResponse.empty();
        return isCurrentUser(l) ? LockResponse.confirm(l) : LockResponse.deny(l);
    }

    public @NonNull LockResponse lock(@NonNull Lock newLock) {
        final var current = checkLock(newLock.ident);
        if (current.responseType == LockResponseType.NONE) {
            var l = new Lock(newLock);
            setLock(l, true);
            return LockResponse.confirm(l);
        } else {
            return current;
        }
    }

    public @NonNull LockResponse update(@NonNull Lock newLock) {
        final var current = checkLock(newLock.ident);
        if (current.responseType == LockResponseType.CONFIRM) {
            var l = new Lock(newLock);
            setLock(l, true);
            return LockResponse.confirm(l);
        }
        return current;
    }

    public @NonNull LockResponse unlock(@NonNull String ident) {
        final var current = checkLock(ident);
        if (current.lock != null && current.responseType == LockResponseType.CONFIRM) {
            setLock(current.lock, false);
            return LockResponse.confirm(null);
        }
        return current;
    }

    private boolean isCurrentUser(Lock lock) {
        Objects.requireNonNull(lock);
        UserInfo userInfo = userInfoService.getUserInfo();
        return userInfo != null && userInfo.getName() != null && userInfo.getName().equals(lock.user);
    }

    private void setLock(Lock lock, boolean set) {
        Objects.requireNonNull(lock);
        LockChange change;
        if (set) {
            this.locks.put(lock.ident, lock);
            change = new LockChange(lock, LockChangeType.LOCKED);
        } else {
            Lock remove = this.locks.remove(lock.ident);
            change = new LockChange(remove, LockChangeType.UNLOCKED);
        }
        this.publishEvent(change);
    }

    /*
     * 
     * Events
     * 
     */

    @PostConstruct
    void init() {
        this.events = Flux.<LockChange>create(this::setEmitter).share();
    }

    private void setEmitter(FluxSink<LockChange> emitter) {
        this.emitter = emitter;
    }

    @PreDestroy
    void destroy() {
        if (this.emitter != null) {
            this.emitter.complete();
        }
    }

    private void publishEvent(LockChange change) {
        if (this.emitter == null) {
            logger.debug("Event emitter is null.");
            return;
        }
        this.emitter.next(change);
    }

    public Flux<LockChange> registerFlux(String ident) {
        return Flux.from(this.events)
                .filter(change -> change != null && change.lock != null && change.lock.ident().equals(ident));
    }

    @Scheduled(fixedRate = 1000 * 30)
    protected void scheduledCheckTimedOutLocks() {
        var toBeRemoved = new ArrayList<Lock>();
        for (Entry<String, Lock> entry : this.locks.entrySet()) {
            var creation = entry.getValue().createdAt;
            if (creation == null || creation.isBefore(Instant.now().minusSeconds(LOCK_TIMEOUT_SECONDS))) {
                toBeRemoved.add(entry.getValue());
            }
        }
        toBeRemoved.forEach(lock -> setLock(lock, false));
    }

}