mqtt-react-hooks Subscriber stops working after one message - react-redux

I have a simple react app using mqtt-react-hooks and redux. I want to update my redux store each time a new message is received by a Subscriber.
Subscriber.tsx
import React, { useEffect} from 'react';
import { useSubscription } from 'mqtt-react-hooks';
import { useAppDispatch } from '../features/item/hooks';
import { addItem } from '../features/item/item-slice';
const Subscriber = () => {
const { message } = useSubscription('queue');
const dispatch = useAppDispatch();
useEffect(() => {
if (message && message.message) {
dispatch(addItem(JSON.parse(message.message)));
}
}, [message]);
return (
<span>{message}</span>
);
};
export default Subscriber
App.tsx
import React from 'react';
import './App.css';
import {useAppSelector} from './features/item/hooks'
import { Connector } from 'mqtt-react-hooks'
import Subscriber from './mqtt/Subscriber'
function App() {
const items = useAppSelector((state) => state.item.items);
return (
<>
<Connector brokerUrl="ws://localhost:9001"
options={{keepalive: 10}}>
<div className="item-holder">
{Array.from(items, ([key, it]) => ({ key, it })).map( (kvp) => { return <div>{kvp.it} key={kvp.key}></div>})}
</div>
<Subscriber />
</Connector>
</>
);
}
export default App;
If I remove the useEffect from the Subscriber, the message gets received and updated. And I can send as many messages as I want. However, when I call the dispatch(addItem(... inside the useEffect, it will receive the first message, but ignores all future messages. My mosquitto broker says that the client has closed the connection. It never attempts to reconnect.
I'm very new to react. I have a feeling I'm not doing this right at all. What I really want is a redux store that maintains state based off of messages coming from an mqtt topic. The app has buttons that allows the user to publish messages back to the mqtt broker and change the redux state.
EDIT
As requested, here's the addItem code.
import {createSlice, PayloadAction} from '#reduxjs/toolkit'
interface ItemState {
items: Item[],
}
const initialState: ItemState = {
items: []
}
const orderSlice = createSlice({
name: 'items',
initialState,
reducers: {
addItem(state, action: PayloadAction<Item>) {
state.items.push(action.payload);
return state;
}
}
});
export const { addItem } = itemSlice.actions;
export default itemSlice.reducer;
And the useAppDispatch comes from ./features/item/hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from './item-store'
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
However, I will add that I got rid of this and used the usual useDispatch and useSelector instead of the "useApp____" versions and got the same result.
I believe the issue lies in the mqtt-react-hooks hooks but my react-fu skills are not yet high enough to solve.

It looks like there is a sequence of things happening that is causing Connector to rerender and drop the connection, here's what I think is going on:
App is subscribing to store state, causing it to rerender every time a new message is received.
You're recreating your mqtt config object every time App renders, because you're passing an object literal {keepalive: 10}
In Connector.tsx line 48, the mqttConnect callback depends on the mqtt options object. React does a referential equality check, sees the options have changed, and causes the callback to be recreated.
In Connector.tsx line 59, this causes the useEffect to rerun because the callback changed, which calls its teardown function, which ends the mqtt connection.
To fix it, you should create your MQTT options outside of App so that they don't change.

Related

Possible memory leak in NativeScript app if user reopens his app multiple times

I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.
Tested with Android sdk emulator and on real device.
AppComponent
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';
import { AuthenticationService } from './authentication.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
public user$: Observable<any>;
private subscriptions: Subscription[] = [];
constructor(private appService: AppService, private authenticationService: AuthenticationService) {}
public ngOnInit(): void {
this.user$ = this.authenticationService.user$;
this.subscriptions.push(
this.authenticationService.user$.subscribe((user: any) => {
console.log('user', !!user);
})
);
}
public ngOnDestroy(): void {
if (this.subscriptions) {
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
async signIn() {
await this.appService.signIn();
}
async signOut() {
await this.appService.signOut();
}
}
AuthenticationService
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
public user$: Observable<any>;
constructor(private appService: AppService) {
this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
}
}
AppService
import { Injectable, NgZone } from '#angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const user$ = new BehaviorSubject<User>(null);
#Injectable({
providedIn: 'root',
})
export class AppService {
constructor(private ngZone: NgZone) {
addAuthStateListener({
onAuthStateChanged: ({ user }) => {
this.ngZone.run(() => {
user$.next(user);
});
},
});
}
public authState(): Observable<User> {
return user$.asObservable().pipe(distinctUntilChanged());
}
async signIn() {
return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
(error: string) => {
throw {
message: error,
};
}
);
}
signOut() {
logout();
}
}
ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.
If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.
If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.
Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.
const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));
timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions
Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:
subscription.add(() => console.log(`everybody's done.`));
One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)
<page-router-outlet
(loaded)="outletLoaded($event)"
(unloaded)="outletUnLoaded($event)"></page-router-outlet>
Then you cn use outletLoaded(args: EventData) {} to initialise your code
respectively outletUnLoaded to destroy your subscriptions.
This is helpful in cases where you have access to the router outlet (in App Component for instance)
In case when you are somewhere inside the navigation tree you can listen for suspend event
Application.on(Application.suspendEvent, (data: EventData) => {
this.backFromBackground = true;
});
Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.
It works pretty well for me.
Hope that help you as well.

ReduxForm is rendered twice when mounted via enzyme

I'm trying to align my tests to follow breaking changes after upgrading react-redux to 6.0.0 and redux-form to 8.1.0 (connected components do not take store in props any longer)
I needed to wrap my connected component in from react-redux in tests and use mount to get to actual component but now ReduxForm is rendered twice.
I tried to use hostNodes() method but it returns 0 elements.
Any ideas how to fix it?
Here is the test:
import React from 'react'
import { mount } from 'enzyme'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
import PasswordResetContainer from './PasswordResetContainer'
describe('PasswordResetContainer', () => {
it('should render only one ReduxForm', () => {
const mockStore = configureStore()
const initialState = {}
const store = mockStore(initialState)
const wrapper = mount(<Provider store={store}><PasswordResetContainer /></Provider>)
const form = wrapper.find('ReduxForm')
console.log(form.debug())
expect(form.length).toEqual(1)
})
And PasswordResetContainer looks like this:
import { connect } from 'react-redux'
import { reduxForm } from 'redux-form'
import PasswordReset from './PasswordReset'
import { resetPassword } from '../Actions'
export const validate = (values) => {
const errors = {}
if (!values.email) {
errors.email = 'E-mail cannot be empty.'
} else if (!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid e-mail.'
}
return errors
}
export default connect(null, { resetPassword })(
reduxForm(
{ form: 'passwordReset',
validate
})(PasswordReset))
Output from test is following:
PasswordResetContainer › should render only one ReduxForm
expect(received).toEqual(expected)
Expected value to equal:
1
Received:
2
Edit (partial solution found):
When I changed wrapper.find('ReduxForm')
into wrapper.find('ReduxForm>Hoc>ReduxForm') it started to work.
Why do I need to do such a magic?
A fix is on library mods to create but if the forms are identical, one quick way to get around the issue is to call first() after find so that
wrapper.find('ReduxForm')
looks like:
wrapper.find('ReduxForm').first()

Nativescript update http response when app launches

I have an app I have inherited that is getting data from an API endpoint. We have found that when we change data on the API, the changes are not reflected in the app. If we uninstall and re-install the app on a mobile device, then the new data from the API is displayed. Here is an example of the Building Detail page:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from "#angular/router";
import { switchMap } from 'rxjs/operators';
import { Building } from "../shared/building/building";
import { HttpService } from "../services/http/http.service";
import {
getString,
setString
} from "application-settings";
#Component({
moduleId: module.id,
selector: 'building-detail',
templateUrl: 'building-detail.component.html',
styleUrls: ["./building-detail-common.css"],
providers: [ Building, HttpService ]
})
export class BuildingDetailComponent implements OnInit {
paramName: string;
constructor(
private route: ActivatedRoute,
public building: Building,
private httpService: HttpService) {
this.route.params.subscribe(
(params) => {
this.paramName = params['name']
}
);
}
ngOnInit() {
console.log("ON INIT FIRED " + this.paramName);
let buildingInfo = JSON.parse(getString("buildingInfo"));
for (let item of buildingInfo) {
if (item.attributes.title === this.paramName) {
this.building.name = item.attributes.title;
this.building.desc = item.attributes.body.value;
let imageEndpoint = "file/file/" + item.relationships.field_building_image.data.id;
let imageUrl = this.httpService.getData(imageEndpoint)
.subscribe(data => {
this.building.image = "https://nav.abtech.edu" + data['data'].attributes.url;
console.log("The building image URL is " + this.building.image);
}, (error) => {
console.log("Error is " + error);
});
}
}
}
}
I am happy to share other files/code if you would like to look at those. Thanks!
The reason your data is not being updated is not because the ngOnInit is not being executed, it's because you're caching the old value and reloading it each time the app is run. You're caching the data persistently across app runs with appSettings and that's why you are seeing the values stay the same until you uninstall.
If you don't want to show a cached value then don't read from the app settings, or at least don't read from appSettings until you've refreshed the data once.
ngOnInit is something that is executed only when your component is created, it will never be executed again.
Also there is difference between app launch and resume, if you want to update data every time when user opens the app, you should listen to resume event and perform apis calls inside ngZone
You may even use push notification / data message if you want to notify user immediately when data changes on backend

