How to share the composition across components in api-composition style? - vue.js

In my application I have two separate components:
products component (show the products in the ui)
filter-products component (show the criteria in ui)
total-filter-products component (show the total criteria items was applied in ui, for example my current search is age,name so on)
They are separate and not communicate with input (:) or output (#).
I also have a ts file, with function useProducts as "api-composition style".
export const useProducts = () => {
const criteria = ref({ category: null, age: null, color: null });
const items = computed(() => { return store.get('products').filter(p => filterByCritiria(p)) });
const load = () => {
const params = parseSome(criteria.value);
axios.post('/api/...', { params }).then(r => { store.set('products', r.data)});
}
return { criteria, items, load }
}
In the products component, I just get the products from useProducts and bind to the template:
setup() {
const { products } = useProducts();
return { products }
}
In the filter-products, I get the criteria from useProducts and bind to the template. every time some value change in criteria ref (using v-model), I run the load function.
setup() {
const { criteria, load } = useProducts();
watch(() => criteria.value, (v) => load() });
return { criteria }
}
In total-filter-products component I do some logic to display the criteria:
setup() {
const { criteria } = useProducts();
return { criteria: parse(criteria) }
}
The problem with this is every time I call to useProducts I execute the function and get new variables.
I get three times the criteria. so if I change the criteria in filter-products, the total-filter-products and products components will never know about it.
How to share the composition across components in api-composition style?

Related

Method to check if item is saved within the Nuxt Store

I currently have a Store that has the "Saved" items from a feed for a user. I'm trying to figure out the best/efficient way to check if the item is already saved within the store.
I can't think of any other way than grabbing the entire store's contents in each feed item and checking whether the id exists? Surely there's a better way?
FeedItem.vue
methods: {
savePost(Post) {
this.$store.commit('savedPosts/addItem', Post)
},
deletePost(Post) {
this.$store.commit('savedPosts/removeItem', Post)
}
}
Store
export const state = () => ({
items: [
],
})
export const mutations = {
updateItemsOnLoad(state, array) {
var oldItems = state.items
var newItems = array.flat()
var joinedItems = newItems.concat(oldItems);
state.items = joinedItems.flat()
},
addItem(state, item) {
state.items.push(item)
this.$warehouse.set('savedPosts', state.items)
},
removeItem(state, item) {
var index = state.items.findIndex(c => c.id == item.id);
state.items.splice(index, 1);
this.$warehouse.set('savedPosts', state.items)
},
}
So my main question: Is there a more efficient way to check whether a post exists within the items array without querying it on every feed item?

Delete item from pinia state

I am new to vue and I have just started using pinia. I wanna delete an item from array but it does not work
here is my store
import {defineStore} from 'pinia'
export interface ObjectDto {
input: string,
}
interface ObjectDtoInterface {
objects: Array<ObjectDto>
}
export const useSearchHistoryStore = defineStore('objectsStore', {
state: (): ObjectDtoInterface => {
return {
objects: [] as ObjectDto[]
}
},
actions: {
add(dto: ObjectDto) {
if (this.objects
.filter(shd => dto.input === shd.input)
.length === 0) {
this.objects.unshift(dto)
}
},
delete(obj: ObjectDto) {
this.objects = this.objects.filter(e => !(e.input === obj.input))
}
}
})
and here is the function from different .ts file
function delete(obj: ObjectDto) {
objectsStore.delete(obj)
}
add action works perfect, it adds item to the state but when I try to delete an item, nothing happens. The data I pass to delete method is 100% good because I checked this many times
Filter does not mutate the original object, you need to reasing
delete(obj: ObjectDto) {
this.objects = this.objects.filter(e => !(e.input === obj.input))
}
more info https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

When state changes for graphql variable, result stays the same on react-native

