what is the difference between writing composables and exporting stuff separately? - vue.js

We're developing an application on vue3-composition-api.
so we are developing scripts on this concept called composables. We use this composables in different places for reusing stuff instead of rewriting them, The problem is that some parts of the composables are created but not being used and it seems to have an impact on performance. Checkout these three approaches.
useFeatureA.js - FIRST APPROACH (using stateful composables)
export function useFeatureA() {
const x = ref(false)
const y = computed(() => {
// uses x.value
})
const z = computed(() => {
// uses x.value
})
const foo = () => {
// uses y.value and z.value
// sets on x.value
}
const bar = () => {
// uses z.value
// sets on x.value
}
return { y, foo, bar }
}
useFeatureA.js - SECOND APPROACH (exporting separately and recompute everytime)
export const getX = () => {
return ref(false);
}
export const getY = (x) => {
return computed(() => {
// uses x.value
});
}
export const foo = (xValue, yValue) => {
const z = // using xValue
// uses yValue and z
return // something to write on x
}
export const bar = (xValue) => {
const z = // using xValue
// uses and z
return // something to write on x
}
ComponentA
<script setup>
const { getX, getY, foo, bar } = useFeatureA();
const x = getX();
const y = getY(x);
x.value = foo(x.value, y.value);
x.value = bar(x.value);
</setup>
useFeatureA.js - THIRD APPROACH (move state to component)
export const getX = () => {
return ref(false);
}
export const getY = (x) => {
return computed(() => {
// uses x.value
});
}
export const getZ = (x) => {
return computed(() => {
// uses x.value
});
}
export const foo = (yValue, zValue) => {
// uses yValue and zValue
return // something to write on x
}
export const bar = (zValue) => {
// uses and z
return // something to write on x
}
ComponentA
<script setup>
const { getX, getY, foo, bar } = useFeatureA();
const x = getX();
const y = getY(x);
const z = getZ(x);
x.value = foo(y.value, z.value);
x.value = bar(z.value);
</setup>
we are wondering whether this solution can be efficient or not and what the differences between these two approaches are.
NOTE: We came up with the idea of separating those two functions in two different composables but the problem is that the project is too big and this approach makes way too many composables while foo and bar are very close to each other because of being related to a specific feature. so we guess this approach of separating them on different composables is out of question.
UPDATE: we have this idea that if we need stateful composable, the second approach is a bottleneck since if the components uses the composable, after the component is destroyed, the state remains in memory.
we guess in second approach tree-shaking is more efficient.

Related

Why the slice method doesn't work on array prop?

