Skip to content

Add rune to more easily combine reactive values and state objects #11739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
StevenStavrakis opened this issue May 23, 2024 · 5 comments
Closed

Comments

@StevenStavrakis
Copy link

StevenStavrakis commented May 23, 2024

Describe the problem

Problem

I'm finding a lot of use out of separating portions of state into different objects and combining them as needed. For example, I am working on a project for playing chess with variants. Every single variant is going to have some number of state properties, but also some state specific to that variant.

In order for derived values and state values to exist on the same object, we have to return an object with getters and setters (AFAIK).

So, the base state can be created as follows:

export const createBaseChessState = (moveValidator: MoveValidationService): T_BaseBoardState => {

    let pov: Color = $state('w')
    let selectedSquare: Square | null = $state(null);
    let handlePromotion: ((piece: PieceSymbol) => void) | null = $state(null);

    let validMoveSquares: Square[] = $derived.by(() => {
        if (selectedSquare === null) {
            return [];
        }
        return moveValidator.getValidMoves(selectedSquare).map(move => move.to);
    });
    let showPromotionDialog: boolean = $derived(handlePromotion !== null);

    let currentBoard: Board = $state([]);
    let checkSquare: Square | null = $state(null);


    return {
        get pov() {
            return pov;
        },
        set pov(value: Color) {
            pov = value;
        },
        get selectedSquare() {
            return selectedSquare;
        },
        set selectedSquare(value: Square | null) {
            selectedSquare = value;
        },
        get handlePromotion() {
            return handlePromotion;
        },
        set handlePromotion(value: ((piece: PieceSymbol) => void) | null) {
            handlePromotion = value;
        },
        get validMoveSquares() {
            return validMoveSquares;
        },
        get showPromotionDialog() {
            return showPromotionDialog;
        },
        get currentBoard() {
            return currentBoard;
        },
        set currentBoard(value: Board) {
            currentBoard = value;
        },
        get checkSquare() {
            return checkSquare;
        },
        set checkSquare(value: Square | null) {
            checkSquare = value;
        },
    }
}

and then for something like standard chess, you will also have some more specific state:

    let isCheckmate: boolean = $state(false);
    let isStalemate: boolean = $state(false);
    let isDraw: boolean = $state(false);
    let isThreefoldRepetition: boolean = $state(false);
    let isInsufficientMaterial: boolean = $state(false);
    let isGameOver: boolean = $state(false);

Now, if I want all this state on the same object, I'm going to have to something like this:

export const createStandardChessState = (moveValidator: MoveValidationService): StandardChessState => {
    const baseState = createBaseChessState(moveValidator);

    let isCheckmate: boolean = $state(false);
    let isStalemate: boolean = $state(false);
    let isDraw: boolean = $state(false);
    let isThreefoldRepetition: boolean = $state(false);
    let isInsufficientMaterial: boolean = $state(false);
    let isGameOver: boolean = $state(false);

    return {
        get pov() {
            return baseState.pov;
        },
        set pov(value: Color) {
            baseState.pov = value;
        },
        get selectedSquare() {
            return baseState.selectedSquare;
        },
        set selectedSquare(value: Square | null) {
            baseState.selectedSquare = value;
        },
        get handlePromotion() {
            return baseState.handlePromotion;
        },
        set handlePromotion(value: ((piece: PieceSymbol) => void) | null) {
            baseState.handlePromotion = value;
        },
        get validMoveSquares() {
            return baseState.validMoveSquares;
        },
        get showPromotionDialog() {
            return baseState.showPromotionDialog;
        },
        get currentBoard() {
            return baseState.currentBoard;
        },
        set currentBoard(value: Board) {
            baseState.currentBoard = value;
        },
        get checkSquare() {
            return baseState.checkSquare;
        },
        set checkSquare(value: Square | null) {
            baseState.checkSquare = value;
        },
        get isCheckmate() {
            return isCheckmate;
        },
        set isCheckmate(value: boolean) {
            isCheckmate = value;
        },
        get isStalemate() {
            return isStalemate;
        },
        set isStalemate(value: boolean) {
            isStalemate = value;
        },
        get isDraw() {
            return isDraw;
        },
        set isDraw(value: boolean) {
            isDraw = value;
        },
        get isThreefoldRepetition() {
            return isThreefoldRepetition;
        },
        set isThreefoldRepetition(value: boolean) {
            isThreefoldRepetition = value;
        },
        get isInsufficientMaterial() {
            return isInsufficientMaterial;
        },
        set isInsufficientMaterial(value: boolean) {
            isInsufficientMaterial = value;
        },
        get isGameOver() {
            return isGameOver;
        },
        set isGameOver(value: boolean) {
            isGameOver = value;
        }

    }
}

This is clearly a lot of code for the task. I'm not an expert, but I wonder if there is also a performance hit associated with this kind of stuff that might not be a problem if handled at compile time.

Why would you want to do this?

I find there is a benefit in that I can create specific types for different pieces of state without constructing complex nested state objects. Additionally, having derived and state values on the same object is desirable in many cases.

Describe the proposed solution

It's hard to say for sure, as I don't know exactly how the internals of svelte 5 work, but something like a rune that makes it easier to combine state.

export const createBaseChessState = (moveValidator: MoveValidationService): T_BaseBoardState => {

    let pov: Color = $state('w')
    let selectedSquare: Square | null = $state(null);
    let handlePromotion: ((piece: PieceSymbol) => void) | null = $state(null);

    let validMoveSquares: Square[] = $derived.by(() => {
        if (selectedSquare === null) {
            return [];
        }
        return moveValidator.getValidMoves(selectedSquare).map(move => move.to);
    });
    let showPromotionDialog: boolean = $derived(handlePromotion !== null);

    let currentBoard: Board = $state([]);
    let checkSquare: Square | null = $state(null);


    const state = $restate(
     pov, 
     selectedSquare, 
     handlePromotion, 
     validMoveSquare, 
     showPromotionDialog, 
     currentBoard, 
     checkSquare
    )
    return state;

}
export const createStandardChessState = (moveValidator: MoveValidationService): StandardChessState => {
    const baseState = createBaseChessState(moveValidator);

    let isCheckmate: boolean = $state(false);
    let isStalemate: boolean = $state(false);
    let isDraw: boolean = $state(false);
    let isThreefoldRepetition: boolean = $state(false);
    let isInsufficientMaterial: boolean = $state(false);
    let isGameOver: boolean = $state(false);

   const combinedState = $restate(
   baseState, 
   isCheckmate,
   isStalemate,
   isDraw,
   isThreefoldRepetition,
   isInsufficientMaterial,
   isGameOver
   )
   return combinedState;
}

$restate might be confusing, so there could be $merge, $compose, $combine

Importance

would make my life easier

@longnguyen2004
Copy link

longnguyen2004 commented May 23, 2024

You can make a class, and utilize $state/$derived for class fields (docs)
REPL (check the compiled code and you'll see that it's very similar to what you wrote above)

@StevenStavrakis
Copy link
Author

I'm aware that state and derived can be used on class fields. I suppose you could extend classes to accomplish something similar, but that doesn't permit for the kind of composability that I am looking for since you can't merge two classes, again, as far as I know.

@FoHoOV
Copy link
Contributor

FoHoOV commented May 23, 2024

If I understood you correctly something similar is already discussed in #11210.

@StevenStavrakis
Copy link
Author

Similar, yes. But not the same. I don't care much about writing getters and setters the one time if that's an issue, but it becomes a pain when I have to do it to combine state objects. It also makes the $inspect rune not work as well since every property shows up as getters and setters instead of the actual value.

The recommended solution here is also slightly different, as it is not used on object properties or array elements but takes in state and flattens it into an object. However, whether that is different depends on how the compiler works.

@dummdidumm
Copy link
Member

You can use $state and create an object of properties on it, including using deriveds. You can merge these objects by writing a small utility function yourself using Object.getOwnPropertyDescriptors. Combined this removes almost all the boilerplate. Playground example. Therefore closing because this can be achieved in userland without a new rune.

@dummdidumm dummdidumm closed this as not planned Won't fix, can't repro, duplicate, stale May 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants