Electrode doesn't show dynamic data in page source - reactjs

Using electrode, I noticed this weird behaviour -
When I view the page source after the page fully loads with all the api calls and data, I am only able to view the content that is static for example, the hyper links, headings, footer links etc.
I have created a custom token handler which checks the context object and populates the custom tokens present in the index.html.
So, whenever, I console.log(context.user.content), only the data that is static such as hyperlinks, headings, footer links are logged.
I guess this is the problem but I am not able to wrap my head around as to why electrode doesn't recognise the content being rendered dynamically.
Token-Handler.js file
import Helmet from 'react-helmet';
const emptyTitleRegex = /<title[^>]*><\/title>/;
module.exports = function setup(options) {
// console.log({ options });
return {
INITIALIZE: context => {
context.user.helmet = Helmet.renderStatic();
},
PAGE_TITLE: context => {
const helmet = context.user.helmet;
const helmetTitleScript = helmet.title.toString();
const helmetTitleEmpty = helmetTitleScript.match(emptyTitleRegex);
return helmetTitleEmpty ? `<title>${options.routeOptions.pageTitle}</title>` : helmetTitleScript;
},
REACT_HELMET_SCRIPTS: context => {
const scriptsFromHelmet = ["link", "style", "script", "noscript"]
.map(tagName => context.user.helmet[tagName].toString())
.join("");
return `<!--scripts from helmet-->${scriptsFromHelmet}`;
},
META_TAGS: context => {
console.log(context,'123') //this is where I am checking
return context.user.helmet.meta.toString();
}
};
};
default.js
module.exports = {
port: portFromEnv() || "3000",
webapp: {
module: "electrode-react-webapp/lib/express",
options: {
prodBundleBase: '/buy-used-car/js/',
insertTokenIds: false,
htmlFile: "./{{env.APP_SRC_DIR}}/client/index.html",
paths: {
"*": {
content: {
module: "./{{env.APP_SRC_DIR}}/server/views/index-view"
},
}
},
serverSideRendering: true,
tokenHandler: "./{{env.APP_SRC_DIR}}/server/token-handler"
}
}
};
Any clue anyone?
EDIT 1
However, any following updates that occur on the meta tags are rendered. I'm not sure that is something electrode allows or is a feature of react-helmet.
EDIT 2
SSR is enabled in electrode.

