Pinia: How to dynamically add new enteries to store state - vuejs3

I have a use case with the pinia in vue 3 that I want to dynamically add new entries to the pinia store using the store actions. for example if I have a state called firstName and if I call a the action of the store it should add new state called lastName in the state as well. Here is what I have tried
import { defineStore } from "pinia";
export const useAdvanceListingsFilterStore = defineStore(
"advance-listing-filters",
{
state: () => {
return {
firstName: "jhon",
};
},
actions: {
setLastName(payload) {
return {
...this.state,
lastName: payload,
};
},
},
}
);
The new state should include the fistName and lastName fields.

The simplest way would by add lastName: null to your state, but I guess it is not what you are trying to achieve.
I have tried to add new items to the state internally using state.$path (see Mutating the state), but the new lastName item was still not accessible outside the state using state.lastName. So I haven't found a way to achieve your goal directly. But there is another way to do it.
You can use a nested object to achieve your goal.
See the playground below.
I should also state, that adding state items dynamically makes your application less predictable.
So, I would rather rethink the design and flow of data.
const { ref, createApp, defineComponent } = Vue
const { createPinia, defineStore, storeToRefs } = Pinia
const useAlertsStore = defineStore("advance-listing-filters",
{
state: () => {
return {
user: { firstName: "John" }
};
},
actions: {
setLastName(payload) {
this.$state.user.lastName = payload;
}
}
}
)
const App = {
setup() {
const store = useAlertsStore()
const { user } = storeToRefs(store)
const lastName = ref('Doe')
return {
store,
user,
lastName
}
}
}
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
<div id="app">
<label>Name: </label> <b>{{store.user.firstName}} {{store.user.lastName}}</b><br /><br />
<label>First Name:</label> <input :value="user.firstName" disabled /><br /><br />
<label>Last Name:</label> <input v-model="lastName" /><br /><br />
<button #click="store.setLastName(lastName)">Set Last Name</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi#0.13.11/lib/index.iife.js"></script>
<script src="https://unpkg.com/pinia#2.0.30/dist/pinia.iife.js"></script>

Related

How to make a pinia work with nested objects in vue3

How can I get a reactive component that updates nested properties:
I have a pinia store defined as follows
import { defineStore } from "pinia"
export const useStore = defineStore({
id: "poc",
state: () => ({ str: "", nested: { obj: "" } }),
persist: {
enabled: true,
strategies: [{ storage: localStorage }],
},
})
and the following vue3 component
<script lang="ts">
import { ref } from "vue"
import { storeToRefs } from "pinia"
import { useStore } from "./store"
export default {
setup() {
const store = useStore()
const example = storeToRefs(store)
const mStr = ref(example.str)
const mObj = ref(example.nested.value.obj) // <--- this is where I believe the problem is
store.str = mStr.value
store.nested.obj = mObj.value
return { mObj, mStr, store }
},
}
</script>
<template>
<h1>PoC</h1>
<input v-model="mObj" placeholder="obj" />
<input v-model="mStr" placeholder="str" />
</template>
when I update the str field it works as expected, but for nested object it doesn't. My suspicion is that I lose reactivity when calling nested.value, that said - I don't know how to make it reactive.
a little bit more digging and https://github.com/vuejs/pinia/discussions/854 finally gave me enough to come up with a (much more elegant) solution on my own.
<script lang="ts">
import { useStore } from "./store"
export default {
setup() {
const store = useStore()
return { store }
},
}
</script>
<template>
<h1>test</h1>
<input v-model="store.str" placeholder="obj" />
<input v-model="store.nested.obj" placeholder="str" />
</template>
FOR PINIA: destructuring the state checkout :storeToRefs()
In order to extract properties from the store while keeping its reactivity, you need to use storeToRefs(). It will create refs for every reactive property. This is useful when you are only using state from the store but not calling any action. Note you can destructure actions directly from the store as they are bound to the store itself too
<script>
import { useStore } from "./store"
import { storeToRefs } from 'pinia' // NOTE this
export default {
setup() {
const store = useStore()
const {str, nested } = storeToRefs(store)
return { str, nested }
},
}
</script>
<template>
<h1>test</h1>
<input v-model="str" placeholder="obj" />
<input v-model="nested.obj" placeholder="str" />
</template>

How to populate FormKit input fields with dynamic data fetched from a database

