session.subscribe throws error when called in onMount - sveltekit

<script>
import {onMount} from 'svelte';
import {session} from "$app/stores"
import {writable} from 'svelte/store';
const store = writable('some value');
let value = null
onMount(() => {
// this works
// return store.subscribe( (storeValue) => {value = storeValue}); // this works
// this throws an error:
// return session.subscribe( (sessionValue) => {value = sessionValue});
// Uncaught (in promise) Error: Function called outside component initialization
});
</script>
can someone please explain to me the problem with session.subscribe and why it keeps throwing?
if I move session.subscribe outside onMount it runs fine.
Note: this code is part of a SvelteKit Project, inside a Svelte component, not a SvelteKit page/route.

What goes wrong
It seems that you are actually experiencing intended behaviour. Under the documentation for $app/stores you will find this:
Stores are contextual — they are added to the context of your root component. This means that session and page are unique to each request on the server, rather than shared between multiple requests handled by the same server simultaneously, which is what makes it safe to include user-specific data in session.
Because of that, you must subscribe to the stores during component initialization (which happens automatically if you reference the store value, e.g. as $page, in a component) before you can use them.
When you were attempting this, you probably got a callstack that looks something like this:
Error: Function called outside component initialization
at get_current_component (index.mjs:953:15)
at getContext (index.mjs:989:12) <----------Here is the problem
at getStores (stores.js:19:17)
at Object.subscribe (stores.js:70:17)
at index.svelte:10:13
at run (index.mjs:18:12)
at Array.map (<anonymous>)
at index.mjs:1816:45
at flush (index.mjs:1075:17)
at init (index.mjs:1908:9)
We can see that Svelte attempts to call getContext when you subscribe to the session. Calling getContext outside of the component root is not allowed, which causes the subscription to fail.
I agree that this is quite unintuitive and I am not really sure why they implemented it this way.
Workaround
By the way, are you really sure you only want to subscribe to session on mount? What are you trying to do?
If you really only want to subscribe to session after component mount, you could use this workaround: Create your own store that updates whenever the session changes, then listen to that.
<script>
import { onMount } from "svelte";
import { session } from "$app/stores";
import { writable } from "svelte/store";
let mySession = writable($session);
$: $mySession = $session;
onMount(()=>{
mySession.subscribe(...whatever...);
})
</script>

Related

Handling OAuth with React 18 useEffect hook running twice