After digging in the docs, realised that there was a slight misunderstanding. So, if data needs to be present in the page source, it needs to be pre-rendered by the server.
Why it wasn't showing at the time I asked the question? Because, data was being evaluated at run-time due ot which the page source only rendered the static content.
Electrode already provides an abstraction, each component that is being rendered has an option to load with pre-fetched data. The catch here is, you have to evaluate what all data needs to be present at runtime because more data is directly proportional to page loading time (as the server won't resolve unless the api you are depending on returns you with either a success or failure )
In terms of implementation, each route has a parameter called init-top which is executed before your page loads.
const routes = [
{
path: "/",
component: withRouter(Root),
init: "./init-top",
routes: [
{
path: "/",
exact: true,
component: Home,
init: "./init-home"
},
in init-home, you can define it something on the lines of -
import reducer from "../../client/reducers";
const initNumber = async () => {
const value = await new Promise(resolve => setTimeout(() => resolve(123), 2000));
return { value };
};
export default async function initTop() {
return {
reducer,
initialState: {
checkBox: { checked: false },
number: await initNumber(),
username: { value: "" },
textarea: { value: "" },
selectedOption: { value: "0-13" }
}
};
}
So,now whenever you load the component, it is loaded with this initialState returned in init-home
I'll just post it here, in case anyone is stuck.

Related

Invalidating a cached query conditionally in React with useQueryClient hook

I am quite new to react and am struggling with a subtle problem with data fetching/re-fetching.
We have a data source with its own UI, which lets you create multiple discussion topics and is open to users. These topics can be updated with comments and other activities(deletion of comments/attachments/links) etc from the UI. This data source also exposes endpoints that list all topics currently in the system and the details of user activities on each of them.
Our UI, which is a React app talks to these end points and:
lists out the topics.
upon clicking on one of the items shows the activity counts on the item.(in a separate panel with two tabs - one for comment and one for other user activities)
I am responsible for number 2 above.
I wrote a custom hook to achieve this goal which is called by the panel and used the useQueryClient to invalidate my query inside the hook, but unfortunately, every time the component(panel) re-renders or I switch between the tabs a new call is made to fetch the count which is deemed unnecessary. Instead we want the call to fetch the counts to go out only when the user clicks on the item and the panel opens up. But I am unable to achieve this without violating the rules of hooks(calling it inside conditionals/ calling it outside of a react component).
export const useTopicActivityCounts = (
topicId: string | undefined,
): ITopicActivityCounts | undefined => {
useQueryClient().invalidateQueries(['TopicActivitytCounts', { topicId }]);
const { data } = useQuery(
['TopicActivityCounts', { topicId }],
() =>
fetchAsync<IResult<ITopicActivityCount>>(endpointUrl, {
method: 'GET',
params: {
id,
},
}).then(resp => resp?.value),
{
enabled: !!topicId,
staleTime: Infinity,
},
);
return data;
this hook is called from here:
export const TopicDetails = memo(({ item, setItem }: ITopicDetails): JSX.Element => {
const counts = useTopicActivityCounts(item?.id);
const headerContent = (
<Stack>
/* page content */
</Stack>
);
const items = [
/* page content */,
];
const pivotItems = [
{
itemKey: 'Tab1',
headerText: localize('Resx.Comms', { count: counts?.commentsCount ?? '0' }),
},
{
itemKey: 'Tab2',
headerText: localize('Resx.Activities', { count: counts?.activitiesCount ?? '0' }),
},
];
return (
/*
page content
*/
);
});
I have tried placing it inside an onSuccess inside the hook and that did not work.

How to add meta tags with gatsby which first need to be fetched

I'm trying to add META tags for Facebook(og-tags). I'm using Gatsby and Helmet. But the problem is that those tags first need to be fetched.
It's a vehicle detail page and I need to show vehicle make and model in those tags, but the vehicle needs first to be fecthed. My code is as follows:
import Helmet from 'react-helmet';
const Page = (props) => {
const [detailsMeta, setDetailsMeta] = useState(undefined);
const resolveVehicleDetailMeta = async () => {
const fetch = require('isomorphic-fetch');
const resolveVehicleImageUrl = (fetchedImage) => {
const parsed = JSON.parse(fetchedImage);
return parsed?.uri
}
const VEHICLE_QUERY = `
query VehicleQuery($reference: String!) {
vehicle (reference: $reference) {
reference
make
model
image
}
}`;
await fetch(`/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: VEHICLE_QUERY,
variables: {
reference: 'some reference'
}
})
})
.then((resp) => resp.json())
.then((result) => {
const vehicle = result?.data?.vehicle;
if(vehicle){
setDetailsMeta({
reference: vehicle.reference,
make: vehicle.make,
model: vehicle.model,
image: resolveVehicleImageUrl(vehicle.image)
})
}
})
.catch((err) => {
console.log('err', err)
});
}
const renderMetaTitle = () => {
const ogTitle = `Tweedehands ${detailsMeta?.make} ${detailsMeta?.model} ${detailsMeta?.reference}`
return ogTitle;
}
return (
<>
<Helmet>
{detailsMeta && <meta property='og:title' content={renderMetaTitle()} />}
...
</Helmet>
The rest...
</>
)
}
And in gatsby config is gatsby-plugin-react-helmet added. The config file is as follows:
const plugins = [
`gatsby-plugin-react-helmet`,
....
]
module.exports = {
developMiddleware: (app) => {
app.use(
'/graphql',
createProxyMiddleware({
target: 'http://localhost:8000'
})
);
},
siteMetadata: {
siteUrl: `https://${settings.DOMAIN}`
},
plugins: plugins
};
Thus, I first fetch data from the server and store it in detailsMeta and then I show it inside Helmet. When I test it on localhost I see those tags and it works fine, but when I test it in Facebook debugger they are not shown.
Can I at all add data to the og-tags which first needs to be fetched and that it be seen by Facebook?
Thanks.
Can I at all add data to the og-tags which first needs to be fetched
and that it be seen by Facebook?
Yes but only if you are using statically analyzed data (i.e: using page queries, static queries, etc). In that case, you just need to add gatsby-plugin-react-helmet plugin in order to add drop-in server-side support to Helmet component.
In your gatsby-config.js:
plugins: [`gatsby-plugin-react-helmet`]
Extracted from https://www.gatsbyjs.com/plugins/gatsby-plugin-react-helmet/
In your case, you are using the fetch method that will be fired on the client-side, so the data won't be statically analyzed hence not present when the Facebook crawler reaches the site. This means that the Helmet component will be populated later than the crawler requests it.
I'm not sure about your specs but you can try converting your fetched into GraphQL nodes in order to use pages queries or static queries fulfill the Helmet component properly.

How to mock a window variable in a single story

I am using Navigator online in my React Application to determine if the client is online. I am showing an offline Fallback Component when the client is offline. For now I have made the Component Pure - so I can display it in Storybook by passing the online status as property. But this is not always a suitable solution.
So I wonder how can you mock global (window) variables for a single story in Storybook? The only - very dirty solution - I found looks as following:
ClientOffline.decorators = [
(Story) => {
const navigatiorInitally = global.navigator
// I am overwritting the navigator object instead of directly the
// online value as this throws an 'TypeError: "x" is read-only' Error
Object.defineProperty(global, 'navigator', {
value: { onLine: false },
writable: false,
})
useEffect(() => {
return () => {
Object.defineProperty(global, 'navigator', {
value: navigatiorInitally,
writable: false,
})
location.reload() //needed because otherwilse other stories throw an error
}
}, [])
return <Story />
},
]
Does anybody have a more straightforward solution?
In this answer I assume that all side effects (and communicating with Navigator is a side effect) are separated from component body into hooks.
Let's assume that you have component that looks like this:
function AwesomeComponent () {
let [connectionStatus, setConnectionStatus] = useState('unknown');
useEffect(function checkConnection() {
if (typeof window === 'undefined' || window.navigator.onLine) setConnectionStatus('online');
else setConnectionStatus('offline');
}, []);
if (connectionStatus === 'online') return <OnlineComponent/>
if (connectionStatus === 'offline') return <FallbackComponent/>
return null;
}
Your can extract your hook into separate module. In my example it would be both - state and effect.
function useConnectionStatus() {
let [connectionStatus, setConnectionStatus] = useState("unknown");
useEffect(function checkConnection() {
if (typeof window === "undefined" || window.navigator.onLine)
setConnectionStatus("online");
else setConnectionStatus("offline");
}, []);
return connectionStatus;
}
This way you separate logic from presentation and can mock individual modules. Storybook have guide to mock individual modules and configure them per story. Better consult the docs, since software is changing and in time something may be done in some other way.
Let's say, you named your file useConnectionStatus.js. To mock it, you will have to create __mock__ folder, create your mocked module there. For example it would be something like this:
// __mock__/useConnectionStatus.js
let connectionStatus = 'online';
export default function useConnectionStatus(){
return connectionStatus;
}
export function decorator(story, { parameters }) {
if (parameters && parameters.offline) {
connectionStatus = 'offline';
}
return story();
}
Next step is to modify webpack config to use your mocked hook instead of actual hook. Documentation provide a way to do this from your .storybook/main.js. At the time of writing it is done like this:
// .storybook/main.js
module.exports = {
// Your Storybook configuration
webpackFinal: (config) => {
config.resolve.alias['path/to/original/useConnectionStatus'] = require.resolve('../__mocks__/useConnectionStatus.js');
return config;
},
};
Now, decorate your previews with our new decorator, and you will be able to set configuration for every specific story separately.
// inside your story
Story.parameters = {
offline: true
}

How to refresh powerbi-client-react component once token has expired?

I am testing a react web app where I can display reports from Power BI. I am using powerbi-client-react to embed the reports. However, I face an issue when I load the component with an expired token, I get this error: Content not available screenshot.
So whenever that happens, I catch it with the error event handler, get a new token and update the powerbi report accessToken. However, it doesn't seem to reload/refresh the embed when I set the new accessToken in react. It only displays the screenshot above.
Error log screenshot.
Is there a way to force refresh the embed component with the new access token? or is my approach not correct? Any mistakes pointer would be appreciated.
import React from 'react';
import {models} from 'powerbi-client';
import {PowerBIEmbed} from 'powerbi-client-react';
// Bootstrap config
let embedConfigTest = {
type: 'report', // Supported types: report, dashboard, tile, visual and qna
id: reportId,
embedUrl: powerBIEmbedURL,
accessToken: null,
tokenType: models.TokenType.Embed,
pageView: 'fitToWidth',
settings: {
panes: {
filters: {
expanded: false,
visible: false,
},
},
background: models.BackgroundType.Transparent,
},
};
const PowerBiReport = ({graphName, ...props}) => {
let [embedToken, setEmbedToken] = React.useState();
let [embedConfig, setEmbedConfig] = React.useState(embedConfigTest);
React.useEffect(
() => {
setEmbedToken(EXPIRED_TOKEN);
setEmbedConfig({
...embedConfig,
accessToken: EXPIRED_TOKEN, // Initiate with known expired token
});
},
[graphName]
);
const changeSettings = (newToken) => {
setEmbedConfig({
...embedConfig,
accessToken: newToken,
});
};
// Map of event handlers to be applied to the embedding report
const eventHandlersMap = new Map([
[
'loaded',
function() {
console.log('Report has loaded');
},
],
[
'rendered',
function() {
console.log('Report has rendered');
},
],
[
'error',
async function(event, embed) {
if (event) {
console.error(event.detail);
console.log(embed);
// Simulate getting a new token and update
setEmbedToken(NEW_TOKEN);
changeSettings(NEW_TOKEN);
}
},
],
]);
return (
<PowerBIEmbed
embedConfig={embedConfig}
eventHandlers={eventHandlersMap}
cssClassName={'report-style-class'}
/>
);
};
export default PowerBiReport;
Thanks #vtCode. Here is a sample but the refresh can only happen in 15 secs interval.
import { PowerBIEmbed } from "powerbi-client-react";
export default function PowerBiContainer({ embeddedToken }) {
const [report, setReport] = useState(null);
useEffect(() => {
if (report == null) return;
report.refresh();
}, [report, embeddedToken]);
return (
<PowerBIEmbed
embedConfig={{ ...embedConfig, accessToken: embeddedToken }}
getEmbeddedComponent={(embeddedReport) => setReport(embeddedReport)};
/>
);
}
Alternatively, you can add the React "key" attribute which remounts the component when embededToken changes
<PowerBIEmbed key={embeddedToken}
embedConfig={{ ...embedConfig, accessToken: embeddedToken }}
/>
I ended up solving this issue, although not so beautiful.
I checked the powerbi-client wiki as it has dependency on it and found out that you could use embed.reload() in the embed object I get from the error function.
For some reason (I could not find out why), the error handler gets triggered twice, so to avoid refreshing the token twice, I had to create a dialog notifying the user that the token had expired and whenever that dialog is closed, I reload the powerbi report.
Exact wiki reference:
Overriding Error Experience
Reload a report
Update embed token

TypeError: result.data.umdHub.articles.forEach is not a function

I am having issues linking pages with slugs. All I am aiming to do is create a page with a list of articles (which I have). But I cannot link those articles to display their content. I understand you might need to use createPages. Below is the code I am trying. Does anyone have experience with this that might be able to point me in the right direction for linking index and article pages?
exports.createPages = ({ graphql, actions }) => {
// **Note:** The graphql function call returns a Promise
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise for more info
const { createPage } = actions
return graphql(`
{
umdHub {
articles {
data {
slug
title
body
subtitle
hero_image {
url_1200_630
}
}
}
}
}
`
).then(result => {
result.data.umdHub.articles.forEach(({ data }) => {
createPage({
path: articles.data.slug,
component: path.resolve(`./src/article.js`),
context: {
// Data passed to context is available
// in page queries as GraphQL variables.
slug: articles.data.slug,
},
})
})
})
}
I am getting this error with the above code:
TypeError: result.data.umdHub.articles.forEach is not a function
Second Attempt:
const path = require(`path`)
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
const articleTemplate = path.resolve(`./src/terp/article.js`)
// Query for markdown nodes to use in creating pages.
// You can query for whatever data you want to create pages for e.g.
// products, portfolio items, landing pages, etc.
return graphql(`
{
umdHub {
articles {
data {
id
title
subtitle
body
summary
hero_image {
url_1200_630
}
authorship_date {
formatted_short
unix
unix_int
formatted_long
formatted_short
time
}
slug
}
}
}
}
`).then(result => {
if (result.errors) {
throw result.errors
}
// Create blog post pages.
result.data.umdHub.articles.data.forEach(data => {
createPage({
// Path for this page — required
path: `${data.slug}`,
component: articleTemplate,
context: {
// Add optional context data to be inserted
// as props into the page component..
//
// The context data can also be used as
// arguments to the page GraphQL query.
//
// The page "path" is always available as a GraphQL
// argument.
},
})
})
})
}
Returns error:
⠐ createPages Your site's "gatsby-node.js" created a page with a
component that doesn't exist

Resources