I'm making a fullstack app with vue3, axios using FormKit. For editing existing records I want to populate the input fields with the current data fetched from a mysql database. I stripped down the code to everything needed to display my problem, which in this code example is populating the FormKit input field with the lotnumber I fetched via the asynchronous function "getLotById". The lotnumber appears in the paragraph section but not in the input field. How can I properly delay the rendering of the FormKit element until the lotnumber has been fetched? Here's my code:
<script>
// import axios
import axios from "axios";
export default {
name: "LotEdit",
data() {
return {
lotnumber: this.lotnumber
}
},
props: {
lotid: Number
},
created: async function () {
await this.getLotById();
},
methods: {
// Get Lot By Id
async getLotById() {
try {
const response = await axios.get(`http://localhost:5000/lot/${this.$route.params.id}`);
this.lotnumber = response.data.lotnumber;
console.log(response.data);
}
catch (err) {
console.log(err);
}
},
}
};
</script>
<template>
<div>
<FormKit
type="text"
name="lotnumber"
label="lotnumber"
placeholder=""
validation="required"
:value="lotnumber"
/>
</div>
<div>
<p> Here the lotnumber appears: {{ lotnumber }}</p>
</div>
</template>
I suggest using a v-model on the FormKit input. Because it is two-way bound it means as soon as the async/await completes the data is populated on the template too. Something like...
<FormKit
v-model="lotnumber"
type="text"
name="lotnumber"
label="lotnumber"
placeholder=""
validation="required"
:value="lotnumber"
/>
Getting a little smarter I managed to solve the problem in the following way:
<script>
// import axios
import axios from "axios";
export default {
name: "LotEdit",
data() {
return {
lotnumber: this.lotnumber
}
},
props: {
lotid: Number
},
mounted: async function () {
const response = await this.getLotById();
const node = this.$formkit.get('lotnumber')
node.input(response.data.lotnumber, false)
},
methods: {
// Get Lot By Id
async getLotById() {
try {
const response = await axios.get(`http://localhost:5000/lot/${this.$route.params.id}`);
console.log(response.data);
return response;
}
catch (err) {
console.log(err);
}
},
}
};
</script>
<template>
<div>
<FormKit
type="text"
id="lotnumber"
name="lotnumber"
label="lotnumber"
placeholder=""
validation="required"
:value="lotnumber"
/>{{ lotnumber }}
</div>
</template>
Feel free to post any recommendations as I'm not a pro yet...
I'm also still figuring out how to handle controlled forms but I guess an alternative way to do it is with Form Generation
<script>
export default {
// ...
async setup() {
try {
const response = await axios.get(`http://localhost:5000/lot/${this.$route.params.id}`);
const schema = [
{
$formkit: "text",
label: "Lot Number",
value: response.data.lotnumber,
validation: "required",
},
];
} catch (err) {
console.log(err);
}
return { schema }
}
// ...
}
</script>
<template>
<FormKit type="form">
<FormKitSchema :schema="schema" />
</FormKit>
</template>

Updating user profile information with redux in firebase

I am trying to use Redux in my React application to update the user profile within my Firebase database from my react component.
This is my component:
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { firestoreConnect } from "react-redux-firebase";
import { compose } from "redux";
import { editProfile } from "../../store/actions/editProfileActions";
class UserProfile extends Component {
state = {
firstName:"",
initials:"",
lastName:""
};
onChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
onSubmit = e => {
e.preventDefault();
console.log(this.state);
this.props.editProfile(this.state);
}
render() {
const { auth, profile } = this.props;
console.log(profile);
if (auth.isEmpty) return <Redirect to="/home" />;
return (
<div className="container">
<form onSubmit={this.onSubmit} className="white">
<h5 className="grey-text text-darken-3">Edit Profile</h5>
<div className="input-field">
<label htmlFor="title">First Name: {profile.firstName}</label>
<input type="text" id="firstName" onChange={this.onChange} />
</div>
<div className="input-field">
<label htmlFor="title">Initials: {profile.initials}</label>
<input type="text" id="initials" onChange={this.onChange} />
</div>
<div className="input-field">
<label htmlFor="title">Last Name: {profile.lastName}</label>
<input type="text" id="lastName" onChange={this.onChange} />
</div>
<div className="input-field">
<button className="btn black z-depth-0">Submit</button>
{ }
</div>
</form>
</div>
)
}
};
const mapStateToProps = state => {
return {
auth: state.firebase.auth,
profile: state.firebase.profile,
};
};
const mapDispatchToProps = dispatch => {
return {
editProfile: edit => dispatch(editProfile(edit))}
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect([
{ collection: "profile"}
])
)(UserProfile);
The component correctly displays the current user information.
This is the action I have set up:
return async (dispatch, getState, { getFirestore, getFirebase }) => {
const firebase = getFirebase();
const user = await firebase
.auth()
.currentUser
.updateProfile({
firstName: profile.firstName
});
dispatch({ type: "EDITPROFILE_SUCCESS", user })
console.log("user = " + profile.firstName);
};
}
When I log the entered profile.firstName I get the entered data.
And my reducer:
const editProfileReducer = (state, action) => {
switch (action.type) {
case "EDITPROFILE_ERROR":
return {
...state,
editError: action.error
};
case "EDITPROFILE_SUCCESS":
return {
...state
};
default:
return state;
}
}
export default editProfileReducer;
Any idea what I am missing here?
In your reducer change the like below
case "EDITPROFILE_SUCCESS":
return {
...state,
user:action.user
};
Above is if you want to update the whole user object
If you want to change only name then
Let’s assume that profileName is in user object then
case "EDITPROFILE_SUCCESS":
return {
...state,
user:Object.assign({}, state.user, profileName:action.user.profileName)
};