I have the following code which defines a Pinia storage:
import { ref, computed, shallowRef } from 'vue'
import { defineStore } from 'pinia'
export const usePokemonStore = defineStore('pokemons', () => {
// define the pokemons list state
const pokemonsList = ref([]);
const pokemonsLoaded = ref([]);
const pokemonsLoadedNames = ref([]);
// computed
const pokemonsListLength = computed(() => pokemonsList.value.length)
const pokemonsLoadedLength = computed(() => pokemonsLoaded.value.length)
// actions
async function getList() {
const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0');
const data = await res.json();
pokemonsList.value = data["results"];
}
async function loadPokemon(name) {
const URI = `https://pokeapi.co/api/v2/pokemon/${name}`
const res = await fetch(URI);
const data = await res.json();
pokemonsLoadedNames.value.push(data["name"])
pokemonsLoaded.value.push(data)
}
async function loadPokemons(offset, limit){
// basic check for limits
limit = limit > pokemonsListLength ? pokemonsListLength : limit;
limit = limit < 0 ? 10 : limit
// basic check for offset
offset = offset < 0 ? 0 : offset;
offset = offset > pokemonsListLength ? 0 : offset
for (let i = offset; i < offset+limit; i++){
// if the pokemon is already loaded skips the request for it
if (pokemonsLoadedNames.value.includes(pokemonsList.value[i].name)) {
continue;
}
// requests the pokemon given a name
loadPokemon(pokemonsList.value[i].name)
}
}
return {
pokemonsList,
pokemonsLoaded,
pokemonsListLength,
pokemonsLoadedLength,
pokemonsLoadedNames,
getList,
loadPokemon,
loadPokemons
}
})
And I have the following component which makes use of that storage to get the pokemons:
<template>
<div class="pokedex">
<PokemonImage class="pokemon-figure" pokemon="" />
<ul v-if="pokemonsToShow" class="pokemon-showcase">
<li class="pokemon-item" v-for="pokemon in pokemonsToShow">
<PokemonCard :pokemon="pokemon" />
</li>
</ul>
<div class="navigation">
<button v-show="page !== 1" #click="pageChange(-1)">Previous Page</button>
<button #click="pageChange(1)">Next Page</button>
</div>
{{ page }}
</div>
</template>
<script setup>
import { onBeforeMount, ref, computed, watch } from 'vue';
import { usePokemonStore } from '../stores/pokemon'
import PokemonCard from '../components/PokemonCard.vue'
import PokemonImage from '../components/PokemonImage.vue'
const pokeStore = usePokemonStore();
const page = ref(1)
const pokemonsToShow = ref([])
// offset and limit calculate based on the page
const limit = computed(() => 20 );
const offset = computed(() => page.value * limit.value - limit.value);
// initial load
onBeforeMount(async () => {
await pokeStore.getList()
await pokeStore.loadPokemons(0, limit.value)
pokemonsToShow.value = pokeStore.pokemonsLoaded.slice(0, pokeStore.pokemonsLoadedLength)
})
const pageChange = async (step) => {
page.value = page.value + step
await pokeStore.loadPokemons(offset.value, limit.value)
const start = offset.value;
const end = offset.value + limit.value;
console.log(start, end)
console.log(pokeStore.pokemonsLoaded)
pokemonsToShow.value = pokeStore.pokemonsLoaded.slice(start, end)
console.log(pokemonsToShow.value)
}
</script>
Now when the user clicks on the page button the page.value is updated so that the computed values for the offset and the limit are also updated (in reality only the offset updates) that way if the page is new I can load new pokemons from that which I do by calling the pokeStore.loadPokemons(offset.value, limit.value) function and awaiting for that inside the pageChange function. But now I want to change the pokemonsToShow so I want to get a slice of the array of loaded pokemons in the storage but every time I try to slice that array I get back nothing, even though when I print the array using console.log(pokeStore.pokemonsLoaded) the array shows as updated with the new values, and the ranges are correct.
I'm expecting the array to slice correctly since if I put static values in this function call:
pokemonsToShow.value = pokeStore.pokemonsLoaded.slice(2, 4)
}
It works for some reason, but not with the values calculated dinamically
This is a tricky thing about console.log().
console.log(pokeStore.pokemonsLoaded) will show you the result of the fetched data even if console.log is in reality executed before the fetch is done. This is due to the fact that many browsers show a "live" view of object data.
https://developer.mozilla.org/en-US/docs/Web/API/Console/log#logging_objects
Don't use console.log(obj), use console.log(JSON.parse(JSON.stringify(obj))) ... many browsers provide a live view that constantly updates as values change. This may not be what you want.
It is probable then that the array has not actually been updated at the time you slice it. I also believe this is true because even though you await this call: await pokeStore.loadPokemons(...), that function does not await it's call to loadPokemon(). Since there is no await, the function immediately finishes executing before the fetch has finished and returns to your component code.
I believe if you do await that call, everything should start working
async function loadPokemons(offset, limit){
.
.
.
await loadPokemon(pokemonsList.value[i].name)
}

for loop only iterating twice with axios

