Apollo Client version 3 cache update delay - javascript

The React application that I have is using location.pathname to send a request to my graphql endpoint using useQuery It means whenever I change the route, it sends a request to server to receive the new data. The issue I have is while it's in loading state I would like to keep current data so it should act kind of like Youtube when you click on a video. There should be a progress bar on top of the page and when the data is received, then I will display the fresh new data.
But after migrating from version 2 to version 3 I lost this feature. Now it redirects to new page without data and after a few sec when the data is received, it displays the new data.
How can I keep current cache state while I'm sending a request to server?
BTW I have also SSR.
Below is my setup:
server.ts
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
ssrMode: true,
link: from([errorLink, httpLink]),
cache,
typeDefs,
ssrForceFetchDelay: 100,
});
client
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
link: from([errorLink, httpLink]),
cache: cache.restore(apolloState || {}),
typeDefs,
});
const usePage = <T = Record<'page', Page>>(options?: Options): QueryResult<T> => {
const { pathname } = useLocation();
return useQuery<T>(getQuery(options?.fragmentName, options?.fragment), {
variables: {
pathname: options?.pathname || pathname,
},
...(options?.queryOptions || {}),
errorPolicy: 'all',
});
};
App
const App = () => {
const content = usePage();
if (content.loading) {
return <ProgressBar />;
}
if (!content) {
return <NoContent action={pageContent.refetch} />;
}
return <Page data={content.data} />
}

I figured out that there's new property called previousData so that we can keep track of the last data that is received. So that we will have something like:
const {previousData, data = previousData,} = useQuery()
data?.doSthHere
You can also check this link:
https://github.com/apollographql/apollo-client/issues/6603#issuecomment-700282431

Related

Function onLoad in SPA Page

I have SPA page, all work very good but when user reload page beeing on winners or garage get info :
Cannot GET /Garage. Then have to pick default url. How to set reload function on current page.
https://darogawlik-async-race-api.netlify.app/ (my app)
const navigateTo = url => {
history.pushState(null, null, url)
router()
}
const router = async () => {
const routes = [
{ path: '/Garage', view: garage },
{ path: '/Winners', view: winners },
]
// Test each route for potential match
const potentialMatches = routes.map(route => ({
route,
isMatch: location.pathname === route.path,
}))
let match = potentialMatches.find(potentialMatches => potentialMatches.isMatch)
if (!match) {
match = {
route: routes[0],
isMatch: true,
}
}
const view = new match.route.view(document.querySelector('#main'))
}
window.addEventListener('popstate', router)
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', e => {
if (e.target.matches('[data-link]')) {
e.preventDefault()
navigateTo(e.target.href)
}
})
router()
})
window.addEventListener('load', router())
This will be a problem with default document handling in the web host - it is not a page load problem. Eg just click this link to get the problem:
https://darogawlik-async-race-api.netlify.app/Garage
Since you are using path based routing, your web host must serve the default document for all paths, including /Garage and /Winners. As an example, in Node.js Express you write code like this. For other web hosts you either write similar code or there is a configuration option that will do it for you.
// Serve static content for physical files, eg .js and .css files
expressApp.use('/', express.static());
// Serve the index.html for other paths
expressApp.get('*', (request, response) => {
response.sendFile('index.html');
}
According to this post on Netlify, you can add a file something like this. I'm no expert on this platform, but hopefully this gives you the info you need to resolve your issue:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

Using multiple endpoints in Apollo Client

this is my first discussion post here. I have learned Apollo + GraphQL through Odyssey. Currently, I am building my own project using Next.js which required fetching data from 2 GraphQL endpoints.
My problem: How can I fetch data from multiple GraphQL endpoints with ApolloClient?
Below is my code for my first endpoint:
import { ApolloClient, InMemoryCache, createHttpLink } from "#apollo/client";
const client = new ApolloClient({
ssrMode: true,
link: createHttpLink({
uri: "https://api.hashnode.com/",
credentials: "same-origin",
headers: {
Authorization: process.env.HASHNODE_AUTH,
},
}),
cache: new InMemoryCache(),
});
export default client;
What you are trying to accomplish is kinda against Apollo's "One Graph" approach.
Take a look at gateways and federation - https://www.apollographql.com/docs/federation/
With that being said, some hacky solution is possible but you will need to maintain a more complex structure and specify the endpoint in every query, which undermines the built-in mechanism and might cause optimization issues.
//Declare your endpoints
const endpoint1 = new HttpLink({
uri: 'https://api.hashnode.com/graphql',
...
})
const endpoint2 = new HttpLink({
uri: 'endpoint2/graphql',
...
})
//pass them to apollo-client config
const client = new ApolloClient({
link: ApolloLink.split(
operation => operation.getContext().clientName === 'endpoint2',
endpoint2, //if above
endpoint1
)
...
})
//pass client name in query/mutation
useQuery(QUERY, {variables, context: {clientName: 'endpoint2'}})
This package seems to do what you want: https://github.com/habx/apollo-multi-endpoint-link
Also, check the discussion here: https://github.com/apollographql/apollo-client/issues/84
Encountered the same problem today. I wanted to have it dynamic so this is what I came out with:
export type DynamicLinkClientName = "aApp" | "bApp" | "graphqlApp";
type Link = RestLink | HttpLink;
type DynamicLink = { link: Link; name: DynamicLinkClientName };
const LINK_MAP: DynamicLink[] = [
{ link: aRestLink, name: "aApp" },
{ link: bAppRestLink, name: "bApp" },
{ link: graphqlAppLink, name: "graphqlApp" },
];
const isClientFromContext = (client: string) => (op: Operation) =>
op.getContext().client === client;
const DynamicApolloLink = LINK_MAP.reduce<ApolloLink | undefined>(
(prevLink, nextLink) => {
// When no name is specified, fallback to defaultLink.
if (!prevLink) {
return ApolloLink.split(
isClientFromContext(nextLink.name),
nextLink.link,
defaultLink
);
}
return ApolloLink.split(
isClientFromContext(nextLink.name),
nextLink.link,
prevLink
);
},
undefined
) as ApolloLink;

NextJS: Failed when fallback set to true

I am using vercel for NextJS and this is my setup in getStaticPaths
const paths = posts.map((post) => ({
params: { player: post.player, id: post.id },
}))
return { paths, fallback: true }
When I set the fallback to true, I have got this error in vercel:
21:55:01.736 info - Generating static pages (1752/1752)
21:55:01.736 > Build error occurred 21:55:01.739 Error: Export
encountered errors on following paths: 21:55:01.739
/clip/[player]/[id]
It is ok when fallback is set to false but I really like to set fallback set to true so that pages can be updated frequently. Any help will be greatly appreciated...
Inside your /clip/[player]/[id].js file, you need to handle the fallback state when that page is being requested on-demand.
// pages/posts/[id].js
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
// If the page is not yet generated, this will be displayed
// initially until getStaticProps() finishes running
if (router.isFallback) {
return <div>Loading...</div>
}
// Render post...
}
// This function gets called at build time
export async function getStaticPaths() {
return {
// Only `/posts/1` and `/posts/2` are generated at build time
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
// Enable statically generating additional pages
// For example: `/posts/3`
fallback: true,
}
}
// This also gets called at build time
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
// Pass post data to the page via props
return {
props: { post },
// Re-generate the post at most once per second
// if a request comes in
revalidate: 1,
}
}
export default Post
https://nextjs.org/docs/basic-features/data-fetching#fallback-true
What I did was conditionally render my component. So, my component receives the object data and if I need to use a value from data, such as "title", I will do...
data?.title
Also, for my entire return component I will conditionally render it. For example...
{data !== undefined ? (
<div className ='main-content'>
<p> This is the content that I want rendered if data is defined </p>
</div>
) : (
<div className = 'fallback-content'>
<p> This shows if data == undefined </p>
</div>
)

Server Side Render Dynamic Page based on Route Param

I'm starting with Next.js and after going through docs, I cannot figure out how to get the route param code inside getStaticPaths method as shown below!?. code is not known before hand by any means and it can be anything.
I don't want to call api and get the data using useEffect inside the component.
File: pages/post/[code].js
import React from 'react';
import apiCall from 'api/something';
export default ({post}) => {
return <>
render components here based on prop `post`
</>
}
export async function getStaticPaths() {
// How to get [code] from the route here, which can be used below?
return {
paths: // NEED [code] HERE from current route,
fallback: false
}
}
export async function getStaticProps(ctx) {
return {
props: {
// [ctx.code] resolved from current route with the help of getStaticPaths,
post: apiCall(ctx.code)
}
}
}
I've tried getServerSideProps which works for me:
export const getServerSideProps = async (ctx) => {
return {
props: {
post: await apiCall(ctx.query.code)
}
};
};
But it fails when I do next export stating:
pages with getServerSideProps can not be exported. See more info here: https://err.sh/next.js/gssp-export
After investigating further on this error I found this solution, which is not feasible for me as my app is hosted on Heroku.
I'm trying to server-side render the html along with the data based on the route param code. But not able to do so now.
The purpose of the function getStaticPaths is to generate a list of paths for which static HTML will be rendered at build time. For example, for a list of 10 posts, you can generate 10 posts/[id] routes ahead of time if you know the id of the posts.
How getStaticPaths works with dynamic routes in more details..
Suppose you have a dynamic route /posts/[postId] if you choose to use static-generation you have to generate a list of paths that will include the postId as a route param and for each path returned, the function getStaticProps will be called to query the data at build time. Example,
// for /post/[postId]
export const getStaticPaths = async () => {
// if you know all the postId ahead of time
const paths = [
{ params: { postId: '1234' } }, // keep in mind postId has to be a string
{ params: { postId: '3792' } },
{ params: { postId: '1749' } },
]
return {
paths,
fallback: false // we are disabling fallback because we know all the paths ahead of time
}
}
// for each path returned getStaticProps will be called at build time
export const getStaticProps = async (context) => {
// you have access to the postId params that you returns from
// getStaticPaths here
const postId = context.params.postId
// now you can query the data from postId and return as props
return {
props: // queried data
}
}
If fallback is set to false any for any route path that is not returned from the function getStaticPaths nextjs will simply show a 404 error page.
How to use fallback: true to generate static pages for route params not known ahead of time
If you know some postId of the posts and the data for the posts do not change very often, you can choose to generate the pages with fallback property set to true, which will display a fallback version of the page for the paths that are not returned from the function getStaticPaths. And on request for the page nextjs will call getStaticProps and send the data as JSON which will be used to render the page in the browser.
Example,
// for /post/[postId]
export const getStaticPaths = async () => {
// you can get how many ever postIds are know ahead of time
// and return as paths with fallback set to true
const posts = // queried data from db or fetched from remote API
const paths = posts.map(post => { params:{ postId: post.id.toString() }})
return {
paths,
fallback: true
}
}
// in your page Component check for fallback and render a loading indicator
import { useRouter } from 'next/router';
const MyPage = (props) => {
// before you do anything
const router = useRouter();
if (router.isFallback) {
return <div>Loading....</div>
}
// rest of your page logic
}
If your data is very dynamic, let's say changing every 30mins or an hour or so. You can choose to use server-side rendering which will fetch the data on per request basis, but TTFB(time to first byte) will be higher. For example,
// for /post/[postId]
export const getServerSideProps = async (context) => {
// you also have access to the param postId from the context
const postId = context.params.postId
// query the data based on the postId and return as props
return {
props: // queried data
}
}
Keep in mind if you choose to go with getServerSideProps the function will be called on per-request basis so time to first byte will be higher.
Depending on use-cases you can also use static generation with client-side data fetching using swr from nextjs team repo link.
As I understand, you want to statically generate dynamic routes at build time.
To do so you need to let Next.js know what pages to generate, by specifying all codes.
export async function getStaticPaths() {
// you don't need here a code from current route
// but you need to specify all known post codes
return {
paths: [
{ params: { code: '1' } },
{ params: { code: '2' } },
{ params: { code: '3' } },
]
fallback: false
}
}
You would need to re-build app every time you change the posts.
Use getServerSideProps if you don't want to re-build project every time. Then the data would be fetched at request time. You can't export it because it requires Node.js server.

ReactJS server-side rendering, setTimeout issue

On my ReactJS App I used setTimeout to defer some Redux action:
export const doLockSide = (lockObject) => (dispatch) => {
const timerId = setTimeout(() => {
dispatch({
type: CONSTANTS.TOPICS_SET_CURRENT_TOPIC_LOCKED_SIDE,
payload: { id: lockObject.topicId, side: lockObject.side, locked: false }
});
}, lockObject.unlockTimeout);
dispatch({
type: CONSTANTS.TOPICS_SET_CURRENT_TOPIC_LOCKED_SIDE,
payload: { id: lockObject.topicId, side: lockObject.side, timerId, locked: true }
});
};
The lockObject comes from the server, so this code is a part of async Redux actions chain. It worked fine, but when I tried to make this functionality to be a part of server side rendering process, it broke the App. I understand the difference between Browser and NodeJS runtime environments and the difference between its implementations of setTimeout. Specifically my timerId could not be processed by Node due to it's an object, while my Redux reducer treats it as an integer. But the main problem is that during server side rendering Node fires setTimeout callback on the server side...
The question. I have some redux-based proccess that should be deferred in some cases including the App start. How can I do it satisfying the requirement of server-side rendering?
After some research I was able to apply the following approach.
1) Push the deferred action data into some special storage in case of server-side rendering, and run it "as is" in case of Browser:
import { _postRender } from '../utils/misc';
const doLockSideUI = (dispatch, lockObject) => {
// the body of previous version of doLockSide inner function
const timerId = setTimeout(() => {/*...*/}, lockObject.unlockTimeout);
dispatch(/*...*/);
};
export const doLockSide = (lockObject) => (dispatch) => {
if(typeof window === 'undefined') { // server-side rendering case
_postRender.actions.push({
name: 'doLockSide',
params: lockObject
});
}
else { // Browser case
doLockSideUI(dispatch, lockObject);
}
};
Where utils/misc.js has the following entity:
// to run actions on the Client after server-side rendering
export const _postRender = { actions: [] };
2) On the server I've imported that _postRender object form utils/misc.js and pushed it to render parameters when all redux-store data dependencies had been resolved:
const markup = renderToString(/*...*/);
const finalState = store.getState();
const params = { markup, finalState, postRender: { ..._postRender } };
_postRender.actions = []; // need to reset post-render actions
return res.status(status).render('index', params);
_postRender.actions has to be cleaned up, otherwise _postRender.actions.push from p.1 will populate it again and again each time the Client had been reloaded.
3) Then I provided my post-render actions the same way as it is done for preloaded state. In my case it is index.ejs template:
<div id="main"><%- markup %></div>
<script>
var __PRELOADED_STATE__ = <%- JSON.stringify(finalState) %>;
var __POST_RENDER__ = <%- JSON.stringify(postRender) %>;
</script>
4) Now I need to call my __POST_RENDER__ actions with given params. For this purpose I updated my root component's did-mount hook and dispatch an additional action which handles the post-render action list:
componentDidMount() {
console.log('The App has been run successfully');
if(window.__POST_RENDER__ && window.__POST_RENDER__.actions.length) {
this.props.dispatch(runAfterRender(window.__POST_RENDER__.actions));
}
}
Where runAfterRender is a new action that is being imported from ../actions/render:
import { doLockSide } from './topic'
export const runAfterRender = (list) => (dispatch) => {
list.forEach(action => {
if(action.name === 'doLockSide') {
dispatch(doLockSide(action.params));
}
// other actions?
});
};
As you can see, it's just a draft and I was forced to import doLockSide action from p.1 and call it explicitly. I guess there may be a list of possible actions that could be called on the Client after server-side rendering, but this approach already works. I wonder if there is a better way...

Categories