React-Redux re-render on dispatch inside HOC not working

I am busy with a little proof of concept where basically the requirement is to have the home page be a login screen when a user has not logged in yet, after which a component with the relevant content is shown instead when the state changes upon successful authentication.
I have to state upfront that I am very new to react and redux and am busy working through a tutorial to get my skills up. However, this tutorial is a bit basic in the sense that it doesn't deal with connecting with a server to get stuff done on it.
My first problem was to get props to be available in the context of the last then of a fetch as I was getting an error that this.props.dispatch was undefined. I used the old javascript trick around that and if I put a console.log in the final then, I can see it is no longer undefined and actually a function as expected.
The problem for me now is that nothing happens when dispatch is called. However, if I manually refresh the page it will display the AuthenticatedPartialPage component as expected because the localstorage got populated.
My understanding is that on dispatch being called, the conditional statement will be reavaluated and AuthenticatedPartialPage should display.
It feels like something is missing, that the dispatch isn't communicating the change back to the parent component and thus nothing happens. Is this correct, and if so, how would I go about wiring up that piece of code?
The HomePage HOC:
import React from 'react';
import { createStore, combineReducers } from 'redux';
import { connect } from 'react-redux';
import AuthenticatedPartialPage from './partials/home-page/authenticated';
import AnonymousPartialPage from './partials/home-page/anonymous';
import { loggedIntoApi, logOutOfApi } from '../actions/authentication';
import authReducer from '../reducers/authentication'
// unconnected stateless react component
const HomePage = (props) => (
<div>
{ !props.auth
? <AnonymousPartialPage />
: <AuthenticatedPartialPage /> }
</div>
);
const mapStateToProps = (state) => {
const store = createStore(
combineReducers({
auth: authReducer
})
);
// When the user logs in, in the Anonymous component, the local storage is set with the response
// of the API when the log in attempt was successful.
const storageAuth = JSON.parse(localStorage.getItem('auth'));
if(storageAuth !== null) {
// Clear auth state in case local storage has been cleaned and thus the user should not be logged in.
store.dispatch(logOutOfApi());
// Make sure the auth info in local storage is contained in the state.auth object.
store.dispatch(loggedIntoApi(...storageAuth))
}
return {
auth: state.auth && state.auth.jwt && storageAuth === null
? state.auth
: storageAuth
};
}
export default connect(mapStateToProps)(HomePage);
with the Anonymous LOC being:
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { loggedIntoApi } from '../../../actions/authentication';
export class AnonymousPartialPage extends React.Component {
constructor(props) {
super(props);
}
onSubmit = (e) => {
e.preventDefault();
const loginData = { ... };
// This is where I thought the problem initially occurred as I
// would get an error that `this.props` was undefined in the final
// then` of the `fetch`. After doing this, however, the error went
// away and I can see that `props.dispatch is no longer undefined
// when using it. Now though, nothing happens.
const props = this.props;
fetch('https://.../api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(loginData)
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if(data && data.jwt) {
props.dispatch(loggedIntoApi(data));
localStorage.setItem('auth', JSON.stringify(data));
}
// else show an error on screen
});
};
render() {
return (
<div>
... onSubmit gets called successfully somewhere in here ...
</div>
);
}
}
export default connect()(AnonymousPartialPage);
the action:
// LOGGED_INTO_API
export const loggedIntoApi = (auth_token) => ({
type: 'LOGGED_INTO_API',
auth: auth_token
});
// LOGGED_OUT_OF_API
export const logOutOfApi = (j) => ({
type: 'LOG_OUT_OF_API'
});
and finally the reducer:
const authDefaultState = { };
export default (state = authDefaultState, action) => {
switch (action.type) {
case 'LOGGED_INTO_API':
// SOLUTION : changed this line "return action.auth;" to this:
return { ...action.auth, time_stamp: new Date().getTime() }
case 'LOG_OUT_OF_API':
return { auth: authDefaultState };
default:
return state;
}
};
My suggestion would be to make sure that the state that you are changing inside Redux is changing according to javascript's equality operator!. There is a really good answer to another question posted that captures this idea here. Basically, you can't mutate an old object and send it back to Redux and hope it will re-render because the equality check with old object will return TRUE and thus Redux thinks that nothing changed! I had to solve this issue by creating an entirely new object with the updated values and sending it through dispatch().
Essentially:
x = {
foo:bar
}
x.foo = "baz"
dispatch(thereWasAChange(x)) // doesn't update because the x_old === x returns TRUE!
Instead I created a new object:
x = {
foo:"bar"
}
y = JSON.parse(JSON.stringify(x)) // creates an entirely new object
dispatch(thereWasAChange(y)) // now it should update x correctly and trigger a rerender
// BE CAREFUL OF THE FOLLOWING!
y = x
dispatch(thereWasAChange(y)) // This WON'T work!!, both y and x reference the SAME OBJECT! and therefore will not trigger a rerender
Hope this helps!

