do not mutate vuex store state outside mutation handlers - vue.js

I am trying to overwrite an object inside an array called device inside my store.
the mutation saveState receives a device, if it doesn't exist in device array it would push the object , but if it is already existing it will just replace it with the received device.
I tried searching for a solution for almost a day and I can’t the problem with my code.
store.device.js
export const state = () => ({
device: []
})
export const mutations = {
saveState(state, device) {
var index = state.device.findIndex(dev => dev.id == device.id)
index === -1 ? state.device.push(device) : (state.device[index] = device)
}
}
export const getters = {
getStateById: state => id => {
return state.device.find(dev => dev.id === id)
}
}

The issue you are having is that Vue cannot detect state changes when you directly try to set an array index like you are doing with state.device[index] = device.
For this they provide Vue.set which allows you to update an array at a certain index. It is used like this:
//Vue.set(array, indexOfItem, newValue)
index === -1 ? state.device.push(device) : Vue.set(state.device, index, device);
You can read about it in the docs here

Related

Best way to have a default dynamic value derived from other atom [Recoil]

I am developing an app, which has sidebar menu. I have an atom, which saves the state of the /menu and an atom which saves the last selected menu key (as this key is used for other selectors too) -> for getting specific info for the current selected key.
export const menuItems = atom({
key: "menuItems",
default: ({ get }) => get(baseApi)("/menu"),
}); -> Returns Menu Items
And then I have an atom, which saves the selected menu item key:
export const selectedMenuKey = atom<string>({
key: "selectedMenuKey",
});
I cannot prefix the initial selected menu key as I don't know it in advance. I want the behavior to be following:
If the key is not set (when the app initially runs) set the selectedMenuKey value to be the first item of the menuItems atom value, otherwise be whatever is set last.
What would you say is the best way to achieve this?
I ran into this exact use case. Here is what I ended up doing.
In my 'activeTabState' file, equivalent to your 'selectedMenuKey':
import { atom, selector } from 'recoil';
import formMapState from './formMapState';
const activeTabState = atom({
key: 'activeTabAtom',
default: selector({
key: 'activeTabDefault',
get: ({ get }) => {
const formMap = get(formMapState);
if (!formMap) return null;
const [defaultTab] = Object.keys(formMap);
return defaultTab;
},
}),
});
export default activeTabState;
Then you can update the tab just like any other recoil state:
const FormNavigationTab = (props) => {
const { text, sectionName } = props;
const [activeTab, setActiveTab] = useRecoilState(activeTabState);
return (
<NavigationTab active={activeTab === sectionName} onClick={() => setActiveTab(sectionName)}>
{text}
</NavigationTab>
);
};
One thing to watch out for is that your activeTab value will be null until the menu items are loaded. So based on my use case, I needed to add a safeguard before using it:
const FormPage = () => {
const map = useRecoilValue(formMapState);
const activeTab = useRecoilValue(activeTabState);
// Starts out null if the map hasn't been set yet, since we don't know what the name of the first tab is
if (!activeTab) return null;
const { text, fields, sections } = map[activeTab];
// ... the rest of the component

Vuex Mutation is already set with the new payload before the function is run

setScorecardActiveVerificationsList(state, payload) {
console.log(
'beginState :>> ',
state.activeScorecardVerificationsList
)
const findIndex = state.activeScorecardVerificationsList.findIndex(
(el) => el.part_id === payload.part_id
)
console.log('findIndex', findIndex)
if (findIndex !== -1) {
const newArray = state.activeScorecardVerificationsList.filter(
(el) => el.part_id !== payload.part_id
)
newArray.push(payload)
state.activeScorecardVerificationsList = newArray
console.log(
'FoundstateactiveScorecardVerificationsList',
state.activeScorecardVerificationsList
)
} else {
state.activeScorecardVerificationsList = [
...state.activeScorecardVerificationsList,
payload,
]
console.log(
'NotFoundactiveScorecardVerificationsList',
state.activeScorecardVerificationsList
)
}
},
I'm still a bit new to Vuex but I'm having trouble with this mutation. This mutation format I've used on two other parts of the code that are working fine so this one is stumping me.
Btw, state.activeScorecardVerificationsList is an array of Objects.
I'm checking to see if the payload with part_id is already existing in the state.
IF it exists, I want to overwrite it and return the state with the newly updated object in the array.
IF this payload with the part_id does not exist, I just want to add it to the array.
Currently I'm having trouble with this part of the code:
console.log(
'beginState :>> ',
state.activeScorecardVerificationsList
)
const findIndex = state.activeScorecardVerificationsList.findIndex(
(el) => el.part_id === payload.part_id
)
For some reason, the beginState console log is showing the state with the payload.part_id (new one) almost immediately before any of the other lines of function is run. therefore, findIndex is returning 0 and it is always overwriting the state, not adding to it.
I'm not entirely sure how the state is already being sent with the new payload. I have a feeling my syntax or understanding of setting a vuex mutation might be off
Any ideas?

i18n won't translate correctly when inside array or object in React Native

I'm trying to use i18n-js to translate some strings into other languages. If I have my code in normal code, it works. Ex:
//Displays "Something" (no quotes) where I want it
<Text> translate("Something"); </Text>
But if I put it inside an array or object, then call it later, it stops working and shows a missing message instead of the text I want translated. Ex:
const messages = {
something: translate("Something"),
// other translations...
}
// later on
// Displays "[missing "en.Something" translation]" (no quotes) where I want it
<Text> messages.something </Text>
The following is my code for my translate function, as well as the config for i18n. I'm using lodash-memoize, but that is unrelated to the issue. I've already checked that the text being passed to i18n.t() is the same (including type) no matter if it's in normal code or in the array, but it still doesn't return the correct thing. I have some error checking written up to avoid getting the missing message on screen, but that still doesn't fix the issue that it can't find the translation.
export const translationGetters = ({
en: () => require('./translations/en.json'),
es: () => require('./translations/es.json')
});
export const translate = memoize(
(key, config) => {
text = i18n.t(key, config)
return text
},
(key, config) => (config ? key + JSON.stringify(config) : key)
);
export const setI18nConfig = () => {
// fallback if no available language fits
const fallback = { languageTag: "en", isRTL: false };
const { languageTag, isRTL } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback;
// clear translation cache
translate.cache.clear();
// update layout direction
I18nManager.forceRTL(isRTL);
// set i18n-js config
i18n.translations = { [languageTag]: translationGetters[languageTag]() };
i18n.locale = languageTag;
};
I have no idea where to go on this. Any advice would be appreciated!
Same problem here, workaround is to return array/object from inside a function:
Don't work
export const translations = [i18.t('path')]
Works
export function getTranslations() {
const translations = [i18.t('path')]
return translations
}

How to get the array of object(song) to store in a variable

I want the url from the data to pass in the music player to play the song.
I'm unable to setState url in the song[] state.
componentWillMount(){
var ur_i = this.props.navigation.getParam('uri');
console.log(JSON.stringify(ur_i));
this.setState(this.state.song=ur_i)
alert(JSON.stringify(this.state.song.url))
}
I want song Url to be store in the song[].
Invalid setState usage method.
setState() operates asynchronously.
setState(updater[, callback])
The callback function of setState(updater[, callback]) is performed and is re-renderable.
Example
timerAction = () => {
const { time } = this.state;
console.log(time);
this.setState({
time: time - 1
})
console.log(time)
};
Usage
componentDidMount(){
var ur_i = this.props.navigation.getParam('uri');
console.log(JSON.stringify(ur_i));
this.setState(song:ur_i[0].song,() => alert(JSON.stringify(ur_i[0].song)))
}

Lists and Components not updating after data change - (VueJS + VueX)

A question about best practice (or even a go-to practice)
I have a list (ex. To-do list). My actual approach is:
On my parent component, I populate my 'store.todos' array. Using a
getter, I get all the To-do's and iterate on a list using a v-for
loop.
Every item is a Component, and I send the to-do item as a prop.
Inside this component, I have logic to update the "done" flag. And this element display a checkbox based on the "state" of the flag. When it does that, it do an action to the db and updates the store state.
Should I instead:
Have each list-item to have a getter, and only send the ID down the child-component?
Everything works fine, but if I add a new item to the to-do list, this item is not updated when I mark it as completed. I wonder if this issue is because I use a prop and not a getter inside the child component
Code:
store:
const state = {
tasks: []
}
const mutations = {
CLEAR_TASKS (state) {
state.tasks = [];
},
SET_TASKS (state, tasks) {
state.tasks = tasks;
},
ADD_TASK (state, payload) {
// if the payload has an index, it replaces that object, if not, pushes a new task to the array
if(payload.index){
state.currentSpaceTasks[payload.index] = payload.task;
// (1) Without this two lines, the item doesn't update
state.tasks.push('');
state.tasks.pop();
}
else{
state.tasks.push(payload.task);
}
},
SET_TASK_COMPLETION (state, task){
let index = state.tasks.findIndex(obj => obj.id == task.id);
state.tasks[index].completed_at = task.completed_at;
}
}
const getters = {
(...)
getTasks: (state) => (parentId) => {
if (parentId) {
return state.tasks.filter(task => task.parent_id == parentId );
} else {
return state.tasks.filter(task => !task.parent_id );
}
}
(...)
}
const actions = {
(...)
/*
* Add a new Task
* 1st commit add a Temp Task, second updates the first one with real information (Optimistic UI - or a wannabe version of it)
*/
addTask({ commit, state }, task ) {
commit('ADD_TASK',{
task
});
let iNewTask = state.currentSpaceTasks.length - 1;
axios.post('/spaces/'+state.route.params.spaceId+'/tasks',task).then(
response => {
let newTask = response.data;
commit('ADD_TASK',{
task: newTask,
index: iNewTask
});
},
error => {
alert(error.response.data);
});
},
markTaskCompleted({ commit, dispatch, state }, task ){
console.log(task.completed_at);
commit('SET_TASK_COMPLETION', task);
dispatch('updateTask', { id: task.id, field: 'completed', value: task.completed_at } ).then(
response => {
commit('SET_TASK_COMPLETION', response.data);
},
error => {
task.completed_at = !task.completed_at;
commit('SET_TASK_COMPLETION', task);
});
},
updateTask({ commit, state }, data ) {
return new Promise((resolve, reject) => {
axios.patch('/spaces/'+state.route.params.spaceId+'/tasks/'+ data.id, data).then(
response => {
resolve(response.data);
},
error => {
reject(error);
});
})
}
}
And basically this is my Parent and Child Components:
Task List component (it loads the tasks from the Getters)
(...)
<task :task = 'item' v-for = "(item, index) in tasks(parentId)" :key = 'item.id"></task>
(...)
The task component display a "checkbox"(using Fontawesome). And changes between checked/unchecked depending on the completed_at being set/true.
This procedure works fine:
Access Task list
Mark one existing item as done - checkbox is checked
This procedure fails
Add a new task (It fires the add task, which firstly adds a 'temporary' item, and after the return of the ajax, updates it with real information (id, etc..). While it doesn't have the id, the task displays a loading instead of the checkbox, and after it updates it shows the checkbox - this works!
Check the newly added task - it does send the request, it updates the item and DB. But checkbox is not updated :(
After digging between Vue.js docs I could fix it.
Vue.js and Vuex does not extend reactivity to properties that were not on the original object.
To add new items in an array for example, you have to do this:
// Vue.set
Vue.set(example1.items, indexOfItem, newValue)
More info here:
https://v2.vuejs.org/v2/guide/reactivity.html
and here: https://v2.vuejs.org/v2/guide/list.html#Caveats
At first it only solved part of the issue. I do not need the "hack" used after pushing an item into the array (push and pop an empty object to force the list to reload)
But having this in mind now, I checked the object returned by the server, and although on the getTasks, the list has all the fields, including the completed_at, after saving a new item, it was only returning the fields that were set (completed_at is null when created). That means that Vue.js was not tracking this property.
I added the property to be returned by the server side (Laravel, btw), and now everything works fine!
If anybody has a point about my code other than this, feel free to add :)
Thanks guys