I'm working with a parent companyList component, a reusable Table component and an useFetch composable in vue 3.2
Without the Table component I had the following code:
companyList
<script setup>
import { computed } from 'vue';
import useFetch from '#/composables/useFetch';
import { formatEmail, formatPhone, formatEnterpriseNumber } from '#/utils/formatters';
const { response, isFetching, error } = useFetch('get', '/companies');
const companies = computed(() =>
response.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
</script>
In the Table component which contains pagination, sorting and a search, a watchEffect checks if the state has changed and triggers an emit from the parent component. In this case getCompanies. This looks like this:
companyList
<script setup>
const getCompanies = (search, sortKey, orderKey) => {
const { response, isFetching, error } = useFetch('get', '/companies', {
params: {
keyword: search,
sort_by: sortKey,
order_by: orderKey,
},
});
};
const companies = computed(() =>
response.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
</script>
<template>
<Spinner v-if="isFetching" size="medium" />
<ErrorMessage v-else-if="error" showReload :description="error" />
<NoDataMessage v-else-if="!companies || companies.length <= 0" />
<div v-else>
<Table :columns="tableColumns" :data="companies" #fetchData="getCompanies">
<template v-slot:id="{ item }">
<Badge>
{{ item.id }}
</Badge>
</template>
<template v-slot:actions="{ item }">
<router-link :to="{ name: 'clientDetails', params: { client_id: item.id } }" class="text-blue-500 lowercase"> {{ $tc('detail', 2) }} </router-link>
</template>
</Table>
</div>
</template>
Question: How can I get the response, isFetching and error out of the getCompanies function and use it inside the template tags? It feels like a waste of using a reusable system if I have to define refs to get them out. On top of that I can't use the same names. Is there another solution than this:
const local_response = ref(null);
const local_isFetching = ref(null);
const local_error = ref(null);
const getCompanies = (search, sortKey, orderKey) => {
const { response, isFetching, error } = useFetch('get', '/companies', {
params: {
keyword: search,
sort_by: sortKey,
order_by: orderKey,
},
});
local_response.value = response;
local_isFetching.value = isFetching;
local_error.value = error;
};
const companies = computed(() =>
local_response.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
useFetch provides an option to delay execution until you want to:
const { execute: getCompanies, response, isFetching, error } = useFetch('get',
'/companies', {
immediate: false, // defer execution until execute is called
params: {
keyword: search,
sort_by: sortKey,
order_by: orderKey,
},
});
// getCompanies, response, isFetching, and error will all be available to the template
Related
I'm new to Vue, javascript & Web development. Using Vue, I tried to recreate the moviedb app(from Brad's 50 JS Projects in 50 Days course).
I'm getting stuck and can't get the data out of a scope.
I've successfully retrieved data & destructured it.
But how can I get those values out of that scope (out of setMovies function) and use it in the Vue file (html template)?
Here's my code:
I've made the api_key private
<h1>MovieDesk</h1>
<div class="hero">
<!-- Search -->
<div class="search">
<form #submit.prevent="handleSearch">
<input type="text" placeholder="Search here..." />
<button #click="handleSearch">Search</button>
</form>
</div>
</div>
<!-- Movies -->
<div v-if="searchOn">
<SearchedMovies />
</div>
<div v-else>
<MovieList/>
</div>
</template>
<script>
// imports-------------------
import { ref } from "#vue/reactivity";
import MovieList from "../components/MovieList.vue";
import SearchedMovies from "../components/SearchedMovies.vue";
import { onMounted } from "#vue/runtime-core";
export default {
components: { MovieList, SearchedMovies },
setup() {
const searchOn = ref(false);
const api_url = ref(
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=api_key&page=1"
);
const movies = ref([])
// getting the data ------------------------------
onMounted(() => {
fetch(api_url.value)
.then((res) => res.json())
.then((data) => {
console.log(data);
setMovies(data.results);
});
});
function setMovies(movies) {
movies.forEach((movie) => {
const { title, poster_path, vote_average, overview } = movie;
});
}
return { searchOn, setMovies };
},
};
</script> ```
In your setMovies function, You can set the response in the movies variable and then return that variable from your setup.
function setMovies(apiResponse) {
movies.value = apiResponse
}
return { movies };
Live Demo :
const { ref, onMounted } = Vue;
const App = {
setup() {
const movies = ref([])
onMounted(() => {
const apiResponse = [{
id: 1,
name: 'Movie 1'
}, {
id: 2,
name: 'Movie 2'
}, {
id: 3,
name: 'Movie 3'
}];
setMovies(apiResponse);
})
function setMovies(res) {
movies.value = res;
}
return {
movies
};
}
};
Vue.createApp(App).mount("#app");
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<pre>{{ movies }}</pre>
</div>
Add 'movies' to the return statement at the bottom of your code, then you should be able to render it.
When making a request to my API from a component and using react-data-table-component everything works perfectly but if I try to make the request from my Product Provider the pagination is incorrect and no longer works as expected.
With this code I make the request, datatable and pagination from my component working perfectly:
import React, { useState, useEffect, useCallback, useMemo } from "react";
import axiosClient from "../config/axiosClient";
import DataTable from 'react-data-table-component-with-filter'
import { CSVLink } from "react-csv"
import { Link } from 'react-router-dom'
import useProducts from "../hooks/useProducts";
const removeItem = (array, item) => {
const newArray = array.slice();
newArray.splice(newArray.findIndex(a => a === item), 1);
return newArray;
};
const ProductsTest = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [totalRows, setTotalRows] = useState(0);
const [perPage, setPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [searchBox, setSearchBox] = useState('')
const STRING_TRADUCTIONS = { "KILOGRAM" : "KILOGRAMOS", "GRAMS" : "GRAMOS", "BOX" : "CAJA", "PACKAGE" : "PAQUETE", "BOTTLE" : "BOTE", "PIECES" : "PIEZAS", "BAG" : "BOLSA", "LITER" : "LITRO" }
const fetchUsers = async (page, limit = perPage, search = searchBox) => {
setLoading(true)
const dataOnLs = localStorage.getItem('cmtjs')
const config = {
headers: {
"Content-Type": "application/json",
apiKey: dataOnLs
} ,
params: {
limit,
page,
search
}
}
const response = await axiosClient(`/products`, config)
const data = response.data.products.docs.map( doc => (
{
_id: doc._id,
idProduct: doc.idProduct,
barCode: doc.barCode,
name: doc.name,
presentation: STRING_TRADUCTIONS[doc.presentation],
salePrice: doc.salePrice,
purchasePrice: doc.purchasePrice,
stock: doc.stock,
user: doc.user.username,
category: doc.category.name,
provider: doc.provider.name
}
))
setData(data);
setTotalRows(response.data.products.totalDocs);
setLoading(false);
};
useEffect(() => {
fetchUsers(1)
}, []);
const columns = useMemo(
() => [
{
name: "ID",
selector: "idProduct",
sortable: true
},
{
name: "Código de Barras",
selector: "barCode",
sortable: true
},
{
name: "Nombre",
selector: "name",
sortable: true
},
{
name: "Presentación",
selector: "presentation",
sortable: true
},
{
name: "Precio",
selector: "salePrice",
sortable: true
},
{
name: "Stock",
selector: "stock",
sortable: true
},
{ cell: row =>
<Link to={ `/dashboard/product/${row._id}`}>
<button className='btn btn-ghost text-xs'>
Mas
</button>
</Link>}
]
);
const handlePageChange = page => {
fetchUsers(page);
setCurrentPage(page);
};
const handlePerRowsChange = async (newPerPage, page) => {
fetchUsers(page, newPerPage);
setPerPage(newPerPage);
}
const headers = [
{ label: "ID", key: "idProduct" },
{ label: "Código de Barras", key: "barCode" },
{ label: "Nombre", key: "name" },
{ label: "Presentación", key: "presentation" },
{ label: "Precio Venta", key: "salePrice" },
{ label: "Precio Compra", key: "purchasePrice" },
{ label: "Stock", key: "stock" },
{ label: "Creador", key: "user" },
{ label: "Categoría", key: "category" },
{ label: "Proveedor", key: "provider" }
]
const paginationComponentOptions = {
rowsPerPageText: 'Mostrar',
rangeSeparatorText: 'de',
selectAllRowsItem: true,
selectAllRowsItemText: 'Todos',
};
const clear = () => {
setPerPage(10)
setSearchBox('')
fetchUsers(1, 10, '')
}
return (
<div>
<input type="text" onChange={(e)=> setSearchBox(e.target.value)}/>
<button onClick={ ()=> fetchUsers()}>Buscar</button>
<CSVLink data={data} headers={headers} filename={"productos.cdtmx.csv"} className="cursor-pointer">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAACnUlEQVRoge2YXUhUQRiGn7HVRVddc8k0QawQw6hQohKSlsp+oDulQBGCbuxGQ1itDIx+IIWINKK86S4o6ibFEG9ywQzCEJLVSCylorYf1CJaXXe6SPvZ1ePZs3tczfNcfsP3zfvOzHc4M2BgYGAQSYTWxJSrReUg6oHEYHNtuRu1TDkupKx27Tp38+9glJZKv9AmPgQSpYiq9w+GYGBBxU8jrf6RUAwsCgwDkcYwEGlM/gFLVf4JkBcEWJQSvw+/Ua4cJYixWjElxoemcB4CDKgRrwqfZGJ0lImpqYChFe53IZefIeAIhUX8DDJsleZkyffA8jBgNkXTe+ouXy93U2Ev+R3PSE7jU70T96VHZNrSdROphCoDHu8kZ1quAVC1u4x4cxwAdQfLMZuiaei4xevPb/VTqYDqI9Ta56Rj4Ak2SxLHCw6zaU0Wxbl7Gfw4QlPnbT01KjLLZ3RuTj9oxJ61lQp7CYUbdiAQVN5rwOOd1EvfvATVxAMfXtHcdZ+k2ATy127hzrN2nIM9emlTRVA7AJAwff4BhJj/QmfpDuwNT0pCsNPOSVA7ULA+j7Jth3g63EfXUC9H8vazJ3t72MRoQbUBsymaK0UOhBCcbbtBbUsTUkoai2uIi4nVU6Miqg2c3HeM7NWZtPc/xjnYQ8+Ii9Y+JxnJadQUHtVToyKqDOSkrqPSXopP+jj/8M+jQF3bdby+KSrspWxOz9JNpBKqmtj1fojk6oKA+Ev3CCsdO8MuKhiWx7/QYmY2A9/CVt2n/4Vgth6oBS4Cod0FfVKa3Z4fgMd/yPb8i+ay/pma30ZTD+RoXl5rxiqtqbxo7vxH83/ZA0sKw0CkWdYGxsOmQj1j/gHNBoQQDhABBXVkTEocCzifgYGBgQp+AlpMnf09Cu/RAAAAAElFTkSuQmCC"/>
</CSVLink>
<DataTable
columns={columns}
data={data}
progressPending={loading}
pagination
paginationServer
paginationTotalRows={totalRows}
paginationDefaultPage={currentPage}
onChangeRowsPerPage={handlePerRowsChange}
onChangePage={handlePageChange}
selectableRows
//onSelectedRowsChange={({ selectedRows }) => console.log(selectedRows)}
paginationComponentOptions={paginationComponentOptions}
noDataComponent="No hay resultados"
/>
{
searchBox && searchBox !== '' && <button onClick={ () => clear() }>Limpiar</button>
}
</div>
)
}
But I have my product provider where I make a get request to all my products, avoiding making the requests from my component and having the data globally, but if I use "getProducts" from my provider, the first view of the datatable is correct however , when clicking on a page or next, the pager advances but the data displayed does not, for example: it shows me the first 10 records but I ask for the next 10 and the pager advances correctly but the data is still the first 10 records, No I know how to use my provider and make the pager show the following data depending on what the user needs.
This is the code of my provider to obtain the product data
const getProducts = async(page, limit, search) => {
const dataOnLs = localStorage.getItem('cmtjs')
const config = {
headers: {
"Content-Type": "application/json",
apiKey: dataOnLs
} ,
params: {
page,
limit,
search
}
}
try {
const { data } = await axiosClient(`/products`, config)
setProducts(data)
} catch (error) {
So in my component I call "getProducts" from my provider to have the products data in "products" using the useEffect hook
useEffect(() => {
getProducts()
setData(products.products?.docs)
setTotalRows(products.products?.totalDocs)
setLoading(false)
}, []);
In my paginator to obtain the following 10 product records, I click and the text changes that indicates which page is being shown, but the data remains the same as the first page
const handlePageChange = page => {
getProducts(page); // it does not show the next 10 records as it happened in the fetch of my component
setCurrentPage(page); // OK
};
In the same way, my browser no longer works using it in this way, I only changed the function to call my provider now, but it does not work
<input type="text" onChange={(e)=> setSearchBox(e.target.value)}/>
<button onClick={ ()=> getProducts()}>Buscar</button>
I would like to know if you can help me to make my datatable and browser work using my provider. Thanks.
In this code
useEffect(() => {
getProducts()
setData(products.products?.docs)
setTotalRows(products.products?.totalDocs)
setLoading(false)
}, []);
You are calling getProducts(), which is asynchronous. Then you try setData(products...), but the asynchronous call did not finish yet, so products was not updated yet. When eventually the asynchronous code terminates, the useEffect statement is not triggered again, because the dependency array states that the effect is only executed when the component mounts.
Split up your effect in two parts instead, so the second effect gets triggered when new products are available:
useEffect(() => {
getProducts()
}, []);
useEffect(() => {
setData(products.products?.docs)
setTotalRows(products.products?.totalDocs)
setLoading(false)
}, [products]);
import axios from "axios";
export const routerid = (itemId) =>
axios.get("https://fakestoreapi.com/products?limit=" + itemId);
<template>
<div>
<div v-for="(item, key) in user" :key="key">
{{ item.price }} <br />
{{ item.description }} <br />
</div>
</div>
</template>
<script>
import { routerid } from "./routerid";
export default {
name: "User",
components: {},
data() {
return {
lists: [],
};
},
mounted() {
if (this.$route.params.id)
routerid(this.$route.params.id).then((r) => {
let obj = r.data;
this.lists = [{ ...obj }];
});
},
computed: {
user: function () {
return this.lists.filter((item) => {
return item.id === this.$route.params.id;
});
},
},
};
</script>
How to make axios url call with query params like this..https://fakestoreapi.com/products?limit=1 Where you can see in the url i have ...like ?limit=id.... So i am little bit confused about it..How to call
Did i correctly call the api or anything missing in the code logic. As of now, In my output, I cant see any response from the api.
Code:- https://codesandbox.io/s/cocky-ives-h19zm7?file=/src/components/routerid.js
Don't you want to fetch a product by its ID?
Then it rather would be:
export const routerid = async (itemId) =>
await axios.get("https://fakestoreapi.com/products/" + itemId);
https://codesandbox.io/s/silly-nightingale-ixz8tr
I have difficult to use vuex global state combine with re-render child-component in Vue.js.
The global state is mutated but does not re-render its data in v-for loop.
All list of data is rendered, but when the new data changes, component in /blog does not change data in it.
Here is some code:
/store/index.js
export const state = () => ({
allData: [],
})
export const getters = {
getAllData: (state) => state.allData,
}
export const mutations = {
GET_DATAS(state, payload) {
state.allData = payload
},
UPDATE_DATA(state, payload) {
const item = state.allData[payload.index]
Object.assign(item, payload)
},
}
export const actions = {
getDatas({ commit, state }, payload) {
return fetch(`URL_FETCH`)
.then((data) => data.json())
.then((data) => {
commit('GET_DATAS', data)
})
.catch((err) => console.log(err))
},
updateData({ commit, state }, payload) {
commit('UPDATE_DATA', payload)
},
}
in /layouts/default.vue
beforeCreate() {
this.$store.dispatch('getDatas').then(() => {
connectSocket()
})
},
methods: {
connectSocket() {
// connect & received message from socket
// received message from socket
this.$root.$emit('updateData', {
index: 12,
price: 34,
change: 56,
percent: 78,
})
},
},
and in /pages/blog/index.vue
<template>
<div>
<div
v-for="index in getAllData"
:key="index.name"
class="w-100 grid-wrapper"
>
<div>{{ index.price }}</div>
<div>{{ index.change }}</div>
<div>{{ index.percent }}</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {}
},
computed: {
...mapGetters(['getAllData']),
},
mounted() {
this.$root.$on('updateData', (item) => {
this.$store.dispatch('updateData', {
index: item.index,
price: item.price,
percent: item.percent,
change: item.change,
})
})
},
}
</script>
Here is a complete example on how to use Vuex and load the data efficiently into a Nuxt app (subjective but using good practices).
/pages/index.vue
<template>
<div>
<main v-if="!$fetchState.pending">
<div v-for="user in allData" :key="user.id" style="padding: 0.5rem 0">
<span>{{ user.email }}</span>
</div>
</main>
<button #click="fakeUpdate">Update the 2nd user</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data() {
return {
mockedData: {
name: 'John Doe',
username: 'jodoe',
email: 'yoloswag#gmail.com',
phone: '1-770-736-8031 x56442',
website: 'hildegard.org',
},
}
},
async fetch() {
await this.setAllData()
},
computed: {
...mapState(['allData']),
},
methods: {
...mapActions(['setAllData', 'updateData']),
fakeUpdate() {
this.updateData({ index: 1, payload: this.mockedData })
},
},
}
</script>
/store/index.js
import Vue from 'vue'
export const state = () => ({
allData: [],
})
export const mutations = {
SET_ALL_DATA(state, payload) {
state.allData = payload
},
UPDATE_SPECIFIC_DATA(state, { index, payload }) {
Vue.set(state.allData, index, payload)
},
}
export const actions = {
async setAllData({ commit }) {
try {
const httpCall = await fetch('https://jsonplaceholder.typicode.com/users')
const response = await httpCall.json()
commit('SET_ALL_DATA', response)
} catch (e) {
console.warn('error >>', e)
}
},
updateData({ commit }, { index, payload }) {
commit('UPDATE_SPECIFIC_DATA', { index, payload })
},
}
I can't seem to figure out what's causing the above issue, and debug properly. From my understanding of Redux Slices, I'm able to directly mutate state in my reducer due to the Immer functionality built-in. If I hard code the redux JSON into the UI component there are no issues which leads me to believe it's a Redux issue. Any advice would be appreciated.
Slice.ts
interface LoadSchedulerState {
gridData: DataRow[] | null,
}
interface DataRow {
id: number,
dis: string,
hour: string
}
const initialState: LoadSchedulerState = {
gridData: null,
}
export const loadSchedulerSlice = createSlice({
name: 'load_scheduler',
initialState,
reducers: {
updateGridData: (state, action: PayloadAction<DataRow>) => {
let newData = [{...action.payload}]
return {...state, gridData:newData}
},
},
});
export const {updateGridData} = loadSchedulerSlice.actions;
export const gridData = (state: { loadScheduler: { gridData: any; }; }) => state.loadScheduler.gridData;
export default loadSchedulerSlice.reducer;
LoadScheduler.ts
import { AgGridColumn, AgGridReact } from "#ag-grid-community/react";
import HeaderGroupComponent from "./HeaderGroupComponent.jsx";
import LoadHeaderComponent from "./LoadHeaderComponent.jsx";
import BtnCellRenderer from './BtnCellRenderer';
import {
AllModules,
ColumnApi,
GridApi,
GridReadyEvent,
} from "#ag-grid-enterprise/all-modules";
import "../../styles/DemoGrid.css";
import { updateGridData, gridData } from "./loadSchedulerSlice";
import { useDispatch, useSelector } from 'react-redux';
const LoadSchedulerGrid = () => {
const [gridApi, setGridApi] = useState<GridApi>();
const [columnApi, setColumnApi] = useState<ColumnApi>();
const [rowData, setRowData] = useState<any>(null);
const gridStateData = useSelector(gridData);
const dispatch = useDispatch();
// PUSH TABLE CHANGES VIA WEBSOCKET TO BACKEND
const handleCellChange = (event: any) => {
}
var init_data = {
id: 0,
dis: "Mon 10/19 8:09 A",
hour: "8 a"
}
const dataSetter = (params: { newValue: any; data: any; }) => {
params.data.dis = params.newValue;
return false;
};
const onGridReady = (params: GridReadyEvent) => {
dispatch(updateGridData(init_data))
setGridApi(params.api);
setColumnApi(params.columnApi);
};
return (
<div className="ag-theme-alpine demo-grid-wrap">
<AgGridReact
onGridReady={(params) => {
onGridReady(params);
}}
immutableData={true}
rowData={gridStateData}
getRowNodeId={node => node.id}
modules={AllModules}
onCellValueChanged={handleCellChange}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
headerComponentFramework: LoadHeaderComponent,
headerComponentParams: {
menuIcon: "fa-bars",
},
}}
>
<AgGridColumn headerName="#" width={50} checkboxSelection sortable={false} suppressMenu filter={false} pinned></AgGridColumn>
<AgGridColumn headerName="Load Details" headerGroupComponentFramework={HeaderGroupComponent}>
<AgGridColumn field="dis" width={110} headerName="Dispatch" editable cellClass="dispatch" valueSetter={dataSetter} />
<AgGridColumn field="hour" width={50} headerName="Hour" cellClass="hour" />
</AgGridColumn>
</AgGridReact>
</div>
);
};
const rules = {
dc_rules:{
"cell-blue": (params: { value: string }) => params.value === 'ERD',
"cell-beige": (params: {value: string }) => params.value === 'PDC',
"cell-cyan": (params: {value: string }) => params.value === 'CRD'
},
nr_cube_rules:{
"cell-red": (params: {value: number }) => params.value > 10.0
}
}
export default LoadSchedulerGrid;
Ag-grid per default tries to directly mutate the state object outside of a reducer. You have to use Ag-Grids immutableData setting.
https://www.ag-grid.com/javascript-data-grid/immutable-data/
They even have a blog article about using RTK with Ag-Grid (even if they use immutable logic in the reducers - within the RTK reducers this is not necessary as you correctly noted): https://blog.ag-grid.com/adding-removing-rows-columns-ag-grid-with-react-redux-toolkit/