Updating mobx store observable within self results in 'Cannot read property of <observable> of undefined' - reactjs

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)
}

Related

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 define an action in a mobx store using mobx-react?

Have been following a few tutorials on youtube and have pretty much never seen anyone explicitly define an action that mutates the state they just throw in into the store. I have been doing the same and while it works a 100% it throws a warning on react native. Just wondering how you could define that something is an action and maybe if someone has a good way to separate the actions into a different file. Here is my store.
export function createCurrencyStore() {
return {
currencies: [
'AED',
'ARS',
'AUD',
],
selectedCurrencyFrom: 'USD',
selectedCurrencyTo: 'EUR',
loading: false,
error: null,
exchangeRate: null,
amount: 1,
fromFilterString: '',
fromFilteredCurrencies: [],
toFilterString: '',
toFilteredCurrencies: [],
setSelectedCurrencyFrom(currency) {
this.selectedCurrencyFrom = currency
},
setSelectedCurrencyTo(currency) {
this.selectedCurrencyTo = currency
},
async getExchangeRate() {
const conn = await fetch(
`https://api.exchangerate-api.com/v4/latest/${this.selectedCurrencyFrom}`
)
const res = await conn.json()
console.log(res)
this.exchangeRate = res.rates[this.selectedCurrencyTo]
},
setFromFilters(string) {
this.fromFilterString = string
if (this.fromFilterString !== '') {
this.fromFilteredCurrencies = this.currencies.filter((currency) =>
currency.toLowerCase().includes(string.toLowerCase())
)
} else {
this.fromFilteredCurrencies = []
}
},
setToFilters(string) {
this.toFilterString = string
if (this.toFilterString !== '') {
this.toFilteredCurrencies = this.currencies.filter((currency) =>
currency.toLowerCase().includes(string.toLowerCase())
)
} else {
this.toFilteredCurrencies = []
}
},
}
}
have pretty much never seen anyone explicitly define an action
Well, this is weird because it is a very common thing to only mutate state through actions to avoid unexpected mutations. In MobX6 actions are enforced by default, but you can disable warnings with configure method:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
a good way to separate the actions into a different file
You don't really need to do it, unless it's a very specific case and you need to somehow reuse actions or something like that. Usually you keep actions and the state they modify together.
I am not quite sure what you are doing with result of createCurrencyStore, are you passing it to observable? Anyway, the best way to create stores in MobX6 is to use makeAutoObservable (or makeObservable if you need some fine tuning). So if you are not using classes then it will look like that:
import { makeAutoObservable } from "mobx"
function createDoubler(value) {
return makeAutoObservable({
value,
get double() {
return this.value * 2
},
increment() {
this.value++
}
})
}
That way every getter will become computed, every method will become action and all other values will be observables basically.
More info in the docs: https://mobx.js.org/observable-state.html
UPDATE:
Since your getExchangeRate function is async then you need to use runInAction inside, or handle result in separate action, or use some other way of handling async actions:
import { runInAction} from "mobx"
async getExchangeRate() {
const conn = await fetch(
`https://api.exchangerate-api.com/v4/latest/${this.selectedCurrencyFrom}`
)
const res = await conn.json()
runInAction(() => {
this.exchangeRate = res.rates[this.selectedCurrencyTo]
})
// or do it in separate function
this.handleExchangeRate(res.rates[this.selectedCurrencyTo])
},
More about async actions: https://mobx.js.org/actions.html#asynchronous-actions

JSX Element does not have any construct or call signatures

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.

data is not properly set in react and redux

