Check for existing instance of ApolloClient to clear cache? - reactjs

We have a React/GraphQL app inside of a main "app". When the user logs out we want to clear the GQL cache. However, the logout functionality exists in the wrapper app, not the React app.
When we log back in the cache is not cleared, which needs to be solved. A couple of questions on how to solve this:
1) can we check for a cache when the React app tries to create a new instance? Is there a "version" flag we can add?
const client = new ApolloClient({
link: authLink.concat(restLink),
cache: () => {
if (client.cache) {
client.cache.reset()
}
return new InMemoryCache();
}
});
2) or can we find the existing client App through any other window or global object?
3) should the react app set the client as part of local state and then compare client with useRef perhaps? If they don't match, then reset?
Open to suggestions...

The official way to clear the cache is calling resetStore in the client instance. You can get the client instance inside of any component within the Apollo context by using e.g. the useApolloClient hook
function MyLogoutButton() {
const client = useApolloClient();
const onLogout = () => {
backend.logout();
client.resetStore();
};
return <button onClick={onLogout}>Logout</button>;
}
Maybe this is doing what you want to do. You seem to be trying to create a new instance of the client, but this is not needed. The resetStore method was build for exactly this use case.

Related

Next JS - Handling getInitialProps on _app.js in SSR vs CSR

I am trying to create a Next JS application that handles the authentication and initial routing inside getInitialProps. I discovered this method can be executed either in the server or on the client.
My approach so far it's to have 2 different handlers based on detecting if I am in executing in the server checking for the presence of the req attribute inside of ctx.
This does the trick but doesn't feel like is the right way of doing. Can somebody, please, tell me if there is a cleaner way.
All authentication is handled in a separate subdomain, so I just need to redirect to the auth subdomain if there is no cookie or auth request fails for some other reason.
import "../../styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
MyApp.getInitialProps = async (appContext) => {
let cookie, user;
let ctx = appContext.ctx;
//Check if I am in the server.
if (ctx.req) {
cookie = ctx.req.headers.cookie
//Do auth request.
//Redirect base on user properties
// handle redirects using res object
ctx.res.writeHead(302, { Location: "/crear-cuenta"});
} else {
cookie = window.document.cookie;
//Do auth request.
//Redirect base on user properties
//Do redirects using client side methods (useRouter hook, location.replace)???
}
//Return pageProps to the page with the authenticted user information.
return { pageProps: { user: user } };
};
export default MyApp;
I think your code is clean enough. Of course you still can maintain it.
My suggestion would be as the followings:
MyApp.getInitialProps = async (appContext) => {
in this line you can use object destructuring technique to get the context straightforward:
MyApp.getInitialProps = async ({ ctx }) => {
then you won't need this line for example anymore : let ctx = appContext.ctx;
The most important part of your code which can be cleaned up by the way is the area that you have written your auth request twice in an if/else condition. I would suggest you to implement that part like this:
const cookie = ctx.req ? ctx.req.headers.cookie : window.document.cookie;
Although I would try to keep everything in getInitialProps on server side, In that case I make a small change to get the cookie as following and process it in server-side only.
const cookie = cookie.parse(ctx.req ? ctx.req.headers.cookie || "" : undefined);
Note that: I'm using a cookie parser which u can install the package yourself as well. (npm install cookie)
if you need to do an extra check on your cookie at client side, I will do that in componentdidmount or in case you are using react hooks in useEffect. But it is not necessary.
Now you can implement //Do auth request once, which will cause cleaner code and of course to reduce unnecessary repetition.

Catching parameters in redirect with Gatsby.js

I have one quick and dirt question.
Is it possible to catch a query parameter from a server redirect inside of Gatsby.js application?
We have a Pardot tracking link that does redirect to our thank you page which is built in Gatsby.js and I want to pass some query parameters to the application it self from that redirect.
So for example:
www.trackedlink.com/thank-you?programme_code=CODE_FROM_REDIRECT_ON_SERVERSIDE
will redirect to:
www.gatsbyapplicationthatwillreadthequery.com/thank-you?programme_code=CODE_FROM_REDIRECT_ON_SERVERSIDE
Is it possible to read that query inside of the application if it's coming from the outside of the app?
Cheers and have a great week!
If they are triggered in the client-side the redirection will be caught by the application and yes, it would possible if they are coming from outside the app or using a standard anchor. Not using a #reach/router (<Link> component since it's a limitation).
A clean and scalable way to use it is by adding in the function in your gatsby-browser.js configuration:
import React from 'react';
import { checkUrlFunction } from './src/services/yourCheckUrlFunction';
export const onClientEntry = () => checkUrlFunction();
Adding a function in gatsby-browser.js with onClientEntry API will trigger your function once the page is loaded. From the documentation:
onClientEntry Function (_: emptyArg, pluginOptions: pluginOptions) => undefined Called when the Gatsby browser runtime first starts.
Your function should look like:
export const checkUrlFunction = () => {
if (typeof window !== 'undefined') {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const programmeCode= urlParams.get('programme_code')
if(programmeCode) window.localStorage.setItem('programmeCode', programmeCode)
console.log(programmeCode); // will output CODE_FROM_REDIRECT_ON_SERVERSIDE
};
};
Note the typeof window !== 'undefined' necessary to avoid issues if the window object is not defined when triggering the function
Hi Ferran, thank you for your solution but unfortunately, it does not
work when the redirect happens. It only works if the query string is
inside of the application
Yes, the idea of adding the function in gatsby-browser.js is to avoid the addition of checkUrlFunction() in each page, template, or component. The disadvantage is that you lose a bit of control but it saves a lot of overwriting code and improves the scalability and readability.
Thanks, Ferran, if you could show me an example of it - it would be
amazing! This cookie topic is sort of unknown water for me
So, with your specifications updated, I've added the localStorage approach since it's easier to achieve in a non-IDE environment like this, but the idea is exactly the same.
Set a vault (cookie or localStorage) automated in the gatsby-browser.js function
if(programmeCode) window.localStorage.setItem('programmeCode', programmeCode)
This sets a localStorage key/value pair ('programmeCode' (key)/programmeCode (value)
Access to that vault in your component. Use a componentDidMount lifecycle or useEffect hook to ensure that is loaded before the DOM tree is mounted.
useEffect(()=>{
if(typeof window !== undefined) console.log(window.location.getItem('programmeCode')
}, [])

How to connect to Laravel Websocket with React?

I'm building an ordering app, where I do the backend with Laravel and the front end with both ReactJS and React Native
I want real-time updates whenever a customer posts an order and whenever an order gets updated.
Currently, I managed to get WebSocket running that uses the pusher API using devmarketer his tutorial.
I'm successful in echoing the order in the console, but now I want to access the channel using my react app
And at this step is where I am facing difficulties.
As I'm unsure how to create a route that is accessible to both my apps and how to access the channel through this route.
The official laravel documentation gives an example of how to access pusher but not how to connect to it with for example an outside connection (example: my react native app)
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'rapio1',
host: 'http://backend.rapio',
authEndpoint: 'http://backend.rapio/broadcasting/auth',
auth: {
headers: {
// Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
}
// encrypted: true
});
window.Echo.channel('rapio.1').listen('OrderPushed', (e) =>{
console.log(e.order)
})
So my question is how can I access a broadcasting channel on my React apps?
ADDED BACKEND EVENT
class OrderPushed implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $neworder;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(Order $neworder)
{
$this->neworder = $neworder;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
//return new Channel('Rapio.'.$this->neworder->id);
return new Channel('Rapio');
}
public function broadcastWith()
{
return [
'status' => $this->neworder->status,
'user' => $this->neworder->user->id,
];
}
}
Are you using the broadcastAs() method on the backend?
It's important to know this in order to answer your question properly because if you are, the Laravel echo client assumes that the namespace is App\OrderPushed.
When using broadcastAs() you need to prefix it with a dot, to tell echo not to use the namespacing so in your example, it would be:
.listen('.OrderPushed')
Also, you don't need to do any additional setup on the backend in order for each client application to connect to the socket server unless you want to have a multi-tenancy setup whereby different backend applications will make use of the WebSockets server.
I also use wsHost and wsPort instead of just host and port, not sure if that makes a difference though
If you can access the data on the frontend by simply console.log'ing to the console you should already be most of the way there.
The way you would actually get the data into your react components depends on if you're using a state management library (such as redux) or just pure react.
Basically, you would maintain a local copy of the data on the frontend and then use the Echo events to update that data. For example, you could have a list of orders in either redux, one of your react components, or somewhere else, that you could append to and modify based on creation, update, and deletion events.
I would personally create an OrderCreated, OrderUpdated, and OrderDeleted event on the backend that would contain the given order model.
class OrdersList extends React.Component {
componentDidMount() {
this.fetchInitialDataUsingHttp();
//Set up listeners when the component is being mounted
window.Echo.channel('rapio.1').listen('OrderCreated', (e) =>{
this.addNewOrder(e.order);
}).listen('OrderUpdated', (e) =>{
this.updateOrder(e.order);
}).listen('OrderDeleted', (e) =>{
this.removeOrder(e.order);
});
}
componentWillUnmount() {
//#TODO: Disconnect echo
}
}

Create-react-app reload on service worker update

I want to modify create-react-app service worker file and implement popup message which will ask user to update app if newer service worker is ready to be activated. I'm almost done with the solution but have one pitfall. I want to reload the app when user confirms service worker update popup, so I've added some coded to the end of register function, see below:
export default function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker."
)
})
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl, config)
}
let preventDevToolsReloadLoop
navigator.serviceWorker.addEventListener("controllerchange", function() {
// ensure refresh is called only once
if (preventDevToolsReloadLoop) {
return
}
preventDevToolsReloadLoop = true
console.log("reload")
window.location.reload(true)
})
})
}
}
But the problem is that it reloads the app also on first visit, when there doesn't exist any service worker yet. How can I solve it?
Update to react-scripts ^3.2.0. Verify that you have the new version of serviceWorker.ts or .js. The old one was called registerServiceWorker.ts and the register function did not accept a configuration object. Note that this solution only works well if you are Not lazy-loading.
then in index.tsx:
serviceWorker.register({
onUpdate: registration => {
alert('New version available! Ready to update?');
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}
});
The latest version of the ServiceWorker.ts register()function accepts a config object with a callback function where we can handle upgrading. If we post a message SKIP_WAITING this tells the service worker to stop waiting and to go ahead and load the new content after the next refresh. In this example I am using a javascript alert to inform the user. Please replace this with a custom toast.
The reason this postMessage function works is because under the hood CRA is using workbox-webpack-plugin which includes a SKIP_WAITING listener.
More About Service Workers
good guide: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68
CRA issue discussing service worker cache: https://github.com/facebook/create-react-app/issues/5316
If you are not using CRA, you can use workbox directly: https://developers.google.com/web/tools/workbox
Completing #jfbloom22's answer:
As you probably want to ask the user after an update has been detected with something more complex than a plain alert, you need to ensure the registration object is available from inside the React's components tree and save it to use after the user accepts to update (for example, by clicking a button).
As an option, in a component you can create a custom event listener on a global object like document and fire this event when the onUpdate callback passed to serviceWorker.register(), passing to it the resgistration object as extra data.
This is exactly what my recently published Service Worker Updater does (some self-promotion). To use it you just need to:
Add it to the dependencies:
yarn add #3m1/service-worker-updater
Use it in your index.js:
import { onServiceWorkerUpdate } from '#3m1/service-worker-updater';
// ...
// There are some naming changes in newer Create React App versions
serviceWorkerRegistration.register({
onUpdate: onServiceWorkerUpdate
});
Use it in some of your React components:
import React from 'react';
import { withServiceWorkerUpdater } from '#3m1/service-worker-updater';
const Updater = (props) => {
const {newServiceWorkerDetected, onLoadNewServiceWorkerAccept} = props;
return newServiceWorkerDetected ? (
<>
New version detected.
<button onClick={ onLoadNewServiceWorkerAccept }>Update!</button>
</>
) : null; // If no update is available, render nothing
}
export default withServiceWorkerUpdater(Updater);
Try to add reload funtion in installingWorker.state === 'installed'
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// do something ..
}
}

How to pass a web worker instance to another component in react js?

Is it possible to pass a web worker instance to another component in react js?
If I start a web worker in one component and then I navigate to another component, should I terminate the previous worker and start a new worker? What are the best practices?
How do I listen to post message event of a web worker in two different react components?
Scenario:
There is a search input and a search button. As soon as user starts typing in the input, after 2 sec delay, I start a web worker in the background to get data from a remote server. When results are back, it is shown in the suggestions below the input. But if the user clicks the search button, before the worker has finished, it navigates to second component, then how do I access the worker instance in the second component?
You could pass a reference to a function which contains the worker functions which child component can make use of. Here's an example of how a parser worker might be setup:
const ComponentWithWorker = ()=>{
const workerRef = useRef();
useEffect(() => {
workerRef.current = new Worker(new URL('web-workers/parserWorker.js', import.meta.url));
() => {
workerRef.current.terminate();
};
}, []);
}
const parserWorker(stuff)=>{
worker.postMessage({content: stuff})
worker.onMessage((data)=>{...})
}
return (
<ParserContext.Provider value={{ parser: parseWorker }}>
...
</ParserContext.Provider>
)
}

Resources