I'm trying to create an app using shopify graphql api to create an ecommerce app on react native expo.
I have an onPress that calls a setState to change the state of the graphQL variable but the results don't change from the initial state of 'currentSubCategories'
const [currentSubCategories, setSubCategories] = useState(Categories[0].subCategory[0].handle);
let {
collection,
loading,
hasMore,
refetch,
isFetchingMore,
} = useCollectionQuery(currentSubCategories, first, priceRange);
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const onSubCategorySelect = (subCategory) => { setSubCategories(subCategory.handle) }
onPress={() => onSubCategorySelect(item)}
function useCollectionQuery(
collectionHandle: string,
first: number,
priceRange: [number, number],
) {
let [isInitFetching, setInitFetching] = useState<boolean>(true);
let [isReloading, setIsReloading] = useState<boolean>(true);
let [collection, setCollection] = useState<Array<Product>>([]);
let isFetchingMore = useRef<boolean>(false);
let hasMore = useRef<boolean>(true);
let defaultCurrency = useDefaultCurrency().data;
let { data, loading, refetch: refetchQuery } = useQuery<
GetCollection,
GetCollectionVariables
>(GET_COLLECTION, {
variables: {
collectionHandle,
first,
sortKey: ProductCollectionSortKeys.BEST_SELLING,
presentmentCurrencies: [defaultCurrency],
},
notifyOnNetworkStatusChange: true,
fetchPolicy: 'no-cache',
});
let getMoreUntilTarget = async (
targetAmount: number,
cursor: string | null,
handle: string,
filter: [number, number],
) => {
let result: Array<Product> = [];
let moreData: Array<Product> = [];
let { data } = await refetchQuery({
first,
collectionHandle: handle,
after: cursor,
});
...
useEffect(() => {
if (!loading) {
isFetchingMore.current = false;
}
if (isInitFetching && !!data && !!data.collectionByHandle) {
let newCollection = mapToProducts(data.collectionByHandle.products);
hasMore.current = !!data.collectionByHandle?.products.pageInfo
.hasNextPage;
setCollection(newCollection);
setIsReloading(false);
setInitFetching(false);
}
}, [loading, isInitFetching]); // eslint-disable-line react-hooks/exhaustive-deps
return {
collection,
loading: isReloading,
hasMore: hasMore.current,
isFetchingMore: isFetchingMore.current,
refetch,
};
}
I'm using flatList to show the result
<FlatList
data={collection}
renderItem={({ item }) => (
<Text>{item.title}</Text>
)}
/>
According to docs you have to pass new variables to refetch otherwise refetch will use initial values.
In this case (custom hook) you have 2 ways to solvethis problem:
return variables from your custom hook (taken from useQuery),
return some own refetch function.
1st option needs 'manual' variables updating like:
refetch( { ...variablesFromHook, collectionHandle: currentSubCategories } );
In 2nd case you can create myRefetch (and return as refetch) taking collectionHandle parameter to call refetch with updated variables - hiding 'complexity' inside your hook.
Both cases needs refetch call after updating state (setSubCategories) so you should use this refetch inside useEffect with [currentSubCategories] dependency ... or simply don't use state, call refetch directly from event handler (in onSubCategorySelect).

Redux - Filterable category with products - Actions & Filter

My state looks exactly like this:
{
categories: {
"unique-category-id" {
"data": {
// All kind of data (name, description ,etc )
"products": [
{
// Products that belong to this category with the current filter
}
]
},
"readyState": "CATEGORY_FETCHED",
"query": { // This object holds all the params that the filter allows to choose from
"brands": [],
"options": {
"usb": true
"minPrice": 200
}
}
}
}
}
This works and fetches data correctly. Now it's time to implement the filtering funcionality. I am trying to make it work like this, not sure if this is the "redux" way of doing things.
1.- On each filter on the category page we assign a click handler:
<li onClick={this.toggleFilterField} key={item.VALUE}>{item.NAME}</li>
toggleFilterField is passed into the container via mapDispatchToProps:
const mapDispatchToProps = (dispatch) => {
return {
toggleFilterField: (category, filter) => {
dispatch(toggleFilterField(category, filter))
}
}
}
export { CustomTagFilter };
export default connect(null, mapDispatchToProps)(CustomTagFilter);
2.- My toogleFilterField should just return an action like this one:
return {
type: CHANGE_FILTER,
category,
filter
}
Then my category_reducer.js should handle this action and update the following state key
state.categories[unique-category-id].query
Now this is OK, but now, after applying the filter to the state I would have to refetch the data from the category.
How and where do I fire a new action based on a previous action success?
Is this the correct way of handling filtered elements in redux that come from a paginated API?

Crash with simple history push

just trying come silly stuff and playing around with Cycle.js. and running into problem. Basically I just have a button. When you click it it's suppose to navigate the location to a random hash and display it. Almost like a stupid router w/o predefined routes. Ie. routes are dynamic. Again this isn't anything practical I am just messing with some stuff and trying to learn Cycle.js. But the code below crashes after I click "Add" button. However the location is updated. If I actually just navigate to "#/asdf" it displays the correct content with "Hash: #/asdf". Not sure why the flow is crashing with error:
render-dom.js:242 TypeError: Cannot read property 'subscribe' of undefined(…)
import Rx from 'rx';
import Cycle from '#cycle/core';
import { div, p, button, makeDOMDriver } from '#cycle/dom';
import { createHashHistory } from 'history';
import ranomdstring from 'randomstring';
const history = createHashHistory({ queryKey: false });
function CreateButton({ DOM }) {
const create$ = DOM.select('.create-button').events('click')
.map(() => {
return ranomdstring.generate(10);
}).startWith(null);
const vtree$ = create$.map(rs => rs ?
history.push(`/${rs}`) :
button('.create-button .btn .btn-default', 'Add')
);
return { DOM: vtree$ };
}
function main(sources) {
const hash = location.hash;
const DOM = sources.DOM;
const vtree$ = hash ?
Rx.Observable.of(
div([
p(`Hash: ${hash}`)
])
) :
CreateButton({ DOM }).DOM;
return {
DOM: vtree$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container')
});
Thank you for the help
I would further suggest using #cycle/history to do your route changing
(Only showing relevant parts)
import {makeHistoryDriver} from '#cycle/history'
import {createHashHistory} from 'history'
function main(sources) {
...
return {history: Rx.Observable.just('/some/route') } // a stream of urls
}
const history = createHashHistory({ queryKey: false })
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
history: makeHistoryDriver(history),
})
On your function CreateButton you are mapping your clicks to history.push() instead of mapping it to a vtree which causes the error:
function CreateButton({ DOM }) {
...
const vtree$ = create$.map(rs => rs
? history.push(`/${rs}`) // <-- not a vtree
: button('.create-button .btn .btn-default', 'Add')
);
...
}
Instead you could use the do operator to perform the hashchange:
function CreateButton({ DOM }) {
const create$ =
...
.do(history.push(`/${rs}`)); // <-- here
const vtree$ = Observable.of(
button('.create-button .btn .btn-default', 'Add')
);
...
}
However in functional programming you should not perform side effects on you app logic, every function must remain pure. Instead, all side effects should be handled by drivers. To learn more take a look at the drivers section on Cycle's documentation
To see a working driver jump at the end of the message.
Moreover on your main function you were not using streams to render your vtree. It would have not been reactive to locationHash changes because vtree$ = hash ? ... : ... is only evaluated once on app bootstrapping (when the main function is evaluated and "wires" every streams together).
An improvement will be to declare your main's vtree$ as following while keeping the same logic:
const vtree$ = hash$.map((hash) => hash ? ... : ...)
Here is a complete solution with a small locationHash driver:
import Rx from 'rx';
import Cycle from '#cycle/core';
import { div, p, button, makeDOMDriver } from '#cycle/dom';
import { createHashHistory } from 'history';
import randomstring from 'randomstring';
function makeLocationHashDriver (params) {
const history = createHashHistory(params);
return (routeChange$) => {
routeChange$
.filter(hash => {
const currentHash = location.hash.replace(/^#?\//g, '')
return hash && hash !== currentHash
})
.subscribe(hash => history.push(`/${hash}`));
return Rx.Observable.fromEvent(window, 'hashchange')
.startWith({})
.map(_ => location.hash);
}
}
function CreateButton({ DOM }) {
const create$ = DOM.select('.create-button').events('click')
.map(() => randomstring.generate(10))
.startWith(null);
const vtree$ = Rx.Observable.of(
button('.create-button .btn .btn-default', 'Add')
);
return { DOM: vtree$, routeChange$: create$ };
}
function main({ DOM, hash }) {
const button = CreateButton({ DOM })
const vtree$ = hash.map(hash => hash
? Rx.Observable.of(
div([
p(`Hash: ${hash}`)
])
)
: button.DOM
)
return {
DOM: vtree$,
hash: button.routeChange$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
hash: makeLocationHashDriver({ queryKey: false })
});
PS: there is a typo in your randomstring function name, I fixed it in my example.