I am getting details from java API .I want to set some of the details to commonvo at UI side and send the
commonvo object to server side. I am using below code:
React component
import { getDetails, populateobjDetails } from './CommonAction'
function mapDispatchToProps(dispatch) {
return {
getDetails: (formType, commonVO) => dispatch(getDetails(formType, commonVO)),
populateobjDetails: (obj, commonVO) => dispatch(populateobjDetails(obj, commonVO)) //edit 1
}
}
Redux Action:
export function getDetails(requestFormType, commonVO) {
return (dispatch) => {
axios.get(`${ROOT_URL}/byName/${commonVO.name}`, { headers: { 'Cache-Control': 'no-cache,no-store,must-revalidate,max-age=-1,private', 'Pragma':'no-cache' } }).then(
(response) => {
let obj= dispatch(setDetails(response.data[0]));
dispatch(populateobjDetails(obj, commonVO))
dispatch(setobjSearchLoadingFlag(false))
}
)
}
}
export function populateobjDetails(obj, commonVO) {
const objDetails = {
projectAlias: obj.projectAlias,
dateExpenseClose: obj.dateExpenseClosed,
projectID: obj.projectId,
id: obj.id,
idClass: obj.idClass
}
return {
type: POPULATE_OBJ_DETAILS,
payload: { ...commonVO, ...objDetails }
}
}
I am adding objDetails in commonVO.But during "Submitting the form" for which these details have been set, i am not able to see new properties being set into the commonVO.
I am new to React-Redux and not much familiar with it.Please let me know if i need to do something more on Redux-react side?
Thank you
Edit: I added method 'populateobjDetails' in 'mapDispatchToProps' function (which was missing in code) and imported the method in the component
Inside your redux action getDetails you're calling:
dispatch(populateobjDetails(obj, commonVO))
However, obj is not really defined inside that action anywhere, so an undefined object is being passed to populateobjDetails, that's why you're not seeing the new properties being set.

Vue watch array (or object) push same old and new values

I'm using Vuex and I've created a module called claims that looks like this:
import to from 'await-to-js'
import { functions } from '#/main'
import Vue from 'vue'
const GENERATE_TX_SUCCESS = 'GENERATE_TX_SUCCESS'
const GENERATE_TX_ERROR = 'GENERATE_TX_ERROR'
export default {
state: [ ],
mutations: {
[GENERATE_TX_SUCCESS] (state, generateTxData) {
state.push({ transaction: { ...generateTxData } })
},
[GENERATE_TX_ERROR] (state, generateTxError) {
state.push({ transaction: { ...generateTxError } })
}
},
actions: {
async generateTx ({ commit }, data) {
const [generateTxError, generateTxData] = await to(functions.httpsCallable('generateTx')(data))
if (generateTxError) {
commit(GENERATE_TX_ERROR, generateTxError)
} else {
commit(GENERATE_TX_SUCCESS, generateTxData)
}
}
},
getters: { }
}
Then, in the .vue component I do have this watch:
watch: {
claims: {
handler (newTxData, oldTxData) {
console.log(newTxData)
}
}
}
The problem I'm facing here is that newTxData is the same as oldTxData.
From my understanding, since this is a push and it detects the change, it's not one of these caveats: https://v2.vuejs.org/v2/guide/list.html#Caveats
So basically the problem is this one:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
https://v2.vuejs.org/v2/api/#vm-watch
Then my question is: how should I workaround this in the mutation?
Edit:
I tried also with Vue.set(state, state.length, generateTxData) but got the same behaviour.
Edit 2 - temporary solution - (bad performance-wise):
I'm adapting #matthew (thanks to #Jacob Goh) to my solution with vuexfire:
computed: {
...mapState({
claims: state => cloneDeep(state.claims)
})
},
This answer is based on this pretty smart answer by #matthew
You will need lodash cloneDeep function
Basically, create a computed value like this
computed: {
claimsForWatcher() {
return _.cloneDeep(this.claims);
}
}
What happens is, everytime a new value is pushed into claims, claimsForWatcher will become an entirely new object.
Therefore, when you watch claimsForWatcher, you won't have the problem where 'the old value will be the same as new value because they reference the same Object/Array' anymore.
watch: {
claimsForWatcher(oldValue, newValue) {
// now oldValue and newValue will be 2 entirely different objects
}
}
This works for ojects as well.
The performance hit is exaggerated for the real life use cases till you probably have hundreds (nested?) properties (objects) in your cloning object.
Either with clone or cloneDeep the clone takes 1/10 of a millisecond as reported with window.performance.now(). So measured under Node with process.hrtime() it could show even lower figures.
You can assign state to a new object:
mutations: {
[GENERATE_TX_SUCCESS] (state, generateTxData) {
state = [
...state,
{ transaction: { ...generateTxData } }
]
},
[GENERATE_TX_ERROR] (state, generateTxError) {
state = [
...state,
{ transaction: { ...generateTxError } }
]
}
}

Resources