Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should the state be merged at the same level as actions and getters? #71

Closed
rijkvanzanten opened this issue Feb 6, 2020 · 14 comments
Closed

Comments

@rijkvanzanten
Copy link

In the readme, you mention

Should the state be merged at the same level as actions and getters?

I figured we should open a discussion around this 馃檪


My 2 cents:

I think we should either scope all things into relevant properties, eg:

store.state.counter
store.getters.doubleCount
store.actions.reset()

or scope none

store.counter     // state
store.doubleCount // getter
store.reset       // action

Having them all merged in the "root" store might cause naming conflicts between getters and state. However, you could make the argument that having a getter with the exact same name as a property in the state is a fairly bad design decision to begin with. In the case of a naming conflict, the state one should probably overwrite the getter, as getters will often rely on the state value to produce their own value.

A big pro for having them all in the root is that you would be able to do things like

import { useStore } from '@/stores/my-store';

export default createComponent({
  setup() {
    const { counter, doubleCount, reset } = useStore();
  }
});

where you can destructure any of the 'items' in the store.

Is there any specific reason why you decided on putting actions / getters in the root currently @posva?

@posva
Copy link
Member

posva commented Feb 6, 2020

hey, thanks for opening the discussion. Initially I only had getters and I wanted to differentiate them from state. Then I added actions.

Having them all merged in the "root" store might cause naming conflicts between getters and state. However, you could make the argument that having a getter with the exact same name as a property in the state is a fairly bad design decision to begin with.

Totally :)

In the case of a naming conflict, the state one should probably overwrite the getter, as getters will often rely on the state value to produce their own value.

I don't think it really matters as long as there is an explicit error for the user

A big pro for having them all in the root is that you would be able to do things like

