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
}
Related
I am attempting to make style class on and off when it is observed, but it works only once. i guess it may be solved with obs.unobserve or obs.disconnect, but i failed to write a code in right way.
Here is the code. (simplified)
useEffect(() => {
const obs = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
$('.class-a').addClass('white');
}
else {
$('.class-a').addClass('black');
}
})
}, { threshold: [0.5] });
obs.observe(document.querySelector('.sub'));
}, [])
You are only querying a single selector. If you want to query more than one, you would have to use document.querySelectorAll().
Edit try this (querying them on their own won't work, have to actually loop through them as well):
const list = document.querySelectorAll('.sub');
list.forEach((el) => {
obs.observe(el);
})
Referring to these mdn docs
Working non-react test: https://codepen.io/shikkaba/pen/eYMVXXX
I have a barchart made with chartjs in a React app. It works well, but the data parsing could be handled by a web worker so as to speed up the page loading.
I have declared my web worker in a useEffect function:
let data = React.useRef([]);
useEffect(() => {
const dataWorker: Worker = new Worker("./worker.ts");
dataWorker.postMessage({
msg: "formatData",
data: { series, theme, range },
});
dataWorker.onmessage = (e: MessageEvent) => {
if (e && e.data) {
data.current = e.data;
}
};
}, [range, series, theme]);
And the webworker is defined in a separate file worker.ts:
self.onmessage = async (e) => {
if (e && e.data && e.data.msg === "formatData") {
const { sortedSeries, theme, range } = e.data;
// eslint-disable-next-line react-hooks/rules-of-hooks
const data = useFormatData(sortedSeries, theme, range);
self.postMessage(data);
}
};
function useFormatData(series, theme, range) {
return useMemo(() => {
// do stuff
};
}, [range, sortedSeries, theme]);
}
The worker makes the page crash with the following error:
Uncaught DOMException: Failed to execute 'postMessage' on 'Worker: function (number) {
var b = number % 10,
output =
toInt((number...... } could not be cloned.
How to fix this?
I recently saw this issue when I was passing an object to the web worker that contained a function that I was not aware of.
By the looks of your error, it could be the case for you as well in regards to series, theme, or range being passed to the worker.
There is a good explanation of alternatives explained in this SO Answer. To summarize, it mentions passing only the the serializable data to the worker and then adding back the functions (if necessary) on the webworker side.
I am trying to add Application Insights in my ReactJS Application. I changed the JS code that is provided on the GitHub Demo to TypeScript.. now I have
class TelemetryProvider extends Component<any, any> {
state = {
initialized: false
};
componentDidMount() {
const { history } = this.props;
const { initialized } = this.state;
const AppInsightsInstrumentationKey = this.props.instrumentationKey;
if (!Boolean(initialized) && Boolean(AppInsightsInstrumentationKey) && Boolean(history)) {
ai.initialize(AppInsightsInstrumentationKey, history);
this.setState({ initialized: true });
}
this.props.after();
}
render() {
const { children } = this.props;
return (
<Fragment>
{children}
</Fragment>
);
}
}
export default withRouter(withAITracking(ai.reactPlugin, TelemetryProvider));
But when I try to import the same component <TelemetryProvider instrumentationKey="INSTRUMENTATION_KEY" after={() => { appInsights = getAppInsights() }}></Telemetry> I get an error Severity Code Description Project File Line Suppression State
(TS) JSX element type 'TelemetryProvider' does not have any construct or call signatures.
I attempted to simply // #ts-ignore, that did not work. How do I go about solving this?
Given the example above, I hit the same issue. I added the following:
let appInsights:any = getAppInsights();
<TelemetryProvider instrumentationKey={yourkeyher} after={() => { appInsights = getAppInsights() }}>after={() => { appInsights = getAppInsights() }}>
Which seem to solve the issue for me, I am now seeing results in Application Insights as expected.
I guess if you want to have the triggers etc on a different Page/Component you may wish to wrap it in your own useHook or just add something like this to the component.
let appInsights:any;
useEffect(() => {
appInsights = getAppInsights();
}, [getAppInsights])
function trackEvent() {
appInsights.trackEvent({ name: 'React - Home Page some event' });
}
Not the best answer, but it's moved me forward. Would be nice to see a simple hooks version in typescript.
Really hope it helps someone or if they have a cleaner answer.
I know that several similar issues like that exist already. However, having reviewed many of them I was not able to find solution to my problem. Here is the situation:
I want my ReportStore to fetch data from the server in form of an array and store them in an observable map. Fetching part is OK, the data is coming correctly. The problem occurs when I try to write values to observable map inside of requestNewReports method. Here is the store code:
ReportStore.ts
import api from "../api";
import { observable, action, ObservableMap } from "mobx";
import { IReportMetaData } from "../pages/ManagerView/TicketSales";
import { notificationStore } from "./NotificationStore";
export class ReportStore {
fetchedReports: ObservableMap<number, IReportMetaData>;
constructor() {
this.fetchedReports = observable.map({});
}
async requestNewReports(fromDate: string = '', toDate: string = '') {
try {
const res = await api.get("reports/filtered", {
params: {
fromDate: from,
toDate: to
}
});
res.data.forEach((row: IReportMetaData, index: number) => {
if (!this.fetchedReports.has(index)) {
this.fetchedReports.set(index, row);
}
});
} catch (err) {
return notificationStore.showError(err.message);
}
}
}
export const reportStore = new ReportStore();
The store is being provided in app.tsx via <Provider reportStore={reportStore}>....</Provider> and injected in the components like that:
export interface ITicketSalesProps {
reportStore?: ReportStore;
}
export const TicketSales = inject('reportStore')(observer((props: ITicketSalesProps) => {
// Component logic
}
And being called like that:
// inside TicketSales
useEffect(() => {
const { fetchedReports, requestNewReports } = props.reportStore!;
if (fetchedReports.size === 0) {
requestNewReports();
}
}, []);
My setup:
create-react-app via npx with TypeScript,
"mobx": "^5.15.3",
"mobx-react": "^6.1.4",
According to some issues the problem lies in legacy decorators. However,according to official mobx documentation
Decorators are only supported out of the box when using TypeScript in create-react-app#^2.1.1. In older versions or when using vanilla JavaScript use either the decorate utility, eject, or the customize-cra package.
Which seems to be exactly my case.
Hence I am out of ideas of why it can be broken...
I even tried to eject and try babel plugin as was suggested in some issues:
{
"plugins": [
["#babel/plugin-proposal-decorators", { "legacy": true }],
["#babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
But no success.
Any help would be appreciated.
Thanks in advance!
I think this issue comes from your javascript code in general and not mobx.
From what I can read here, requestNewReports() is not a bound method. In your component you destructure your store and assign requestNewReports to a new variable so "this" is not your store anymore. Thus you have the "undefined" error.
You could change it to:
// inside TicketSales
useEffect(() => {
const {reportStore} = props
if (reportStore.fetchedReports.size === 0) {
reportStore.requestNewReports();
}
}, []);
Or, you could bind the method to the instance like:
...
constructor() {
this.fetchedReports = observable.map({});
this.requestNewReports = this.requestNewReports.bind(this)
}
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.