Passing form variables to action creator in React-Redux

In a React-Redux app, I'm trying to pass the form variable from a component to the action creator without using Redux-Form, Formic or any other extension.
myForm.js
import { connect } from "react-redux";
import { fetchData } from "../actions/myActions";
class myForm extends Component {
constructor(props) {
super(props);
this.state = {
from: "",
to: ""
};
this.onFormSubmit = this.onFormSubmit.bind(this);
}
onFormSubmit(event) {
event.preventDefault();
const from = event.target.elements.from.value;
const to = event.target.elements.to.value;
this.setState({
from: from,
to: to
});
this.props.fetchData(
this.state.from,
this.state.to,
);
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<div>
<input
type="text"
name="from"
/>
</div>
<div>
<input
type="text"
name="to"
/>
</div>
</form>
</div>
export default connect(
null,
{ fetchData }
)(myForm);
I'm passing the action creator fetchData to myForm component and on form submit invoke the onFormSubmit function, which passes the form variables to fetchData like this:
this.props.fetchData(
this.state.from,
this.state.to,
);
Then inside myActions.js I try to access those form variables and start an API request.
myActions.js
import { FETCH_DATA } from "./types";
import axios from "axios";
const APP_KEY = <my api key>;
export const fetchData = (from,to) => async dispatch => {
const response = await axios.get`${URL}/${from}/to/${to}?app_key=${APP_KEY}`;
dispatch({
type: FETCH_DATA,
payload: response.data.journeys
});
};
Is what I'm trying the right approach?
Unfortunately it seems that variables from and to don't get passed to the action creator inside myAction.js.
setState is an async operation, hence this.props.fetchData is called, before the state is even set. You need to use the callback in the second argument of setStatein myForm.js which is excuted after the state has been updated.
this.setState({
from: from,
to: to
}, () => {
this.props.fetchData(this.state.from,this.state.to)
});
Hope this helps. Happy coding !

React props using Meteor Apollo

I am playing with the Meteor Apollo demo repo.
I am having difficulty passing variables down to children with React. I am getting an error
imports/ui/Container.jsx:10:6: Unexpected token (10:6)
The below code is the Container.jsx component:
import React from 'react';
import { Accounts } from 'meteor/std:accounts-ui';
class Container extends React.Component {
render() {
let userId = this.props.userId;
let currentUser = this.props.currentUser;
}
return (
<Accounts.ui.LoginForm />
{ userId ? (
<div>
<pre>{JSON.stringify(currentUser, null, 2)}</pre>
<button onClick={() => currentUser.refetch()}>Refetch!</button>
</div>
) : 'Please log in!' }
);
}
}
It is passed props via the Meteor Apollo data system (I have omitted some imports at the top):
const App = ({ userId, currentUser }) => {
return (
<div>
<Sidebar />
<Header />
<Container userId={userId} currentUser={currentUser} />
</div>
)
}
// This container brings in Apollo GraphQL data
const AppWithData = connect({
mapQueriesToProps({ ownProps }) {
if (ownProps.userId) {
return {
currentUser: {
query: `
query getUserData ($id: String!) {
user(id: $id) {
emails {
address
verified
}
randomString
}
}
`,
variables: {
id: ownProps.userId,
},
},
};
}
},
})(App);
// This container brings in Tracker-enabled Meteor data
const AppWithUserId = createContainer(() => {
return {
userId: Meteor.userId(),
};
}, AppWithData);
export default AppWithUserId;
I would really appreciate some pointers.
I believe the error is that you accidentally ended the render function before the return statement.
render() { // <- here it starts
let userId = this.props.userId;
let currentUser = this.props.currentUser;
} // <- here it ends
Another error is that your return statement doesn't return a single DOM element, but two of them: an Accounts.ui.LoginForm and a div. The return function should only return one element. Just put the entire thing into a single <div>.

Resources