platform icon indicating copy to clipboard operation
platform copied to clipboard

docs(`@ngrx/signals`): add a mention of restructuring `withComputed` or `withMethods` for re-use in those features

Open michael-small opened this issue 11 months ago • 9 comments

Information

The first major stumbling point I had with using features like withComputed or withMethods was the need to reference other computeds or methods in those features. For example, if I wanted a withComputed to expose be able to return a fullName that was a store's firstName and store's lastName, and then also expose a allCapsFullName that was the full name computed inside another computed. But do to what I thought was a limitation of how to return an object like withComputed(({ firstName, lastName }) => ({...}), I had to make a second withComputed

  withComputed(({ firstName, lastName }) => ({
    fullName: computed(() => {
      return `${firstName()} ${lastName()}`;
    }),
  })),
  withComputed(({ fullName }) => ({
    allCapsFullName: computed(() => {
      return fullName().toUpperCase();
    }),
  })),

However, someone pointed out that the first withComputed could be restructured to have a return block, with helper consts or functions.

  withComputed(({ firstName, lastName }) => {
    const fullName = computed(() => {
      return `${firstName()} ${lastName()}`;
    })
    return {
      fullName: fullName,
      allCapsFullName: computed(() => {
        return fullName().toUpperCase();
      }),
    }
  }),

In retrospect this is fairly obvious, but I see the former approach all the time and was stuck in that mindset myself. I think partially because most examples of things like withComputed have those returns without a return block, and because there is a lot of little syntax nuances with functions and objects in the signal store to become comfortable with that add up.

I think something like an FAQ page point on this is warranted with how often this little nuance turns into what is perceived as a bigger problem than it is with an easy solution. Or some examples of the second syntax baked into other pages. Willing to formalize/condense this language to make a PR.

Documentation page

https://ngrx.io/guide/signals/faq

I would be willing to submit a PR to fix this issue

  • [X] Yes
  • [ ] No

michael-small avatar Jan 10 '25 14:01 michael-small

@michael-small, I think both approaches work fine. Sometimes, I'd like to point out that there are "foundational" methods or computeds. In that case, two features are exactly what I need. Maybe add both options to the FAQ?

rainerhahnekamp avatar Jan 20 '25 23:01 rainerhahnekamp

Yeah, good point. Both are valid, and I imagine one may come to people more naturally than the other. But it is important to know they both exist for whatever approach the user may need., the foundational blocks, or the self contained.

michael-small avatar Jan 21 '25 03:01 michael-small

I think the first approach is more obvious and natural, and people already "know" this because its behavior is mentioned in the docs IIRC.

The second approach can be a useful "tip" that isn't very obvious. I've seen some questions about this, so that's why I propose to include the tip in the main documentation. Similar to how we already point things out, e.g :

Image

timdeschryver avatar Feb 16 '25 17:02 timdeschryver

That makes sense. So essentially what you are saying if I understand right, @timdeschryver , it would be some tip like that in one of the non-FAQ pages (possibly "Core Concepts" ??), and wrapped in one of those <div class="alert is-helpful"> helpers.

@rainerhahnekamp sounds legit?

michael-small avatar Feb 16 '25 23:02 michael-small

Yep, that's exactly what I mean @michael-small

timdeschryver avatar Feb 18 '25 18:02 timdeschryver

Yup @michael-small

rainerhahnekamp avatar Feb 18 '25 18:02 rainerhahnekamp

@timdeschryver @rainerhahnekamp

Thoughts on this being the example (short of maybe comments inline)?

  withComputed(({ books, filter }) => {
    const direction = computed(() => (filter.order() === 'asc' ? 1 : -1));

    const sortBooks = (direction: number) =>
      books().toSorted((a, b) => direction * a.title.localeCompare(b.title));

    return {
      sortedBooks: computed(() => sortBooks(direction())),
      reverseSortedBooks: computed(() => sortBooks(-1 * direction())),
    };
  }),

Benefits

  1. Jumps off of existing example, but introduces a need for re-use.
  2. Tangible example of re-using
    • a helper computed value, like you may want in this withComputed block
    • an arrow function, like you may also want in withMethods as well
    • Either withComputed or withMethods can have their own computed/function helpers

michael-small avatar Mar 16 '25 20:03 michael-small

@michael-small sorry for the late reply. I like the proposed example 👍

With seeing the example, I think we can combine both approaches in the same snippet. I think this is the most efficient way to do it. As developers we tend to just take a look at the snippets and try to understand them 😅

The existing snippet can be changed to:

export const BooksStore = signalStore(
  withState(initialState),
  // 👇 Accessing previously defined state signals and properties.
  withComputed(({ books, filter }) => ({
    booksCount: computed(() => books().length),
    sortDirection: computed(() => filter.order() === 'asc' ? 1 : -1),
   })),
   // 👇 Also access previously defined computed properties.
   withComputed(({ books, sortDirection }) => {
    // 👇 Define helper functions.
    const sortBooks = (direction: number) =>
      books().toSorted((a, b) => direction * a.title.localeCompare(b.title));

    return {
      sortedBooks: computed(() => sortBooks(sortDirection())),
      reversedBooks: computed(() => sortBooks(-1 * sortDirection())),
    };
  }),
);

timdeschryver avatar Apr 05 '25 17:04 timdeschryver

@timdeschryver thank you. I'll think of a good spot to drop it in on the page and make a PR.

michael-small avatar Apr 07 '25 22:04 michael-small