Vue3 | Pinia - Watching storeToRefs in composable function does not work - javascript

I'm trying to understand the purpose of composables. I have a simple composable like this and was trying to watch state from a Pinia store where the watch does not trigger:
import { ref, watch, computed } from "vue";
import { storeToRefs } from "pinia";
import useFlightsStore from "src/pinia/flights.js";
import usePassengersStore from "src/pinia/passengers.js";
export function useFlight() {
const route = useRoute();
const modalStore = useModalStore();
const flightsStore = useFlightsStore();
const { selection } = storeToRefs(flightsStore);
const passengersStore = usePassengersStore();
const { passengers, adults, children, infants } =
storeToRefs(passengersStore);
watch([adults, children, infants], (val) => console.log('value changes', val))
Where as the same thing in a Vue component works as expected.
So we cannot watch values inside composables?

I think you can watch values inside composables.
But, to watch a pinia state it has to be inside an arrow function:
watch(() => somePiniaState, (n) => console.log(n, " value changed"));
It's like watching a reactive object.
I believe this should be documented better. In Pinia documentation we can read how to watch the whole store or how to subscribe to a store but not how to watch a single state property inside a component or composable.
Also, the docs are somewhat shy in explaining that you can watch a property inside a store using setup() way of describing a store.
More on this here:
https://github.com/vuejs/pinia/discussions/794#discussioncomment-1643242
This error also silently fails (or does not execute), which is not helpful...

I needed to watch a specific state attribute in one of my components but I didn't find my use case on the official documentation.
I used a mix between a watch and storeToRefs to do it.
import { usePlaylistsStore } from '#/stores/playlists'
import { storeToRefs } from 'pinia'
export default {
name: 'PlaylistDetail',
setup() {
const playlistsStore = usePlaylistsStore()
const { selectedGenres } = storeToRefs(playlistsStore)
return { selectedGenres }
},
watch: {
selectedGenres(newValue, oldValue) {
// do something
}
}
}

Related

SolidJS: Updating component based on value served by Context's store?

In React all props are updated and propagated to children automatically which is nice but it slows down and requires lots of optimization at some point.
So I'm building an app with SolidJS using Context + createStore patterng and I'm having problems with consuming that state.
I'd like to create AppProvider component that manages State props and Dispatch functions. The Provider will be performing all operations on appStore, implement functions and serve them all via AppContextState and AppContextDispatch providers.
Then I need to consume that data to update components that are dependent on it reactively.
Look at the code below:
/// index.tsx
import { render } from 'solid-js/web';
import { AppProvider } from '#/providers/AppProvider';
import App from './App';
render(() => (
<AppProvider>
<App />
</AppProvider>
), document.getElementById('root') as HTMLElement);
/// AppProvider.tsx
import { createContext, useContext, JSX } from 'solid-js';
import { createStore } from 'solid-js/store';
// Interfaces
interface IAppState {
isConnected: boolean;
user: { name: string; }
}
interface IAppDispatch {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
}
// Initialize
const initialState = {
isConnected: false,
user: { name: '' }
}
const initialDispatch = {
connect: () => {},
disconnect: () => {}
}
// Contexts
const AppContextState = createContext<IAppState>();
const AppContextDispatch = createContext<IAppDispatch>();
export const useAppState = () => useContext(AppContextState);
export const useAppDispatch = () => useContext(AppContextDispatch);
// Provider
export const AppProvider = (props: { children: JSX.Element }) => {
const [appStore, setAppStore] = createStore<IAppState>(initialState);
async function connect() {
setAppStore("isConnected", true);
setAppStore("user", "name", 'Chad');
}
async function disconnect() {
setAppStore("isConnected", false);
setAppStore("user", "name", '');
}
return (
<AppContextState.Provider value={appStore}>
<AppContextDispatch.Provider value={{ connect, disconnect }}>
{props.children}
</AppContextDispatch.Provider>
</AppContextState.Provider>
)
}
/// App.tsx
import { useAppState, useAppDispatch } from '#/providers/AppProvider';
export default function App() {
const { user, isConnected } = useAppState();
const { connect, disconnect } = useAppDispatch();
return (
<Show when={isConnected} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
)
}
This component will show a button that should run the connect function and update isConnected state and make the component within <Show> block visible but it doesn't do anything.
I verified that state is being updated by logging data of appStore in connect method.
When I change the component to depend on user.name instead isConnected it works
<Show when={user.name} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
However my app has many components depending on various data types, including boolean that for some doesn't work in this example with SolidJS.
I'd like to know what am I doing wrong here and understand what is the best way to share state between components. I keep reading documentation and fiddling with it but this particular problem bothers me for a past few days.
Plain Values in Solid cannot be tracked
The problem here is that primitive values / variables cannot be reactive in solid. We have two ways of tracking value access: Through function calls, and through property getters/proxies (which use signals under the hood).
So, what happens when you access a store property?
const state = useAppState();
createEffect(() => {
console.log(state.isConnected)
})
In this case, the property access is occurring within the effect, so it gets tracked, and reruns when the property value updates. On the other hand, with this:
const { isConnected } = useAppState();
We are accessing the property at the top level of the component (which is untracked and not reactive in solid). So even though we use this value in a context that is reactive (like the when prop in `), we can't run any special under-the-hood tracking to set up updates.
So why did user.name work?
The reason is that stores are deeply reactive (for primitives, objects and arrays), so
const { user } = useAppState();
Means that you are eagerly accessing the user object (so if the user property changes, you won't get updated), but the properties of the user object were not accessed yet, they only get accessed further on, in <Show when={user.name}>, so the property access user.name is able to be tracked.

Make shared property reactive in Vue Composition API composable by declaring variable outside of exported function

I am using the composition api plugin for vue2 (https://github.com/vuejs/composition-api) to reuse composables in my app.
I have two components that reuse my modalTrigger.js composable, where I'd like to declare some sort of shared state (instead of using a bloated vuex state management).
So in my components I do something like:
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'SearchButton',
setup(props, context) {
const { getModalOpenState, setModalOpenState } = modalTrigger();
return {
getModalOpenState,
setModalOpenState,
};
},
};
And in my modalTrigger I have code like:
import { computed, ref, onMounted } from '#vue/composition-api';
let modalOpen = false; // needs to be outside to be accessed from multiple components
export default function () {
modalOpen = ref(false);
const getModalOpenState = computed(() => modalOpen.value);
const setModalOpenState = (state) => {
console.log('changing state from: ', modalOpen.value, ' to: ', state);
modalOpen.value = state;
};
onMounted(() => {
console.log('init trigger');
});
return {
getModalOpenState,
setModalOpenState,
};
}
This works, but only because I declare the modalOpen variable outside of the function.
If I use this:
export default function () {
const modalOpen = ref(false); // <------
const getModalOpenState = computed(() => modalOpen.value);
...
It is not reactive because the modalTrigger is instantiated twice, both with it's own reactive property.
I don't know if that is really the way to go, it seems, that I am doing something wrong.
I also tried declaring the ref outside:
const modalOpen = ref(false);
export default function () {
const getModalOpenState = computed(() => modalOpen.value);
But this would throw an error:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function.
So what would be the correct way to achieve this?
I somehow expected Vue to be aware of the existing modalTrigger instance and handling duplicate variable creation itself...
Well, anyway, thanks a lot in advance for any hints and tipps.
Cheers
Edit:
The complete header.vue file:
<template>
<header ref="rootElement" :class="rootClasses">
<button #click="setModalOpenState(true)">SET TRUE</button>
<slot />
</header>
</template>
<script>
import { onMounted, computed } from '#vue/composition-api';
import subNavigation from '../../../../composables/subNavigation';
import mobileNavigation from '../../../../composables/mobileNavigation';
import search from '../../../../composables/searchButton';
import { stickyNavigation } from '../../../../composables/stickyNav';
import metaNavigation from '../../../../composables/metaNavigation';
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'Header',
setup(props, context) {
const { rootElement, rootClasses } = stickyNavigation(props, context);
mobileNavigation();
subNavigation();
search();
metaNavigation();
const { getModalOpenState, setModalOpenState } = modalTrigger();
onMounted(() => {
console.log('Header: getModalOpenState: ', getModalOpenState.value);
setModalOpenState(true);
console.log('Header: getModalOpenStat: ', getModalOpenState.value);
});
return {
rootClasses,
rootElement,
getModalOpenState,
setModalOpenState,
};
},
};
</script>
The composition API is setup somewhere else where there are Vue components mounted a bit differently than you normally would.
So I can't really share the whole code,but it has this inside:
import Vue from 'vue';
import CompositionApi from '#vue/composition-api';
Vue.use(CompositionApi)
The composition API and every other composable works just fine...

How to access nuxt `$config` in Vuex state? Only access method is through store actions methods?

I have used to dotenv library to use .env file, but I have to change runtimeConfig because I realized it was easy to expose my project secret key.
In my latest project, I have used nuxt "^2.14" and mode is SPA.
So I only use "publicRuntimeConfig" in nuxt.config.ts like that.
.env
Test_BASE_URL:'https://test.org'
nuxt.config.ts
export default {
publicRuntimeConfig:{baseURL: proccess.env.Test_BASE_URL||''}
}
I can use env like that in vue file.
sample.vue
<script>
export default {
mounted(){
console.log(this.$config.baseURL)
}
}
</script>
But I couldn't use "$config" in store's state.
I tried to write that but it always return "undefied"
index.ts
export const state = (context) => ({
url:context.$config
})
I have referred the this guys solutions
and changed state's value through the actions method.
I have used SPA, so I made method like 'nuxtServerInit'as plugins.
plugins/clientInit.ts
import {Context} from "#nuxt/types";
export default function (context:Context) {
context.store.dispatch('initEnvURL',context.$config)
}
index.ts
interface State {
testURL: string
}
const state = () => ({
testURL:''
})
const mutations = {
setTestURl(state:State,config:any) {
state.testURL = config.baseURL
}
const actions = {
initEnvURL({commit},$config) {
commit('setTestURl',$config)
}
}
export default {state,mutations,actions}
I success to change state value through actions methods above,
but I don't know why "context" can't use store/state objects directly.
Does anyone know how to use $config in store/state?
or is it impossible only way to use $config through actions method like above?
That's because in Vuex, only actions actually receive the app context.
State, Mutations and Getters can't access it by design.
Your initial state should be contextless, i.e. with values that doesn't depend on the runtime execution.
Mutations are stateless, they just take a parameter and update the state. That's all. Contextful parameters should be coming from the caller.
Getters are just reactive state transformations, and should not rely on context properties, that would be messing with the Vuex module state.
So yes, what you have to do it initialise your store within the nuxtServerInit actions (or from a plugin for SPA apps):
nuxtServerInit({ store, config } ) {
store.commit('UPDATE_BASE_URL', config.baseUrl)
}
It does NOT show up through the type system even when using #nuxt/types.
Access it like this in store/index.ts or store/module.ts:
import { ActionTree, MutationTree } from 'vuex'
const actions: ActionTree<ModuleState, RootState> = {
async yourActionName({ commit }, payload): Promise<void> {
try {
let url = this.app.$config.baseURL + "/path"; // <- config is accessed here.
const res = await this.$axios.get<number>(url);
commit("mutateState", res.data);
return;
} catch (error) {
// Error handling
}
},
};
My nuxt.config.js looks like:
export default {
...
publicRuntimeConfig: {
baseURL: process.env.BASE_URL || 'http://localhost:5000/api',
}
...
};

React - Apollo Client, How to add query results to the state

I've created a react app driven by Apollo client and graphQL.
My schema is defined so the expected result is an array of objects ([{name:"metric 1", type:"type A"},{name:"metric 2", type:"type B"}])
On my jsx file I have the following query defined:
query metrics($id: String!) {
metrics(id: $id) {
type
name
}
}`;
I've wrapped the component with Apollo HOC like so:
export default graphql(metricsQuery, {
options: (ownProps) => {
return {
variables: {id: ownProps.id}
}
}
})(MetricsComp);
The Apollo client works fine and returns the expected list on the props in the render method.
I want to let the user manipulate the results on the client (edit / remove a metric from the list, no mutation to the actual data on the server is needed). However since the results are on the component props, I have to move them to the state in order to be able to mutate. How can I move the results to the state without causing an infinite loop?
If apollo works anything like relay in this matter, you could try using componentWillReceiveProps:
class ... extends Component {
componentWillReceiveProps({ metrics }) {
if(metrics) {
this.setState({
metrics,
})
}
}
}
something like this.
componentWillReceiveProps will be deprecated soon (reference link)
If you are using React 16 then you can do this:
class DemoClass extends Component {
state = {
demoState: null // This is the state value which is dependent on props
}
render() {
...
}
}
DemoClass.propTypes = {
demoProp: PropTypes.any.isRequired, // This prop will be set as state of the component (demoState)
}
DemoClass.getDerivedStateFromProps = (props, state) => {
if (state.demoState === null && props.demoProp) {
return {
demoState: props.demoProp,
}
}
return null;
}
You can learn more about this by reading these: link1, link2
you can use this:
import {useState} from 'react';
import {useQuery} from '#apollo/client';
const [metrics,setMetrics]=useState();
useQuery(metricsQuery,{
variables:{id: ownProps.id},
onCompleted({metrics}){
setMetrics(metrics);
}
});

How to update state using Redux?

I am using this starter kit https://github.com/davezuko/react-redux-starter-kit and am following some tutorials at the same time, but the style of this codebase is slightly more advanced/different than the tutorials I am watching. I am just a little lost with one thing.
HomeView.js - This is just a view that is used in the router, there are higher level components like Root elsewhere I don't think I need to share that, if I do let me know, but it's all in the github link provided above.
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { searchListing } from '../../redux/modules/search'
export class HomeView extends React.Component {
componentDidMount () {
console.log(this.props)
}
render () {
return (
<main onClick={this.props.searchListing}>
<NavBar search={this.props.search} />
<Hero/>
<FilterBar/>
<Listings/>
<Footer/>
</main>
)
}
}
I am using connect() and passing in mapStateToProps to tell the HomeView component about the state. I am also telling it about my searchListing function that is an action which returns a type and payload.
export const searchListing = (value) => {
console.log(value)
return {
type: SEARCH_LISTINGS,
payload: value
}
}
Obviously when I call the method inside the connect() I am passing in an empty object searchListing: () => searchListing({})
const mapStateToProps = (state) => {
return {
search: { city: state.search }
}
}
export default connect((mapStateToProps), { searchListing: () => searchListing({}) })(HomeView)
This is where I am stuck, I am trying to take the pattern from the repo, which they just pass 1, I think anytime that action is created the logic is just add 1 there is no new information passed from the component.
What I am trying to accomplish is input search into a form and from the component pass the users query into the action payload, then the reducer, then update the new state with the query. I hope that is the right idea.
So if in the example the value of 1 is hardcoded and passed into the connect() method, how can I make it so that I am updating value from the component dynamically? Is this even the right thinking?
You almost got it right. Just modify the connect function to pass the action you want to call directly:
const mapStateToProps = (state) => ({
search: { city: state.search }
});
export default connect((mapStateToProps), {
searchListing
})(HomeView);
Then you may use this action with this.props.searchListing(stringToSearch) where stringToSearch is a variable containing the input value.
Notice : You don't seem to currently retrieve the user query. You may need to retrieve it first and then pass it to the searchListing action.
If you need to call a function method, use dispatch.
import { searchListing } from '../../redux/modules/search';
const mapDispatchToProps = (dispatch) => ({
searchListing: () => {
dispatch(searchListing());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeView);
Then, you have made the function a prop, use it with searchListing.

Categories

Resources