adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
597 stars 50 forks source link

Children not render #43

Closed crimer closed 1 year ago

crimer commented 1 year ago

Hi, I have a page that can change its own state. Below is an example. When I click the button, the state changes to the "Loading state" and then it automatically change back, but Children are not displayed.

public class FirstPage : Component
{
    private BasePage _baseRef;

    public override VisualNode Render() => new BasePage(r => _baseRef = r)
    {
        new VStack()
            {
                new Label("First!"),
                new Button("Change view")
                    .OnClicked(() =>
                    {
                        _baseRef.SetIsLoading();
                        Task.Run(async () =>
                        {
                            await Task.Delay(TimeSpan.FromSeconds(2));
                            _baseRef.SetIsSuccess();
                        });
                    })
            }
            .Spacing(20)
            .HCenter()
            .VCenter()
    };
}

public enum PageState
{
    Success = 0,
    Error = 1,
    Loading = 2
}

public class BasePageState
{
    public PageState PageState { get; set; }
}

public class BasePage : Component<BasePageState>
{
    public BasePage(Action<BasePage> func) => func(this);

    public override VisualNode Render()
    {
        return new ContentPage()
        {
            State.PageState == PageState.Loading ? new Label("Loading view") : null,
            State.PageState == PageState.Error ? new Label("Error view") : null,
            State.PageState == PageState.Success ? Children() : null,
        }.Title("Base");
    }

    public void SetIsLoading() => SetState(prev => { prev.PageState = PageState.Loading; });

    public void SetIsError() => SetState(prev => { prev.PageState = PageState.Error; });

    public void SetIsSuccess() => SetState(prev => { prev.PageState = PageState.Success; });
}

public class App : Component
{
    public override VisualNode Render()
    {
        return new NavigationPage()
        {
            new FirstPage()
        }.Set(Microsoft.Maui.Controls.NavigationPage.BarTextColorProperty, Colors.White);
    }
}
adospace commented 1 year ago

Hi, I see a few errors in your code, you're mixing imperative and declarative approaches. Try to always to declare what should be your view based on the state. As a general rule, State is owned by the parent component and passed to the child as props. Another thing you should always avoid is taking references to visual node elements like a component because they are replaced every time the render tree is recreated. This is something you should write instead

public enum PageState
{
    Success = 0,
    Error = 1,
    Loading = 2
}

public class FirstPageState
{
    public PageState PageState { get; set; }
}

public class FirstPage : Component<FirstPageState>
{
    public override VisualNode Render() 
        => new BasePage()
        {
            new VStack()
            {
                new Label("First!"),
                new Button("Change view")
                    .OnClicked(() =>
                    {
                        SetState(s => s.PageState = PageState.Loading);
                        Task.Run(async () =>
                        {
                            await Task.Delay(TimeSpan.FromSeconds(2));
                            SetState(s => s.PageState = PageState.Success);
                        });
                    })
            }
            .Spacing(20)
            .HCenter()
            .VCenter()
        }
        .IsLoading(State.PageState == PageState.Loading)
        .IsError(State.PageState == PageState.Error)
        ;
}

public class BasePage : Component
{
    private bool _isLoading;
    private bool _isError;

    public BasePage IsLoading(bool isLoading)
    {
        _isLoading = isLoading;
        return this;
    }

    public BasePage IsError(bool isError)
    {
        _isError = isError;
        return this;
    }

    public override VisualNode Render()
    {
        return new ContentPage()
        {
            _isLoading ? new Label("Loading view") : null,
            _isError ? new Label("Error view") : null,
            !_isLoading && !_isError ? Children() : null,
        }.Title("Base");
    }
}

public class App : Component
{
    public override VisualNode Render()
    {
        return new NavigationPage()
        {
            new FirstPage()
        }.Set(Microsoft.Maui.Controls.NavigationPage.BarTextColorProperty, Colors.White);
    }
}
crimer commented 1 year ago

Thanks for help. My experience using React JS made me make some kind of the Context. MauiReactor has a CreateParameter that will allow me use shared state in child components. Is it possible to make a general context like in React JS with a state and functions to change it or useful functions that may be used on many pages? Meybe this feature will be come later?)

adospace commented 1 year ago

Well, yes, the Parameters feature (https://adospace.gitbook.io/mauireactor/components/component-parameters) is something similar (actually inspired to) ReactJs context but it's not of course a global state manager like Redux etc. KeeMind sample https://github.com/adospace/kee-mind uses the parameters approach.

One other option is to use dependency injection and inject your global state into your components. You could subscribe/unsubscribe to state-changed events when required through the combination OnMount/OnPropsChange/OnWillUnmount.

In my applications I usually start using the normal way to pass state to children using props, moving to other approaches when I saw the real benefit but of course, it's just a matter of choice.

crimer commented 1 year ago

Okay, I got it, I'll look at your example, thank you