const [Datalist,setDatalist] = useState([]);
useEffect(() => {
axios.get( 'http://0.0.0.0:8000/api/v1/questions/history/1')
.then(response => {
const questions = response.data;
const datalist = [];
for (let i = 0; i < questions.length - 1; i++) {
const data = new Object();
data.isExpanded = false;
data.question_id = questions[i].id;
data.question = questions[i].content;
data.type = questions[i].type;
data.commentType = questions[i].comment_type;
data.answer = [];
datalist.push(data);
}
setDatalist(datalist);
});
},[]);
I have three questions in my database currently. The for loop should be iterating through 0 to 2, however, it is only iterating twice.
And I'm also having problems putting the data into Datalist.
Anybody know where the issue is??
Thanks in advance!!
Change your for loop to this:
for (let i = 0; i < questions.length; i++)
Since you are iterating over each question you receive, you could use the map-method (if your environment supports ES6-Syntax - but since you're using react, it most likely dooes).
From the MDN Docs:
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
With map, your code could look like this:
(Also note the removal of const data = new Object();. you can initialize an object and assign its properties/values at the same time)
const [Datalist,setDatalist] = useState([]);
useEffect(() => {
axios.get( 'http://0.0.0.0:8000/api/v1/questions/history/1')
.then(response => {
const questions = response.data;
const datalist = questions.map(question => {
return {
isExpanded: false;
question_id: question.id;
question: question.content;
type: question.type;
commentType: question.comment_type;
answer: [];
};
});
setDatalist(datalist);
});
},[]);

Debounce mobx-react and props

I'm trying to debounce the method call: "chart.calculateChartData(props.answers)".
I tried:
- autorun
- reaction
- use-debounce from a react lib.
- setTimeout within calculateChartData
Each solution led to an update cycle or didn't work because MobX is not immutable.
Has someone a hint?
function QuantificationChart(props: QuantificationChartProps) {
const {t} = useTranslation();
const rootStore = useRootStore();
const chart = rootStore.formulaStore.getChart<Chart>(Chart.chartName);
const data = chart.calculateChartData(props.answers);
calculateChartData = (questionData: { [id: string]: number; } = {}) => {
let chartData = [];
for (let i = 0; i < this.numberOfYears + 1; ++i) {
let customData = {...questionData, "year" : i};
let chartEntry = {
cost: this.rootStore.formulaStore.calculateFormula(this.cost.formula, customData),
earn: this.rootStore.formulaStore.calculateFormula(this.earn.formula, customData),
sum: 0
};
chartEntry.sum = (chartEntry.earn - chartEntry.cost) + (chartData[i - 1]?.sum || 0);
chartData.push(chartEntry);
}
return chartData;
};
Hint: It's my first project with MobX
Found a solution. Seems to work:
Based on: https://mobx-react.js.org/recipes-effects
const [data, setData] = React.useState(chart.calculateChartData(props.answers));
React.useEffect(
() =>
autorun(() => {
setData(chart.calculateChartData(props.answers));
}, {delay: 1000}),
[],
);

How to Improve The axios.spread

The below code i use for doing multiple HTTP calls depending on the studentList.
It works well; however, I think the axios spread is not necessary
export default {
getFee (studentList: { studentId: string }[]) {
if (studentList.length < 1) {
Promise.resolve()
}
let promises = []
for (const student of studentList) {
if (!student.studentId) {
Promise.resolve()
}
var url = `${API_URL}/${student.studentId}`
promises.push(Axios.get(url))
}
return Axios.all(promises)
.then(Axios.spread((...args) => {
// customise the response here
return args
.map(response => response.data)
.map(data => {
// #ts-ignore
data.totalMark = data.markinPhysics + data.markinMaths + data.markinChemistry // total mark sum of marks in differnet discplines
return data
})
}))
.catch(error => {
switch (error.response.status) {
case 400:
console.log('student not found')
break
case 500:
console.log('error invoking')
break
default:
console.log('unknown error')
I have to do multiple network calls in Vue and I am using Axios.
I got it working by axios, all and axios.spread, but I think the code can be improved.
The logic is to do multiple calls for the student list and get the outputs back
Can anyone help?
Axios.all
as well as Promise.all accepts array of promises and returns a new Promise which is resolved whenever all of the given promises are resolved with an array with the result of each promise
e.g.
const promise1 = Promise.resolve('data1');
const promise2 = Promise.resolve('data2');
Promise.all([
promise1,
promise2,
]).then(results => {
// results is an array with 2 elements
console.log(results[0]); // data1
console.log(results[1]); // data2
});
you can use Axios.spread to to assign each result to a variable like this:
Promise.all([
promise1,
promise2,
]).then(Axios.spread(( result1, result2 ) => {
// args is an array with 2 elements
console.log(result1); // data1
console.log(result2); // data2
});
alternatively you can use ES6 Destructuring assignment:
Promise.all([
promise1,
promise2,
]).then(([ result1, result2 ]) => {
// args is an array with 2 elements
console.log(result1); // data1
console.log(result2); // data2
});
Unnecessary Promise.resolve()
Your Promise.resolve() function calls have no effect on the getFee method since you're not returning them
What would my implementation be
async function getFee(studentList) {
try {
const promises = studentList.reduce((acc, student) =>
student.studentId
? acc.concat(Axios.get(`${API_URL}/${student.studentId}`))
: acc
, []);
const responses = await Axios.all(promises);
return responses
.map(response => response.data)
.map(data => ({
// return new object
// with data's properties
// instead of assinging the new ones directly to the data
...data,
// total mark sum of marks in differnet discplines
totalMark: data.markinPhysics + data.markinMaths + data.markinChemistry,
}));
} catch (error) {
switch (error.response.status) {
case 400:
console.log("student not found");
break;
case 500:
console.log("error invoking");
break;
default:
console.log("unknown error");
}
}
}
export default {
getFee
}
Since you're only using args as an array, you could remove axios.spread.
axios.spread() might only be useful in older browsers now that ES2015 introduced its own spread operator. The main purpose of axios.spread() is to expand the result of axios.all() into an argument list, such that you could do:
axios.all(promiseArray).then(axios.spread(function(arg1, arg2, arg3) {
/*...*/
}))
instead of:
axios.all(promiseArray).then(function(args) {
var arg1 = args[0]
var arg2 = args[1]
var arg3 = args[2]
/*...*/
})
ES2015's rest operator does the inverse of axios.spread(), so when you combine them (as seen below), you end up with the result above, as if axios.spread() and the rest operator weren't even used:
axios.all(promiseArray).then(axios.spread(function(...args) {
var arg1 = args[0]
var arg2 = args[1]
var arg3 = args[2]
/*...*/
}))
// or newer syntax:
axios.all(promiseArray).then(axios.spread((...args) => {
const arg1 = args[0]
const arg2 = args[1]
const arg3 = args[2]
/*...*/
}))
To avoid promise chaining and improve readability, I think below can be used.
const [arg1, arg2] = await Promise.all(promises)

custom sum elements by key using lodash

I do have two objects containing keys like
var a = {bar:[1,2], foo:[7,9]}
var b = {bar:[2,2], foo:[3,1]}
I want to get the fallowing results:
var c = {bar:[3,4], foo:[10,10]}
I already have a for logic like:
for (let key in b) {
if (a[key]) {
a[key][0] += b[key][0];
a[key][1] += b[key][1];
}
else a[key] = b[key];
}
But I would like to make this logic in a lodash way. How can I Do it?
You can use create a function that takes n objects, and collects them to an array using rest parameters. Now you can spread the array into _.mergeWith() to combine the objects, and in the customizer function sum the items in the arrays using Array.map() or lodash's _.map() and _.add():
const { mergeWith, isArray, map, add } = _
const fn = (...rest) => _.mergeWith({}, ...rest, (o = [], s) =>
map(s, (n, i) => add(n, o[i]))
)
const a = {bar:[1,2], foo:[7,9]}
const b = {bar:[2,2], foo:[3,1]}
const c = {bar:[3,2], foo:[5,6]}
const d = {bar:[4,2], foo:[5,4]}
const result = fn(a, b, c, d)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
You can also use lodash/fp to create a function that merges all values to a multidimensional array with _.mergeAllWith(), then transpose the arrays using _.zipAll(), and sums each array:
const { rest, flow, mergeAllWith, isArray, head, mapValues, zipAll, map, sum } = _
const fn = rest(flow(
mergeAllWith((o, s) => [...isArray(head(o)) ? o : [o], s]), // combine to a multidimensional array
mapValues(flow(
zipAll,
map(sum)
)),
))
const a = {bar:[1,2], foo:[7,9]}
const b = {bar:[2,2], foo:[3,1]}
const c = {bar:[3,2], foo:[5,6]}
const d = {bar:[4,2], foo:[5,4]}
const result = fn(a, b, c, d)
console.log(result)
<script src='https://cdn.jsdelivr.net/g/lodash#4(lodash.min.js+lodash.fp.min.js)'></script>
You can accomplish this using plain JavaScript with Object.entries, concat and reduce:
const a = { bar: [1,2], foo: [7,9] };
const b = { bar: [2,2], foo: [3,1] };
const entries = Object.entries(a).concat(Object.entries(b));
const result = entries.reduce((accum, [key, val]) => {
accum[key] = accum[key] ? accum[key].map((x, i) => x + val[i]) : val;
return accum;
}, { });
console.log(result);