Background
I have recently upgraded a fairly sizeable React app to React 18 and for the most part it has been great. One of the key changes is the new double mount in development causing useEffect hooks to all run twice, this is clearly documented in their docs.
I have read their new effect documentation https://beta.reactjs.org/learn/lifecycle-of-reactive-effects and although it is quite detailed there is a use case I believe I have found which is not very well covered.
The issue
Essentially the issue I have run into is I am implementing OAuth integration with a third-party product. The flow:
-> User clicks create integration -> Redirect to product login -> Gets redirected back to our app with authorisation code -> We hit our API to finalise the integration (HTTP POST request)
The problem comes now that the useEffect hook runs twice it means that we would hit this last POST request twice, first one would succeed and the second would fail because the integration is already setup.
This is not potentially a major issue but the user would see an error message even though the request worked and just feels like a bad pattern.
Considered solutions
Refactoring to use a button
I could potentially get the user to click a button on the redirect URL after they have logged into the third-party product. This would work and seems to be what the React guides recommend (Although different use case they suggested - https://beta.reactjs.org/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers).
The problem with this is that the user has already clicked a button to create the integration so it feels like a worse user experience.
Ignore the duplicate API call
This issue is only a problem in development however it is still a bit annoying and feels like an issue I want to explore further
Code setup
I have simplified the code for this example but hopefully this gives a rough idea of how the intended code is meant to function.
const IntegrationRedirect: React.FC = () => {
const navigate = useNavigate();
const organisationIntegrationsService = useOrganisationIntegrationsService();
// Make call on the mount of this component
useEffect(() => {
// Call the method
handleCreateIntegration();
}, []);
const handleCreateIntegration = async (): Promise<void> => {
// Setup request
const request: ICreateIntegration = {
authorisationCode: ''
};
try {
// Make service call
const setupIntegrationResponse = await organisationIntegrationsService.createIntegration(request);
// Handle error
if (setupIntegrationResponse.data.errors) {
throw 'Failed to setup integrations';
}
// Navigate away on success
routes.organisation.integrations.navigate(navigate);
}
catch (error) {
// Handle error
}
};
return ();
};
What I am after
I am after suggestions based on the React 18 changes that would handle this situation, I feel that although this is a little specific/niche it is still a viable use case. It would be good to have a clean way to handle this as OAuth integration is quite a common flow for integration between products.
You can use the useRef() together with useEffect() for a workaround
const effectRan = useRef(false)
useEffect(() => {
if (effectRan.current === false) {
// do the async data fetch here
handleCreateIntegration();
}
//cleanup function
return () => {
effectRan.current = true // this will be set to true on the initial unmount
}
}, []);
This is a workaround suggested by Dave Gray on his youtube channel https://www.youtube.com/watch?v=81faZzp18NM

How to access the React Query `queryClient` outside the provider? e.g. to invalidate queries in Cypress tests

Is it possible to access my app's React Query queryClient outside React?
In my some of my Cypress tests I fetch or mutate some data but then I'd like to invalidate all/part of the queryClient cache afterwards.
Is that possible?
I have tried importing the "same" queryClient that is used in the app, but it doesn't work.
ℹ️ I include these fetch/mutations in my tests merely to allow me to bypass convoluted steps that user's would normally take in the app which already have Cypress tests.
You can add a reference to queryClient to window, then invoke it's methods in the test.
From #АлексейМартинкевич example code,
import { QueryClient } from "react-query";
const queryClient = new QueryClient();
if (window.Cypress) { // only during testing
window.queryClient = queryClient;
}
export queryClient;
In the test
cy.window().then(win => { // get the window used by app (window in above code)
win.queryClient.invalidateQueries(...) // exact same instance as app is using
})
I believe that importing will give you a new instance, you must pass a reference to the app's active instance.
Just read your comment which says exactly this - leaving answer as a code example.
You can store query client as a separate module/global variable. demo

NextJS environment variable undefined in API route

I'm using NextJS v 10.0.9. I have created an .env.development.local file in the root of my project, as described in the docs. Contents:
API_SERVER=http://127.0.0.1:5000/map/abc123
In an API route:
pages/api/objects.js
export function getObjects() {
console.log(process.env.API_SERVER)
}
But when I run the application with next dev, this prints undefined. I have restarted my server numerous times since defining/changing the variable, but always get undefined. I've also tried adding a NEXT_PUBLIC_ prefix, which shouldn't be necessary and isn't desired, but I wanted to see what would happen. Result: no change. I've also tried using .env.local.
What am I doing wrong?
Update: I found this line in the docs on API routes: "For an API route to work, you need to export a function as default (a.k.a request handler), which then receives the following parameters: req, res."
So I've modified the code in my API route to:
export default function getObjects(req, res) {
console.log("test ", process.env.API_SERVER)
}
But it still isn't working. I do not have a next.config.js file because, as I understand it, this is no longer necessary as of NextJS 9.4.
I have also tried declaring and using an entirely new variable in the file (TEST=value), but this is also undefined when I try to use it in the API route.
The issue was how I was calling the API route. When I visited it directly in the browser, the environment variable was available and printed in the console just fine. But I had been following a tutorial that wasn't written for NextJS, and it had me import the API functions directly:
// WRONG WRONG WRONG
import getObjects from './api/objects'
// [...]
export default function MyApp({}) {
useEffect(() => {
getObjects().then(data => {
// do stuff
}
})
}

Jest test for a copy to clipboard method using react with typescript

I am trying to ensure that the right value is copied to the users clipboard when they click a button. This is my copy method. I am using a ref on the input to access the right value.
protected copyToClipboard() {
console.log("clicked!");
const text = this.controls.copyData;
if (!_.isNil(text)) {
text.current.focus();
text.current.select();
document.execCommand("copy");
this.setState({copied: true});
}
}
For my test:
test("Ensure right value is copied to clipboard", () => {
const wrapper = mount(<MyComponent />);
const copyButton = wrapper.find(".copyBtn");
copyButton.simulate("click");
const copyToClipboardSpy = jest.spyOn(document as any, "execCommand");
wrapper.update();
expect(copyToClipboardSpy).toHaveBeenCalledWith("copy");
});
The error I receive when I run the test is TypeError: document.execCommand is not a function which makes sense, but I am unsure how to approach this.
I am relatively new to testing, just to put that out there. I also have read that I may not be able to access the document.execCommand but have struggled to find a good alternative to hijack the test and access the value being copied. I appreciate any advice that can be given on the matter!
Posting this in case anyone else was in a similar boat. It's doesn't necessarily check the value yet, but one piece I managed was with the document.execCommand method.
I set up a mock function above the wrapper:
document.execCommand = jest.fn();
With this, the test stopped throwing the TypeError. Then my expectations included checking for the spy to have been called, expect my copy state to have changed to true, and:
expect(document.execCommand).toHaveBeenCalledWith("copy");
Test passes! A possible solution for the value is to see if I can "paste" the value and then check it. Will edit this response if/when I can manage that
When you use navigator.clipBoard.writeText instead of using document.exec("copy"), you can refer to this thread for an elegant solution that lets you assert on the content as well.
Being execCommand no longer an option as it is deprecated (see MDN), you should be using navigator.clipboard.writeText('your copied data');.
To mock navigator.clipboard, you could do the following:
// It's important to keep a copy, so your tests don't bleed
const originalClipboard = navigator.clipboard;
const mockedWriteText = jest.fn();
navigator.clipboard = {
writeText: mockedWriteText,
};
const copyComponent = await screen.findByTestId('copy-component');
await userEvent.click(copyComponent);
expect(mockedWriteText).toHaveBeenCalledTimes(1);
expect(mockedWriteText).toHaveBeenCalledWith('your copied data');
// Remember to restore the original clipboard
navigator.clipboard = originalClipboard;
jest.resetAllMocks();
You can also do Object.assignProperty instead of directly modifying the navigator object.
This snippet assumes you are using React Testing Library with User Event.

share() vs ReplaySubject: Which one, and neither works

I'm trying to implement short-term caching in my Angular service -- a bunch of sub-components get created in rapid succession, and each one has an HTTP call. I want to cache them while the page is loading, but not forever.
I've tried the following two methods, neither of which have worked. In both cases, the HTTP URL is hit once for each instance of the component that is created; I want to avoid that -- ideally, the URL would be hit once when the grid is created, then the cache expires and the next time I need to create the component it hits the URL all over again. I pulled both techniques from other threads on StackOverflow.
share() (in service)
getData(id: number): Observable<MyClass[]> {
return this._http.get(this.URL)
.map((response: Response) => <MyClass[]>response.json())
.share();
}
ReplaySubject (in service)
private replaySubject = new ReplaySubject(1, 10000);
getData(id: number): Observable<MyClass[]> {
if (this.replaySubject.observers.length) {
return this.replaySubject;
} else {
return this._http.get(this.URL)
.map((response: Response) => {
let data = <MyClass[]>response.json();
this.replaySubject.next(data);
return data;
});
}
}
Caller (in component)
ngOnInit() {
this.myService.getData(this.id)
.subscribe((resultData: MyClass[]) => {
this.data = resultData;
},
(error: any) => {
alert(error);
});
}
There's really no need to hit the URL each time the component is created -- they return the same data, and in a grid of rows that contain the component, the data will be the same. I could call it once when the grid itself is created, and pass that data into the component. But I want to avoid that, for two reasons: first, the component should be relatively self-sufficient. If I use the component elsewhere, I don't want to the parent component to have to cache data there, too. Second, I want to find a short-term caching pattern that can be applied elsewhere in the application. I'm not the only person working on this, and I want to keep the code clean.
Most importantly, if you want to make something persistent even when creating/destroying Angular components it can't be created in that component but in a service that is shared among your components.
Regarding RxJS, you usually don't have to use ReplaySubject directly and use just publishReplay(1, 10000)->refCount() instead.
The share() operator is just a shorthand for publish()->refCount() that uses Subject internally which means it doesn't replay cached values.

Resources