symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
831 stars 300 forks source link

[LiveComponent] Re-read url query params when re-render? #2233

Open Nayte91 opened 1 week ago

Nayte91 commented 1 week ago

Hello,

Currently, imagine you have a page with a FacetMenu LC and another ArticleResult LC. FacetMenu write and read the URL query params (searchTerm, status, page, ...), and ArticleResult read them.

When you first load your page with some query params, everything is OK; Both components are able to read the query params. But when I modify an input in the facet menu, here how I make it work for now:

#[AsLiveComponent]
class FacetMenu
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveProp(writable: true, url: true, onUpdated: 'onChange')]
    public string $name = '';

    #[LiveProp(writable: true, url: true, onUpdated: 'onChange')]
    public string $status = '';

    #[LiveProp(writable: true, url: true, onUpdated: 'onChange')]
    public int $page = 1;

    #[LiveAction]
    public function reset(): void
    {
        $this->name = '';
        $this->status = '';
        $this->page = 1;

        $this->onChange();
    }

    #[PostMount]
    public function onChange(): void
    {
        $this->page = 1;
        $this->emit(
            'facetSetted',
            array_filter(
                get_object_vars($this),
                fn($value, $key) => in_array($key, ['name', 'status', 'page']),
                ARRAY_FILTER_USE_BOTH
            )
        );
    }
}

#[AsLiveComponent]
class ArticleResults
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveProp(url: true)]
    public ?string $name = null;

    #[LiveProp(url: true)]
    public ?string $status = null;

    #[LiveProp(url: true)]
    public ?int $page = 1;

    private ItemsPage $articlesPage;

    public function __construct(private readonly QueryBus $queryBus) {}

    #[LiveListener('facetSetted')]
    public function reload(
        #[LiveArg] string $name,
        #[LiveArg] string $status,
        #[LiveArg] int $page,
    ): void {
        $this->name = $name;
        $this->status = $status;
        $this->page = $page;

        $this->getArticlesPage();
    }

    public function getArticlesPage(): ItemsPage
    {
        return $this->articlesPage ??= $this->searchArticles();
    }

    private function searchArticles(): ItemsPage
    {
        $filter = new Filter(
            $this->page,
            $this->name,
            $this->status
        );

        return $this->queryBus->query(new GetArticlesPaginated($filter));
    }
}

I feel like packing properties from SearchMenu, serializing them to send them through JS, then recall them one by one as method parameters, and reallocate them before doing logic, is rewriting each property basically 3 times, for components that already know about the properties, as they welcome them from URL when first loading.

So do you know a way to tell, before doing logic in a given method, to "re-read" the url as it can have changed? Or do you see another way to achieve this? I know about the UX icons implementation, but the search bar AND the results div are on the same component; So another way to ask my question is "how would you design the icons page if you have to do 2 separate components for SearchBar and IconResults? How would you pass the parameters between them?"

  1. change the behavior of "url:true" LiveProps to check again the values from the URL when re-render?
  2. propose a readUrl() method (maybe make a UrlTrait) that will make the component re-read the URL and re-hydrate the LiveProps?

Part of the discussion about https://github.com/symfony/ux/discussions/2212 (challenge point 2).

smnandre commented 6 days ago

Having two components sharing the same live props (ie: managed as statefull props and stored in the dom) looks a bit to me as some "global state".

Do both of your components really need to have these values as LiveProps ? Could one of them (or a third one) be responsible to store this data and signal when it has some changes ? (i confirm the 3 events system via the browser is probably not the most efficient method)

Nayte91 commented 6 days ago

Having two components sharing the same live props (ie: managed as statefull props and stored in the dom) looks a bit to me as some "global state".

That's exactly my starting point for this design, as I already encountered it on React; Simple answer is that query is a global state, the mother of all, over the session, over the sharing with friends, over every components on the page.

Do both of your components really need to have these values as LiveProps ? Could one of them (or a third one) be responsible to store this data and signal when it has some changes ? (i confirm the 3 events system via the browser is probably not the most efficient method)

I'm not sure if using query params is the most-efficient-method-the-humanity-will-ever-find, maybe it is, probably not, but it's a way to achieve. Pros are:

It's totally possible that it exists a better optimized design, I would gladly use it if so 😅. For now, I feel like if you want to pass an arbitrary (potentially long) number of params, where no one is "hidden", and you want it to be intuitive for end user, it's a good candidate 🙏.

Why do you think that going up to browser then down could be not efficient? Could be slow? When you tell "be responsible to store this data", which solution of sharing the prop between LCs you have in mind?

smnandre commented 6 days ago

somehow elegantly designed as the search Menu writes, and the Result compo reads

This is certainly your intention, but you currently configure both your components to read and write the query string.

Simple answer is that query is a global state, the mother of all, over the session, over the sharing with friends, over every components on the page.

Im not talking about using the query string ;) I was mentionning using it twice as a storage, with possibilities for endless loops.

But this is philosophical, and if you want to do it that way i'm not the one who's gonna prevent you to 😅

Personnaly, i'd use only one component for this, and delegate / pass this data to other ones, but again, preferences :)

Why do you think that going up to browser then down could be not efficient? Could be slow?

You are going to make 3 XHR calls every time you change something in the URL, i would try to avoid that (because networking is the most failure-prone part of any web app)

which solution of sharing the prop between LCs you have in mind?

Sharing data can be done with nested components, events, shared "registry-of-any-kind", local storage.

But sharing "props" feels to me (i insist on the feel part) an anti-pattern. In my mind, a prop cannot be a prop if it is shared with someone else.

Or, to be precise, a read/write prop.

Nayte91 commented 3 days ago

This is certainly your intention, but you currently configure both your components to read and write the query string.

Oh that's a nice catch! You're right, I can remove "writable: true" on the Result LC. It works, I appreciate! Example on previous post updated accordingly.

You are going to make 3 XHR calls every time you change something in the URL, i would try to avoid that (because networking is the most failure-prone part of any web app)

Why 3 ?

  1. One from Menu LC to update URL,
  2. one from Menu LC to tell the Results LC that there's changes,
  3. one from Results LC to read updated URL?

Sharing data can be done with nested components, events, shared "registry-of-any-kind", local storage.

You're maybe right, I will definitely try to build this upon two or three components: one parent that will "centralize" the $filter, and child for menu and/or results. That's maybe better! Keep you in touch.

Nayte91 commented 3 days ago

Thoughts: there should be a global event like dom navigate I can listen when url changes. Maybe we can relate a stimulus action to this event, pushing back the data to the LC? Something setted automatically like #[LiveProp(url: true, urlListener: true)]? I need to test on my side how it works manually, to begin with. If I programmatically change the url, is the event fired by the browser, and how it looks like?

Edit: I failed to do it :sob: Not sure how to suscribe to this event window.navigation.addEventListener("navigate") with stimulus and send the action to a LC!