Redux + storybook throws warning about changing store on the fly even with module.hot implemtended

I'm using storybook and I want to add redux as decorator.
Whe running storybook, I got warning in console:
<Provider> does not support changing `store` on the fly. It is most likely that you see this error because you updated to Redux 2.x and React Redux 2.x which no longer hot reload reducers automatically. See https://github.com/reactjs/react-redux/releases/tag/v2.0.0 for the migration instructions.
It's my code for config storybook:
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */
import React from 'react';
import { configure, storiesOf } from '#storybook/react';
import { Provider as ReduxProvider } from 'react-redux';
import forEach from 'lodash/forEach';
import unset from 'lodash/unset';
import Provider from 'components/Provider';
import initStore from 'utils/initStore';
import messages from '../lang/en.json';
const req = require.context('../components', true, /_stories\.js$/);
const ProviderDecorator = (storyFn) => {
const TheProvider = Provider(() => storyFn());
return (
<ReduxProvider store={initStore()}>
<TheProvider key={Math.random()} now={1499149917064} locale="en" messages={messages} />
</ReduxProvider>
);
}
function loadStories() {
req.keys().forEach((filename) => {
const data = req(filename);
if (data.Component !== undefined && data.name !== undefined && data.stories !== undefined) {
const Component = data.Component;
const stories = storiesOf(data.name, module);
stories.addDecorator(ProviderDecorator);
let decorator = data.stories.__decorator;
if (data.stories.__decorator !== undefined) {
stories.addDecorator((storyFn) => data.stories.__decorator(storyFn()));
}
forEach(data.stories, (el, key) => {
if (key.indexOf('__') !== 0) {
stories.add(key, () => (
<Component {...el} />
));
}
});
} else {
console.error(`Missing test data for ${filename}!`)
}
});
}
configure(loadStories, module);
and initStore file:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunkMiddleware from 'redux-thunk';
import { persistStore, autoRehydrate } from 'redux-persist';
import reducers from 'containers/redux/reducers';
export default () => {
const store = createStore(
reducers,
{},
composeWithDevTools(applyMiddleware(thunkMiddleware), autoRehydrate()),
);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../containers/redux/reducers', () => {
const nextReducers = require('../containers/redux/reducers'); // eslint-disable-line global-require
store.replaceReducer(nextReducers);
});
}
persistStore(store);
return store;
};
So as you can see I followed instructions from link in warning. What have I done wrong and how can I remove this warning? I know it won't show on production server, but it's pretty annoying in dev mode. :/
The reason this is happening has to do with the way Storybook hot-loads.
When you change your story, that module is hot-loaded, meaning that the code inside it is executed again.
Since you're using a store creator function and not a store instance from another module, the actual store object that is being passed to ReduxProvider on hot-load is new every time.
However, the React tree that is re-constructed is for the most part identical, meaning that the ReduxProvider instance is re-rendered with new props instead of being re-created.
Essentially, this is changing its store on the fly.
The solve is to make sure that ReduxProvider instance is new, too, on hot-load. This is easily solved by passing it a unique key prop, e.g.:
const ProviderDecorator = (storyFn) => {
const TheProvider = Provider(() => storyFn());
return (
<ReduxProvider key={Math.random()} store={initStore()}>
<TheProvider key={Math.random()} now={1499149917064} locale="en" messages={messages} />
</ReduxProvider>
);
}
From React Keys:
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.

Resources