Electron.js + React/Redux - best practices - javascript

So, here the thing.
We have main process and renderer and they`re is isolated from each other and only bridge is IPC messages(to main process) or BrowserWindow webcontents send(to main process).
I'm handling my events in componentDidMount like so:
ipcRenderer.on('deviceWasPlugged', this.deviceWasPluggedListener);
And ofc I'll remove this listener with componentWillUnount for preventing memory leaking in case the component was destroyed:
ipcRenderer.removeListener('deviceWasPlugged', this.deviceWasPluggedListener);
Obviously I have a such method in my Class/Component
deviceWasPluggedListener = (e, somedata) => {
// boring stuff goes there...
}
All is peachy, except one fact - It doesn't seems right as for me.
I still think, there should be a better place to keep my events listeners.
I think, I cant keep it in redux middleware, coz event based model won't allow me to unsubscribe of this events in redux middleware and multiple events will be created when I'll be trying to dispatch smth, even if I'll switch it by action.type:
const someMiddleware = store => next => action => {
if (action.type === 'SOME_TYPE') {
// ...
}
next(action);
};
export default someMiddleware;
So, guys, please tell me is there any better place to keep my backend events? It should be triggered every time I want it, and it shouldn't cause memory leaking problem with maxEventListeners.

Related

Is it OK to have multiple listener middlewares in the same store?

If I use createListenerMiddleware to create 2 separate middlewares, that handle different responsibilities, is that going to have any significant performance cost over creating a single listener middleware? Are there any undesirable ways they can interact?
One concern is if I dispatch addListener, removeListener or clearAllListeners, are they going to reach both middlewares, or be consumed by the first in the chain?
The alternative is creating a single shared listener middleware in its own module that can be imported to both the other modules to have listeners added, which would likely cover most use-cases but not if I want to pass in different extra arguments or different error handling.
I created the listener middleware :)
You should really only have one listener middleware instance in an app, and I can't really think of a good reason why you would want to have multiple listener middleware instances. And yes, the add/remove listener actions will definitely be handled and stopped by the first listener middleware instance that sees them, with no differentiation.
If the goal is to have separate listener+effect definitions that don't rely on importing the same middleware instance, I'd suggest trying the third file organization approach shown in the docs, where feature files export a callback that gets startListening as a parameter:
https://redux-toolkit.js.org/api/createListenerMiddleware#organizing-listeners-in-files
// feature1Slice.ts
import type { AppStartListening } from '../../app/listenerMiddleware'
const feature1Slice = createSlice(/* */)
const { action1 } = feature1Slice.actions
export default feature1Slice.reducer
export const addFeature1Listeners = (startListening: AppStartListening) => {
startListening({
actionCreator: action1,
effect: () => {},
})
}

How to deal with race conditions in event listeners and shared state?

I have 2 event listeners that operate on the same shared data/state. For instance:
let sharedState = {
username: 'Bob',
isOnline: false,
};
emitter.on('friendStatus', (status) => {
sharedState.isOnline = status.isOnline;
});
emitter.on('friendData', (friend) => {
if (sharedState.isOnline) {
sharedState.username = friend.username;
}
});
My problem is that these events are emitted at any order. The friendData event might come in before the friendStatus. But friendData does something with the data returned from friendStatus. In other words: I need the event handler for friendData to execute after friendStatus, but I don't have this assurance from the event emitter perspective. I need to somehow implement this in my code.
Now of course I could simply remove the if (sharedState.isOnline) { from the friendData listener and let it run its course. Then I'd have a function run after both handlers have finished and somewhat reconciliate the shared state dependencies:
emitter.on('friendStatus', (status) => {
sharedState.isOnline = status.isOnline;
reconcileStateBetweenUsernameAndIsOnline();
});
emitter.on('friendData', (friend) => {
sharedState.username = friend.username;
reconcileStateBetweenUsernameAndIsOnline();
});
Problem is that this reconciliation function knows about this specific data dependencies use case; hence cannot be very generic. With large interconnected data dependencies this seems a lot harder to achieve. For instance I am already dealing with other subscriptions and other data dependencies and my reconciliation function is becoming quite large and complicated.
My question is: is there a better way to model this? For instance if I had the assurance that the handlers would run in a specific order I wouldn't have this issue.
EDIT: expected behavior is to use the sharedState and render a UI where I want the username to show ONLY if the status isOnline is true.
From #Bergi's answer in the comments the solution I was hinting seems to be the most appropriate for such case. Simply let the event-handlers set their own independent state, then observe on the values changing and write appropriate logic based on what you need to do. For instance I need to show a username; this function shouldn't care about the order or have any knowledge of time: it should simply check whether the isOnline status is true and if there's a username. Then the observable pattern can be used to call this function whenever each dependency of the function changes. In this case the function depends on status.isOnline and friend.username hence it will observe and re-execute whenever those values change.
function showUsername() {
if (status.isOnline && friend.username != '') return true;
}
This function must observe the properties it depends on (status.isOnline and friend.username). You can have a look at RxJS or other libraries for achieving this in a more "standard" way.

Is it safe to call sagaMiddleware.run multiple times?

I'm using redux and redux-saga in an application to manage state and asynchronous actions. In order to make my life easier, I wrote a class that acts essentially as a saga manager, with a method that "registers" a saga. This register method forks the new saga and combines it with all other registered sagas using redux-saga/effects/all:
class SagasManager {
public registerSaga = (saga: any) => {
this._sagas.push(fork(saga));
this._combined = all(this._sagas);
}
}
This class is then used by my store to get the _combined saga, supposedly after all sagas are registered:
const store = Redux.createStore(
reducer,
initialState,
compose(Redux.applyMiddleware(sagaMiddleware, otherMiddleware)),
);
sagaMiddleware.run(sagasManager.getSaga());
However, I ran into the problem that depending on circumstances (like import order), this doesn't always work as intended. What was happening was that some of the sagas weren't getting registered before the call to sagaMiddleware.run.
I worked around this by providing a callback on SagasManager:
class SagasManager {
public registerSaga = (saga: any) => {
this._sagas.push(fork(saga));
this._combined = all(this._sagas);
this.onSagaRegister();
}
}
And then the store code can use this as
sagasManager.onSagaRegister = () => sagaMiddleware.run(sagasManager.getSaga());
This seems to work, but I can't find in the docs whether this is safe. I did see that .run returns a Task, which has methods for canceling and the like, but since my problem is only in that awkward time between when the store is constructed and the application is rendered I don't that would be an issue.
Can anyone explain whether this is safe, and if not what a better solution would be?
It may depend on what you mean by "safe". What exactly do you mean by that in this case?
First, here's the source of runSaga itself, and where it gets used by the saga middleware.
Looking inside runSaga, I see:
export function runSaga(options, saga, ...args) {
const iterator = saga(...args)
// skip a bunch of code
const env = {
stdChannel: channel,
dispatch: wrapSagaDispatch(dispatch),
getState,
sagaMonitor,
logError,
onError,
finalizeRunEffect,
}
const task = proc(env, iterator, context, effectId, getMetaInfo(saga), null)
if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task)
}
return task
}
What I'm getting out of that is that nothing "destructive" will happen when you call runSaga(mySagaFunction). However, if you call runSaga() with the same saga function multiple times, it seems like you'll probably have multiple copies of that saga running, which could result in behavior your app doesn't want.
You may want to try experimenting with this. For example, what happens if you have a counter app, and do this?
function* doIncrement() {
yield take("DO_INCREMENT");
put({type : "INCREMENT"});
}
sagaMiddleware.runSaga(doIncrement);
sagaMiddleware.runSaga(doIncrement);
store.dispatch({type : "DO_INCREMENT"});
console.log(store.getState().counter);
// what's the value?
My guess is that the counter would be 2, because both copies of doIncrement would have responded.
If that sort of behavior is a concern, then you probably want to make sure that prior sagas are canceled.
I actually ran across a recipe for canceling sagas during hot-reloading a while back, and included a version of that in a gist for my own usage. You might want to refer to that for ideas.

How to connect RxJs to React component

Most examples for using RxJS to observe button clicks are like:
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.subscribe(() => console.log('Clicked!'));
Is this okay in React? Since there is the virtual DOM, is it still okay to get reference to the real DOM like this? What happens after a re-render? Is the subscription lost?
I.e. they use document.querySelector.
But when I write my render() method, I'm used to <button onClick={...} >.
What is the recommended way to do this in React? Get rid of the onClick inside the JSX, add a class/id to my button, or maybe a ref, and use the style above?
Also, should I unsubscribe anywhere, e.g. in componentWillUnmount()? In which case I'd want to keep references to all my subscribers (event listeners)? In which case this seem much more complex than the old (callback) way.
Any suggestions, thoughts on this?
I've thought about this a lot - I think the answer by #ArtemDevAkk is one really good way to do it, but I'm not sure it accounts for how you intend to use the data. I'd like to suggest an alternative approach. Admittedly, I've used hooks for this, but you could do this in old school classes in a similar way.
I've had to break up your question into a few parts:
How to reference a DOM node in React
How to create a Subject and make it fire
How to subscribe to any Observable in React (useEffect)
Getting a reference to a DOM node
In case all you are asking is how to reference a DOM node, the rule of thumb is that to access the DOM in React, you use the useRef hook (or createRef in a Class).
Keeping your Subject active (for sharing events)
The benefit of this method is that your Subject will be created once and kept alive indefinitely, so anything is able to subscribe and unsubscribe at will. I can't think of a great reason for using RxJs in a React project because React has its own ways to handle events, so it's hard to know if this will solve your problem.
function MyRxJsComponent() {
// establish a stateful subject, so it lives as long as your component does
const [ clicks$ ] = useState(new Subject());
// register an event handler on your button click that calls next on your Subject
return <button onClick={e => clicks$.next(e)}></button>
}
This will create one Subject that will stay alive as long as your component is alive on the page. My best guess for how you might use this is by using a Context to contain your subject (put clicks$ in your context) and then pushing events to that Subject.
Note how I'm not using fromEvent to create an Observable, because this Observable would be created and destroyed with your button, making it impossible for anything to stay subscribed to it outside the component.
Just using an Observable internally
Alternatively (as #ArtemDevAkk alluded to), you could just create your Observable for local use and accept the limitation that it will be created and destroyed (this might actually be preferable, but again, it depends on what you're actually trying to achieve).
function MyRxJsComponent() {
// get a reference to the button (use <button ref={buttonRef} in your component)
const buttonRef = useRef(null)
// use useEffect to tell React that something outside of its
// control is going to happen (note how the return is _another_
// function that unsubscribes.
useEffect( () => {
// create the observable from the buttonRef we created
const clicks$ = fromEvent(buttonRef.current, 'click')
clicks$.subscribe( click => {
// do whatever you need to with the click event
console.log('button was clicked!', click)
})
return () => {
start$.unsubscribe()
}
}, [buttonRef]);
return <button ref={buttonRef}>Click me!</button>;
}
But should you?
I think overall, I've shown above how you can use state to keep a Subject alive for as long as you need; you can use useEffect() to subscribe to an observable and unsubscribe when you don't need it any more. (I haven't tested the samples locally, so there might be a small tweak needed..)
I mentioned using a Context to share your Subject with other components, which is one way I can imagine RxJs being useful in React.
Ultimately, though, if this is all you want to do, there are better ways to handle events in React. A simple <button onClick={() => console.log('clicked')}> could be all you need. React, being a reactive library in itself, isn't really designed to contain the amount of state that an Observable can contain - your views are meant to be simplified projections of state stored elsewhere. For the same token, it's generally advised that you don't try to reference a specific element unless you have an exceptional reason to do so.
const buttonRef = useRef(null)
useEffect( () => {
const start$ = fromEvent(buttonRef.current, 'click').subscribe( click => {
console.log('click event :', click)
})
return () => {
start$.unsubscribe()
}
}, [])
add ref to button

React & Firebase + Flux : Child component does not render

I've already checked this SO article, however, the solution does not work. I have a simple messaging app using Firebase + Flux:
App
-UserList Component (sidebar)
-MessageList Component
The UserList component gets props from this.state.threads (state belongs to main App component).
In my Flux ThreadStore, I have the following event listeners bound to a firebaseRef:
const _firebaseThreadAdded = (snapshot) => {
threadsRef.child(snapshot.key()).on('value', (snap) => {
console.log('thread added to firebase');
_threads.push({
key: snap.key(),
threadType: snap.val().threadType,
data: snap.val()});
});
}
ref.child(ref.getAuth().uid).child('threads').on('child_added', Actions.firebaseThreadAdded);
The UserList uses this.props.threads.map(thread => { return(<li>{thread.name}</li>) } ) (summarised) to then render a list of thread names in the side bar based on the thread keys in the firebaseRef/userId/threads.
However, this does not render any list of users until I click any button in the app or delete a reference directly from the Firebase Forge UI (I also have a 'child_removed' event listener).
For the life of me I cannot figure this out. Can anyone explain why this is happening / how to fix it? I've spent a whole half day trying and haven't come up with anything.
FYI:
Relevant Dispatcher.register entry:
case actionConstants.FIREBASE_THREAD_ADDED:
_firebaseThreadAdded(action.data);
threadStore.emitChange();
break;
Relevant Actions entry:
firebaseThreadAdded(data) {
AppDispatcher.handleAction({
actionType: actions.FIREBASE_THREAD_ADDED, data
});
},
Okay, so I figured it out after a lot of reading - a little new to Flux. The issue was here:
case actionConstants.FIREBASE_THREAD_ADDED:
_firebaseThreadAdded(action.data);
threadStore.emitChange();
break;
The emitChange() action was being triggered before the Firebase promise was being resolved, and hence the child components were updating hte state without any data. Hence, the list was not being rendered. What I did to change this was re-architect the function so that threadStore.emitChange() would only be triggered on resolve:
const _firebaseThreadAdded = (snapshot) => {
threadsRef.child(snapshot.key()).once('value').then(snap => {
console.log('thread added to firebase: ', snap.key());
_threads.push({
threadId: snap.key(),
threadType: snap.val().threadType,
data: snap.val()});
threadStore.emitChange();
});
}
and the resulting dispatcher register entry:
case actionConstants.FIREBASE_THREAD_ADDED:
_firebaseThreadAdded(action.data);
break;
Don't know if this is the best way of handling this, but it's doing the job - the emit event is only triggered once firebase has loaded. Hope this helps anyone with a similar issue for promises/firebase/ajax!

Categories

Resources