I was thinking of making the whole store a shallowReactive to automatically unwrap getters when accessed (instead of having to use .value. In that scenario, destructuring wouldn't work. But I don't think that's a problem as having the store on a variable makes more sense when reading the code because variables are likely to have a broad naming like name, items instead of being prefixed with a name like user.name, cart.items

Personally, I like the idea of grouping everything into the store without having state, getters or actions

@rijkvanzanten
Copy link
Author

rijkvanzanten commented Feb 6, 2020

Would it make sense to use .value on the state properties as well? That way you could say:

store.counter === ref()          // state
store.doubleCount === computed() // getter

It would also help clearing up confusing while working on components "When to use .value and when not to". Currently, the indicator to use .value is the absence of .state., but if actions / getters are both in the root, it could be confusing to have to mix

store.counter
store.doubleCount.value

@posva
Copy link
Member

posva commented Feb 6, 2020

Would it make sense to use .value on the state properties as well?

No, that's not desirable. Having not to use .value is much easier

Currently, the indicator to use .value is the absence of .state., but if actions / getters are both in the root, it could be confusing to have to mix

To be fair it's the same as ref vs reactive. I don't think this is a problem, specially given autocompletion

@posva posva changed the title [Discussion] Should the state be merged at the same level as actions and getters? Should the state be merged at the same level as actions and getters? Feb 7, 2020
@rijkvanzanten
Copy link
Author

With

Getter with params that act like computed properties

implemented, the difference between an action and a getter wouldn't be clear anymore:

store.state.counter      // state
store.doubleCount        // getter
store.counterPlus(5)     // action or getter?

Having them all scoped would solve this, and would also mean it "follows" the structure of the createStore config parameter:

store.state.counter
store.getters.doubleCount
store.getters.counterPlus(5)
store.actions.incrementBy(5)

When / if the getters-with-params functionality is implemented, would Pinia still have "getters without parameters" too? Eg would store.doubleCount become store.doubleCount()?

@posva
Copy link
Member

posva commented Feb 22, 2020

The getters with parameters is different from a function, it's still an idea and no proposal has been made. Otherwise yeah, it would just be an action

@TheJoeSchr
Copy link

Hi, this is a great project, I love it! Something I yearned for a long time now.

Little backstory to understand where I'm coming from:

What brought me to Vue was the sweet, simple style of handling state.
This example from the Vue docs got me all fired up:

<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">Reverse Message</button>
</div>
<script>
var app5 = new Vue({
  el: '#app-5',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})
</script>

Finally a framework using a immutable state as source of truth and then build something from that in a functional fashion with vue.render(), whenever this changes. "Awesome! 馃憤" I thought.

But then Vuex gets recommend a lot and vuex kind of gets in the way of things with their overly boilerplate actions and mutations stringifed syntax. (by no fault of theirs, we simply hadn't the reactive tech yet ).
SoI tended to use vue-apollo instead, but it also gets kind of verbose (see our discussion here)

With this in mind I petition to also changes Pinias' interface to have state, getter & actions on the same level. I think there is real merit in this simplicity. I see Issue #84 also as a expression of this opinion.

Furthermore maybe if the suggestion of getting rid of the .value could be turned on it's head by using computed (like @rijkvanzanten suggested but combine them with a js getter, I think that would be really cool.

I mean something like this sketch

// ** are for empathizing
export const useMainStore = createStore({
  // a function that returns a fresh state
  state: () => ({
    counter: 0,
    **get double()**  {
       return doubleCount.value; // either this
       return counter*2// or maybe don't even need computed        
    },
  }),
  // optional getters
  getters: {
    doubleCount: **computed**(() => state.counter * 2),
  },
// optional actions
  actions: {
    setCounter(val:number) {      
      this.state.counter = val;
    },
  },
})

Now it should work without .value all around

import { useMainStore } from '@/stores/main'

export default defineComponent({
  setup() {
    const {state:main} = useMainStore()
    console.log(store.counter); // 0

    main.counter = 2 // TS-Error , because its  Readonly<T>
    main.setCounter(2); // alrighty
    console.log(main.counter) // 2 
    console.log(main.double) // 4, I guess 馃

    return {
      // gives access to the `counter` and `double`
     ...toRefs(main) // not sure about `toRefs()`

      // gives access to the whole store
      store: useMainStore(),
    }
  },
})

Also I think there would be great advantages in picking up suggestions from #58 to use Typescripts Readonly utility type to indicate state shouldn't be modified from outside, as I tried to show here as well.

Love to hear your thoughts about this!

@posva
Copy link
Member

posva commented Jul 29, 2020

The overall direction was exposing everything at the same level. See #190

You won't be able to do const { state } = useMainStore() though

@rijkvanzanten
Copy link
Author

You won't be able to do const { state } = useMainStore() though

I don't think that's a problem, as it's the same with props and reactive objects.

Would that mean that鈥攅ven though it might be a bit of a weird use case鈥攜ou'd be able to do something like:

const { counter } = toRefs(useMainStore());

?

@posva
Copy link
Member

posva commented Jul 29, 2020

Yes, since to unwrap we would internally use reactive or shallowReactive

@TheJoeSchr
Copy link

Awesome, very promising!
@pr #190:

 // gives access only to the state
      state: computed(() => main.state),

Lovely 馃槏

I also think people will get used to toRefs for destructuring, since we have to do it often with props.

Does this mean, after this changes, there won't be a .value mismatch between state & getters? If yes, would adding another indirection like using js property getter help or doesn't it work?

@posva
Copy link
Member

posva commented Sep 22, 2020

Closing since #190 was merged

@JoeSchr That code you shared is precisely something that cannot be done anymore 馃槄 . It might be worth exposing some readonly $state if there is a usecase for it

@posva posva closed this as completed Sep 22, 2020
@TheJoeSchr
Copy link

@posva looks sweet, will have to check it out over the next few days!

@TheJoeSchr
Copy link

ah, but now you can change the state directly, without needing a action right? I feel like this is invalidating the use case for a store, namely the one-direction-flow of data. In my opinion if state can be changed directly, quickly there will be spaghetti code and a lot of coupling and major headaches when debugging. :(

@posva
Copy link
Member

posva commented Sep 22, 2020

See #58

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants