Auto unsubscribe from custom store - javascript

I am developing app using svelte and I use custom stores.
I know that svelte automatically handles unsubscribe for us if we use $
<h1>The count is {$count}</h1>
but, as I have to filter my array in script, how can I use this advantage?
from
const filteredMenu = menu.filter("header");
to
$: filteredMenu = menu.filter("header");
?. or maybe I have to manually unsubscribe on unMount hook?
I am including my code
// /store/menu.ts
import { writable } from "svelte/store";
import { myCustomFetch } from "#/utils/fetch";
import type { NavbarType } from "#/types/store/menu";
const createMenu = () => {
const { subscribe, set } = writable(null);
let menuData: Array<NavbarType> = null;
return {
subscribe,
fetch: async (): Promise<void> => {
const { data, success } = await myCustomFetch("/api/menu/");
menuData = success ? data : null;
},
sort(arr: Array<NavbarType>, key: string = "ordering") {
return arr.sort((a: NavbarType, b: NavbarType) => a[key] - b[key]);
},
filter(position: string, shouldSort: boolean = true) {
const filtered = menuData.filter((item: NavbarType) =>
["both", position].includes(item.position)
);
return shouldSort ? this.sort(filtered) : filtered;
},
reset: () => set(null),
};
};
export const menu = createMenu();
// Navbar.svelte
<sript>
const filteredMenu = menu.filter("header");
</script>
{#each filteredMenu as item, index (index)}
<a
href={item.url}
target={item.is_external ? "_blank" : null}
class:link-selected={activeIndex == index}>{item.title}
</a>
{/each}

Related

Vue.js The list of mp3 files has already been stored in an array, but the sound still won't play

I encountered a problem while coding and was able to fix it by searching the internet and making small adjustments to sample code. However, as I continued to add more lines of code, I came across parts that I did not fully understand but was still able to adjust to make them work. My goal is to create code that will play a sound to notify people who are waiting for service that it is their turn. The "filenames.value" variable already holds an array list of MP3 files, but I'm currently unable to get any sound to play.
: onMounted is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.
<script setup>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
const collection1Data = ref([]);
const collection2Data = ref([]);
const finalData = ref([]);
const latestfinalData = ref([]);
const fetchData = async () => {
const [collection1Response, collection2Response] = await Promise.all([
axios.get('https://koh-abx.com:50100/onboardshows'),
axios.get('https://koh-abx.com:50100/onboardlands'),
]);
collection1Data.value = collection1Response.data;
collection2Data.value = collection2Response.data;
};
onMounted(async () => {
await fetchData();
setInterval(async () => {
await fetchData();
finalData.value = [];
collection1Data.value.forEach(doc1 => {
const matchingDoc = collection2Data.value.find(doc2 => doc1.idshow === doc2.idshow);
if (matchingDoc) {
finalData.value.push({
idshow: doc1.idshow,
numbershow: doc1.updatedAt > matchingDoc.updatedAt ? doc1.numbershow : matchingDoc.numbershow,
ab: doc1.updatedAt > matchingDoc.updatedAt ? doc1.ab : matchingDoc.ab,
updatedAt: doc1.updatedAt > matchingDoc.updatedAt ? doc1.updatedAt : matchingDoc.updatedAt
});
} else {
finalData.value.push({
idshow: doc1.idshow,
numbershow: doc1.numbershow,
ab: doc1.ab,
updatedAt: doc1.updatedAt
});
}
});
collection2Data.value.forEach(doc2 => {
if (!finalData.value.find(doc => doc.idshow === doc2.idshow)) {
finalData.value.push({
idshow: doc2.idshow,
numbershow: doc2.numbershow,
ab: doc2.ab,
updatedAt: doc2.updatedAt
});
}
});
console.log(finalData.value);
latestfinalData.value = finalData.value.filter(doc => (Date.now() - new Date(doc.updatedAt).getTime()) < 15000);
console.log(latestfinalData.value);
const filenames = computed(() => {
return latestfinalData.value.map(item => {
const digits = item.numbershow.toString().split('');
return digits.map(digit => `https://koh-samui.com/sound/${digit}.mp3`);
});
});
console.log(filenames.value);
const audioRef = ref(null);
const isPlaying = ref(false);
onMounted(() => {
const sounds = filenames.value;
let currentSound = 0;
audioRef.value = new Audio(sounds[currentSound]);
audioRef.value.addEventListener("ended", () => {
isPlaying.value = false;
currentSound++;
if (currentSound < sounds.length) {
audioRef.value.src = sounds[currentSound];
audioRef.value.play();
}
});
if (!isPlaying.value) {
isPlaying.value = true;
audioRef.value.play();
}
});
}, 2000);
});
</script>
Below is the code that I try to change but still no sound come out
<script setup>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
const collection1Data = ref([]);
const collection2Data = ref([]);
const finalData = ref([]);
const latestfinalData = ref([]);
const sounds = ref([]);
const audioRef = ref(null)
const isPlaying = ref(false)
const fetchData = async () => {
const [collection1Response, collection2Response] = await Promise.all([
axios.get('https://koh-abx.com:50100/onboardshows'),
axios.get('https://koh-abx.com:50100/onboardlands'),
]);
collection1Data.value = collection1Response.data;
collection2Data.value = collection2Response.data;
};
onMounted(async () => {
await fetchData();
setInterval(() => {
fetchData().then(() => {
finalData.value = [];
collection1Data.value.forEach(doc1 => {
const matchingDoc = collection2Data.value.find(doc2 => doc1.idshow === doc2.idshow);
if (matchingDoc) {
finalData.value.push({
idshow: doc1.idshow,
numbershow: doc1.updatedAt > matchingDoc.updatedAt ? doc1.numbershow : matchingDoc.numbershow,
ab: doc1.updatedAt > matchingDoc.updatedAt ? doc1.ab : matchingDoc.ab,
updatedAt: doc1.updatedAt > matchingDoc.updatedAt ? doc1.updatedAt : matchingDoc.updatedAt
});
} else {
finalData.value.push({
idshow: doc1.idshow,
numbershow: doc1.numbershow,
ab: doc1.ab,
updatedAt: doc1.updatedAt
});
}
});
collection2Data.value.forEach(doc2 => {
if (!finalData.value.find(doc => doc.idshow === doc2.idshow)) {
finalData.value.push({
idshow: doc2.idshow,
numbershow: doc2.numbershow,
ab: doc2.ab,
updatedAt: doc2.updatedAt
});
}
});
console.log(finalData.value);
latestfinalData.value = finalData.value.filter(doc => (Date.now() - new Date(doc.updatedAt).getTime()) < 15000);
console.log(latestfinalData.value);
});
const filenames = computed(() => {
return latestfinalData.value.map(item => {
const digits = item.numbershow.toString().split('');
return digits.map(digit => `https://koh-abx.com/sound/${digit}.mp3`);
});
});
console.log(filenames.value);
sounds.value = filenames.value ;
playSound();
}, 5000);
});
const playSound = () => {
let currentSound = 0;
audioRef.value = new Audio(sounds.value[currentSound]);
audioRef.value.addEventListener("ended", () => {
isPlaying.value = false;
currentSound++;
if (currentSound < sounds.value.length) {
audioRef.value.src = sounds.value[currentSound];
audioRef.value.play();
}
});
if (!isPlaying.value) {
isPlaying.value = true;
audioRef.value.play();
}
};
</script>
The problem with your code is that the "onMounted" hook is called twice.
The first time it is called outside the range function and the second time it is called inside.
To fix this, you need to move the audioRef and isPlaying variables and the onMounted hook that references them out of the interval function.
So the hook is only registered once and the audioRef and isPlaying variables are accessible by both the interval function and the hook.
Here the code:
<script>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
const setup = () => {
// Define reactive references to store data from API requests
const collection1Data = ref([]);
const collection2Data = ref([]);
const finalData = ref([]);
const latestfinalData = ref([]);
// Function to fetch data from two API endpoints
const fetchData = async () => {
try {
// Use Promise.all to make concurrent API requests
const [collection1Response, collection2Response] = await Promise.all([
axios.get('https://koh-abx.com:50100/onboardshows'),
axios.get('https://koh-abx.com:50100/onboardlands'),
]);
// Update the reactive references with API response data
collection1Data.value = collection1Response.data;
collection2Data.value = collection2Response.data;
} catch (error) {
// Log any errors to the console
console.error(error);
}
};
// Function to combine data from two API endpoints and filter unique values
const combineData = () => {
// Combine data from two API endpoints
finalData.value = [...collection1Data.value, ...collection2Data.value];
// Use Map to store unique data
const uniqueData = new Map();
finalData.value.forEach(doc => {
const existingDoc = uniqueData.get(doc.idshow);
if (existingDoc) {
// If the document with the same idshow exists in the Map, update it with latest data
uniqueData.set(doc.idshow, {
idshow: doc.idshow,
numbershow: existingDoc.updatedAt > doc.updatedAt ? existingDoc.numbershow : doc.numbershow,
ab: existingDoc.updatedAt > doc.updatedAt ? existingDoc.ab : doc.ab,
updatedAt: existingDoc.updatedAt > doc.updatedAt ? existingDoc.updatedAt : doc.updatedAt
});
} else {
// If the document with the same idshow does not exist in the Map, add it
uniqueData.set(doc.idshow, {
idshow: doc.idshow,
numbershow: doc.numbershow,
ab: doc.ab,
updatedAt: doc.updatedAt
});
}
});
// Convert Map values to an array
finalData.value = [...uniqueData.values()];
// Sort the array by updatedAt in descending order and store only the latest 10 items
latestfinalData.value = finalData.value.sort((a, b) => a.updatedAt > b.updatedAt ? -1 : 1).slice(0, 10);
};
// Call the fetchData function on component mount
onMounted(fetchData);
// Use computed to watch for changes to collection1Data and collection2Data and call combineData
computed(() => {
combineData();
});
return {
collection1Data,
collection2Data,
finalData,
latestfinalData,
fetchData
};
};
export default {
setup
};
</script>
I took the liberty of making some optimizations to the code

React useState can't be set in useEffect

I am learning react and trying set object from queryStr for later reuse,
can't set searchFilter in useEffect
this line prinst null:
console.log(searchFilter.transactionId)//prints null
interface TransactionSearchFilter {
transactionId?: number;
}
const TransactionList: any = (props: any) => {
const queryStr = location.search.substring(1);
const [searchFilter, setSearchFilter] = React.useState<TransactionSearchFilter>({
transactionId: null,
});
const parseQueryParams = () => {
const queryObj = QueryString.parse(queryStr);
console.log(queryObj.transactionId)//prints 10
setSearchFilter({ ...searchFilter, ...queryObj });
console.log(searchFilter.transactionId)//prints null
};
React.useEffect(() => {
parseQueryParams();
}, []);
return (<div>Hello World</div>);
}; export default TransactionList;

How to get state from custom hooks to update in "parent" component?

I am trying to separate some logic from my component into a custom hook. I feel like i'm misunderstanding some fundamentals but I thought my code would work. I basically update my state in my custom useTrip hook, and i want my map component to have that same updated state.
useTrip.js:
export const useTrip = () => {
const [businesses, setBusinesses] = useState([])
useEffect(()=>{
console.log(businesses) //prints expected results
},[businesses])
const fetchData = async (name, lat, lng) => {
const response = await fetch('http://localhost:5000/category/' + lat + "/" + lng + '/' + name)
const result = await response.json();
setBusinesses(result)
}
return { businesses, fetchData }
}
Map.js (component that uses useTrip):
export const Map= (props) => {
const {businesses} = useTrip()
return(<>
{businesses.map((.....)}
</>)
}
Parent.js (parent of map.js):
export const Parent= (props) => {
const {fetchData} = useTrip()
useEffect(() => {
fetchData(title, lat, lng)
}, [origin])
return(<>
</>)
}
The businesses is always an empty array when inside the Map component. my code was working before i started refactoring. Isnt the updated state in the custom hook suppose to be consistent across the components that use it?
You must use your custom hook on Parent component, and send the businesses to your Map component via props.
i.e.
function Parent (props) {
const { fetchData, businesses } = useTrip()
useEffect(() => {
fetchData(title, lat, lng)
}, [origin])
return (
<Map businesses={businesses} />
)
}
function Map (props) {
const { businesses } = props
return (
<>
{businesses.map(/* ... */)}
</>
)
}
If you call your custom hook on each component, they will get their own state
I have played around with this a bit, and come up with a better, solution. It is in the first code block.
import {useEffect, useState} from 'react';
import { v4 as uuidv4 } from 'uuid';
const constant_data = {
altering_var: null,
queue: {},
default_set: false
};
export const useConstantVariable = (defaultUser) => {
//set an id to a unique value so this component can be identified
const [id, setId] = useState(uuidv4());
//use this variable to force updates to screen
const [updateId, setUpdateId] = useState({});
//set the data contained in this hook
const setData = (data) => {
constant_data.altering_var = data;
};
//force an update of screen
const updateScreen = () => {
setUpdateId({...updateId});
};
//make a copy of the data so it is seen as a new constant instance
const saveData = () =>{
//if the value is an array copy the array
if(Array.isArray(constant_data.altering_var)){
constant_data.altering_var = [...constant_data.altering_var];
//if the value is an object copy it with its prototype
} else if(typeof constant_data.altering_var === 'object' && constant_data.altering_var !== null){
constant_data.altering_var = completeAssign({}, constant_data.altering_var);
} else {
//do no operation on basic types
}
}
//update all instances of this hook application wide
const updateAll = () => {
saveData();
//now get all instances and update them, remove broken links.
Object.keys(constant_data.queue).map((k)=> {
const value = constant_data.queue[k];
if (typeof value !== 'undefined' && value !== null) {
constant_data.queue[k]();
} else {
delete constant_data.queue[k]
}
return true;
});
};
//set the function to call to update this component
constant_data.queue[id] = updateScreen;
//for the first instance of this hook called set the default value.
if (typeof defaultUser !== 'undefined' && !constant_data.default_set) {
constant_data.default_set = true;
setData(defaultUser);
}
//when this component is destroyed remove all references to it in the queue used for updating.
useEffect(() => {
return () => {
delete constant_data.queue[id];
};
}, []);
//return the new variable to the constant
return [
constant_data.altering_var,
(data) => {
setData(data);
updateAll();
}
];
};
function completeAssign(target, source) {
target = Object.assign(target, source);
Object.setPrototypeOf(target, Object.getPrototypeOf(source));
return target;
}
OLD ANSWER
This is how we managed to solve this issue, it is not perfect, and I am open to suggestions for improvements. But we created a user component to share our user across the entire app.
const users = {client: {isSet: () => { return false; } } }
const instances = {client: []}
export const useClientUser = (defaultUser) => {
const [updateId, setUpdateId] = useState(uuidv4());
const setClientUser = (data) => {
users.client = new Person(data);
}
const updateScreen = () => {
setUpdateId(uuidv4());
}
useEffect(()=>{
if(defaultUser !== '' && typeof defaultUser !== 'undefined'){
setClientUser(defaultUser);
}
instances.client.push(updateScreen);
}, []);
return [users.client , (data) => { setClientUser(data);
instances.client = instances.client.filter((value)=> {
if(typeof value !== 'undefined'){ return true } else { return false }
} );
instances.client.map((value)=> {if(typeof value !== 'undefined') { value() } })
} ];
}
I have rewritten our component to show how yours would hypothetically work.
import { v4 as uuidv4 } from 'uuid';
//create super globals to share across all components
const global_hooks = {businesses: {isSet: false } }
const instances = {businesses: []}
export const useTrip = () => {
//use a unique id to set state change of object
const [updateId, setUpdateId] = useState(uuidv4());
//use this function to update the state causing a rerender
const updateScreen = () => {
setUpdateId(uuidv4());
}
//when this component is created add our update function to the update array
useEffect(()=>{
instances.businesses.push(updateScreen);
}, []);
useEffect(()=>{
console.log(global_hooks.businesses) //prints expected results
},[updateId]);
const fetchData = async (name, lat, lng) => {
const response = await fetch('http://localhost:5000/category/' + lat + "/" + lng + '/' + name)
const result = await response.json();
global_hooks.businesses = result;
global_hooks.businesses.isSet = true;
}
return {businesses: global_hooks.businesses, fetchData: (name, lat, lng) => {
//fetch your data
fetchData(name, lat, lng);
//remove update functions that no longer exist
instances.businesses = instances.business.filter((value)=> {
if(typeof value !== 'undefined'){ return true } else { return false }
} );
//call update functions that exist
instances.businesses.map((value)=> {if(typeof value !== 'undefined') { value() } })
}
};
}

Apollo GraphQL appends duplicates to component state

I have a page that contains a component that renders a list from the results of a query. When I load the page the first time, the list renders fine. But whenever I go to another page and navigate back, an additional set of the result is appended to the list, creating duplicates in the DOM.
I'm not sure what I'm doing wrong here, but I don't want a new set items to be appended to the list every time I load the page.
apolloClient (https://github.com/vercel/next.js/tree/canary/examples/with-apollo)
let apolloClient;
const createApolloClient = () =>
new ApolloClient({
ssrMode: typeof window === "undefined",
link: new HttpLink({
uri: DB_URI,
credentials: "same-origin",
}),
cache: new InMemoryCache(),
});
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
if (initialState) {
const existingCache = _apolloClient.extract();
const data = merge(initialState, existingCache);
_apolloClient.cache.restore(data);
}
if (typeof window === "undefined") return _apolloClient;
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function addApolloState(client, pageProps) {
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
}
return pageProps;
}
export function useApollo(pageProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME];
return useMemo(() => initializeApollo(state), [state]);
}
On my page I use getStaticProps as follows
export async function getStaticProps() {
const apolloClient = initializeApollo();
await apolloClient.query({
query: GET_THINGS,
});
return addApolloState(apolloClient, {
props: {},
revalidate: 1,
});
}
My list component looks as follows:
const ItemsList: React.FunctionComponent<Props> = (props) => {
const { loading, error, data } = useQuery(GET_THINGS, {});
const { items} = data;
const { filters } = props;
const [filteredItems, setFilteredItems] = useState(items);
useEffect(() => {
setFilteredItems(filterItems(items, filters));
}, [filters, items]);
const renderItems = (filteredItems: Array<Item>) =>
filteredItems.map((item) => (
<li key={item.id}>
<Link href={`/items/${item.id}`}>{item.name}</Link>
</li>
));
if (loading) return <div>"Loading...";</div>;
if (error) return <div>`Error! ${error.message}`;</div>;
return (
<div>
{filteredItems?.length > 0 ? (
<ul>{renderItems(filteredItems)}</ul>
) : (
<span>No items matched the criteria</span>
)}
</div>
);
};
export default ItemsList;

Double rendering with react custom hook

I am trying to implement custom global state hook based on the article here State Management with React Hooks — No Redux or Context API. I keep getting double renders. It seems to be with the following piece of code:
function useCustom() {
const newListener = useState()[1];
effect(() => {
this.listeners.push(newListener);
return () => {
this.listeners = this.listeners.filter(
listener => listener !== newListener
);
};
}, []);
return [this.state, this.setState, this.actions];
}
If you console log inside this piece of code you can see it running twice at initial render and also twice every time you update the hook.
Any help on how to fix this would be much appreciated.
Here is the full code:
CodeSandbox
import React, { useState, useEffect, useLayoutEffect } from "react";
const effect = typeof window === "undefined" ? useEffect : useLayoutEffect;
function setState(newState) {
if (newState === this.state) return;
this.state = newState;
this.listeners.forEach(listener => {
listener(this.state);
});
}
function useCustom() {
const newListener = useState()[1];
effect(() => {
this.listeners.push(newListener);
return () => {
this.listeners = this.listeners.filter(
listener => listener !== newListener
);
};
}, []);
return [this.state, this.setState, this.actions];
}
function associateActions(store, actions) {
const associatedActions = {};
if (actions) {
Object.keys(actions).forEach(key => {
if (typeof actions[key] === "function") {
associatedActions[key] = actions[key].bind(null, store);
}
if (typeof actions[key] === "object") {
associatedActions[key] = associateActions(store, actions[key]);
}
});
}
return associatedActions;
}
const useGlobalHook = (initialState, actions) => {
const store = { state: initialState, listeners: [] };
store.setState = setState.bind(store);
store.actions = associateActions(store, actions);
return useCustom.bind(store, React);
};
export default useGlobalHook;
Then set up the store like so:
import useGlobalState from './useGlobalState';
const initialState = false;
const useValue = useGlobalState(initialState);
export default useValue;
And the component
import React from 'react';
import useValue from '../store/useValue';
const Component1 = () => {
const [value, setValue] = useValue();
console.log('rendered component');
return (
<div>
<p>Value1: {value ? 'true' : 'false'}</p>
<button onClick={() => setValue(!value)}>Toggle Me</button>
</div>
);
};
export default Component1;

Categories