ref no longer reactive within a reactive array object - javascript

The following code works fine. When we change the language the text gets updated correctly thanks to the ref:
const mainNavigationLinks = computed(() => [
{ label: context.root.$t('navigationMenu.home') },
{ label: context.root.$t('navigationMenu.tickets') },
])
return {
mainNavigationLinks,
}
But what we would really like is to have the mainNavigationLinks as a reactive object, so we can add and remove items from the array and have Vue update the components accordingly with the correct translations by using the ref within the array. According to the Vue docs this should be possible
However, when we try the code below we see that the label is no longer reactive:
const mainNavigation = reactive([
{ label: context.root.$t('navigationMenu.home') },
{ label: context.root.$t('navigationMenu.tickets') },
])
const mainNavigationLinks = computed(() => mainNavigation)
What are we missing here to be able to add items to the array and still have it reactive? Thank you for your help.

It seems like what you want is to have the array be reactive, but the items within the array be computed properties.
const mainNavigation = reactive([
{ label: computed(() => context.root.$t('navigationMenu.home')) },
{ label: computed(() => context.root.$t('navigationMenu.tickets')) },
])
Alternatively, you might be able to get away with not using computed at all here, as long as each label property refers to a function that you have to call:
const mainNavigation = reactive([
{ label: () => context.root.$t('navigationMenu.home') },
{ label: () => context.root.$t('navigationMenu.tickets') },
])
That's the main idea: each label needs to be evaluated at the time it is used, which is why it must be either a computed property (you benefit from caching) or a function. Your code doesn't work because you are getting the label translations immediately when you constructed the array.

Related

Is it possible to call a function in an object that references itself?

I have an object that looks something like this:
const state = []
const spells = [
{
name: 'test',
channelDuration: 1000,
castDuration: 1000,
cast: () => {
state.push(THIS) // <--
}
}, {...}, {...}
]
When a player uses a spell, that spell gets stored into the game state and then waits for the channelDuration and castDuration to pass.
Once able to cast, it looks into the state and calls the cast method:
State.quest.bossFightState[USER].spell.cast()
state = [Spell]
On said cast, i need to push the spell into the a new array called statuses. However, i can't reference the object within the cast method without using some additional helper functions to grab the specific spell. And I dont want to pass add a parameter spell into the cast method because that just seems dumb.
Any ideas? Or is a helper method the way to go here.
I'm surprised nobody else caught this... You're using this inside of an arrow function. The this keyword acts differently in arrow functions - read more about that here
const state = [];
const spells = [
{
name: 'test',
cast: function () {
state.push(this);
},
},
];
spells[0].cast();
console.log(state);
One option would be to use a function or method instead of an arrow function, so that .cast will have a this referring to the outer object.
const state = []
const spells = [
{
name: 'test',
cast() {
state.push(this)
}
}
]
spells[0].cast();
console.log(state);

re-populate a directive array reactively in Vue.js

I have an array defined in my data() which gets populated through a custom directive in its bind hook as below:
import Vue from 'vue'
export default {
el: '#showingFilters',
name: "Filters",
data() {
return {
country: '' // v-modelled to a <select>
states: [],
}
},
directives: {
arraysetter: {
bind: function(el, binding, vnode) {
vnode.context[binding.arg] = Object.keys(el.options).map(op => el.options[op].value);
},
},
},
methods: {
countryChangeHandler() {
this.states.splice(0)
fetch(`/scripts/statejson.php?country=${this.country}`)
.then(response => response.json())
.then(res => {
res.states.forEach( (element,i) => {
Vue.set(this.states, i, element.urlid)
});
})
},
}
The problem starts when I want to re-populate the states array in the countryChangeHandler() method (when #change happens for the country select tag).
I used splice(0) to make the array empty first and I have then used Vue.set to make the re-population reactive, but Vue still doesn't know about it!!! The array has the correct elements though! I just don't know how to make this reactive.
PS: I searched to do this without forEach but $set needs an index.
I'd appreciate any help here.
This solution should work and maintain reactivity.
You should be able to replace the entire array without using splice or set.
I have used a closure to capture this because sometimes the fetch call interferes with the this reference, even inside a lambda expression.
countryChangeHandler() {
this.states = []
const that = this
fetch(`/scripts/statejson.php?country=${this.country}`)
.then(response => response.json())
.then(res => {
that.states = res.states.map(it=>it.urlid)
})
},

Appending multi dimensional array in react state

I'm trying to append array which is react state:
const [ products, setProducts ] = useState([])
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
const copy = JSON.parse(JSON.stringify(products))
copy[category.id] = data
setProducts(copy)
})
})
},[])
service.getCategory() fetches data over HTTP returning array. products is nested array, or at least it's suppose to be. config.category is defined as:
categories: [
{
name: 'product1',
id: 0
},
{
name: 'product2',
id: 1
},
{
name: 'product3',
id: 2
}]
}
Eventually products should be appended 3 times and it should contain 3 arrays containing products from these categories. Instead products array ends up including only data from last HTTP fetch, meaning the final array looks something like this
products = [null, null, [{},{},{},..{}]].
I hope someone knows what's going on? Been tinkering with this for a while now.
The problem is that your fulfillment handlers close over a stale copy of products (the empty array that's part of the initial state). In a useEffect (or useCallback or useMemo, etc.) hook, you can't use any state items that aren't part of the dependency array that you provide to the hook. In your case, you just want to get the data on mount, so an empty dependency array is correct. That means you can't use any state items in the callback.
What you can do instead is use the callback form of the state setter:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => { // Use the callback form
const copy = products.slice(); // Shallow copy of array
copy[category.id] = data; // Set this data
return copy; // Return the shallow copy
});
});
});
}, []);
Or more concisely (but harder to debug!) without the explanatory comments:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => Object.assign([], products, {[category.id]: data}));
});
});
}, []);
Those both use the same logic as your original code, but update the array correctly. (They also only make a shallow copy of the array. There's no need for a deep copy, we're not modifying any of the objects, just the array itself.)
But, that does a state update each time getCategory completes — so, three times in your example of three categories. If it happens that the request for id 2 completes before the request for id 1 or 0, your array will look like this after the first state update:
[undefined, undefined, {/*data for id = 2*/}]
You'll need to be sure that you handle those undefined entries when rendering your component.

React set state to nested object value

I need to set state on nested object value that changes dynamically Im not sure how this can be done, this is what Ive tried.
const [userRoles] = useState(null);
const { isLoading, user, error } = useAuth0();
useEffect(() => {
console.log(user);
// const i = Object.values(user).map(value => value.roles);
// ^ this line gives me an react error boundary error
}, [user]);
// This is the provider
<UserProvider
id="1"
email={user?.email}
roles={userRoles}
>
The user object looks like this:
{
name: "GGG",
"website.com": {
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
I need to grab the roles value but its parent, "website.com" changes everytime I call the api so i need to find a way to search for the roles.
I think you need to modify the shape of your object. I find it strange that some keys seem to be fixed, but one seems to be variable. Dynamic keys can be very useful, but this doesn't seem like the right place to use them. I suggest that you change the shape of the user object to something like this:
{
name: "GGG",
site: {
url: "website.com",
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
In your particular use case, fixed keys will save you lots and lots of headaches.
You can search the values for an element with key roles, and if found, return the roles value, otherwise undefined will be returned.
Object.values(user).find(el => el.roles)?.roles;
Note: I totally agree with others that you should seek to normalize your data to not use any dynamically generated property keys.
const user1 = {
name: "GGG",
"website.com": {
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
const user2 = {
name: "GGG",
friends: {},
otherData: {}
}
const roles1 = Object.values(user1).find(el => el.roles)?.roles;
const roles2 = Object.values(user2).find(el => el.roles)?.roles;
console.log(roles1); // ["SuperUser"]
console.log(roles2); // undefined
I would recommend what others have said about not having a dynamic key in your data object.
For updating complex object states I know if you are using React Hooks you can use the spread operator and basically clone the state and update it with an updated version. React hooks: How do I update state on a nested object with useState()?

Change variable by reference in react hooks

I have a value, i want to pass it to a function and change the original value from withinf the fuction and show it on the screen... im using react hooks. i dont want to use the state/setState, because the value is like 10 layers deep into a json, so it would be very hard to change it using spread... heres an example of what i want to do:
let data = {
phase:{
document:{
name: 'Example'}
}
}
changeName(phase.document.name)
function changeName(name){
name = "Changed name"
}
after that i want to display the changed name... is there a way of doing this?
You can use lodash.set:
let data = {
phase: {
document: {
name: "Example",
},
},
};
const shallowCopy = { ...data };
lodash.set(shallowCopy, `phase.document.name`, `New Changed Name`);
// You must change the reference in order to render
setState(shallowCopy);

Categories