Infinite loop in redux-saga effect - react-redux

I've been able to create an infinite loop that keeps getting data from the API.
I use useEffect conditional dispatch to run, which should only run once.
But when I look into Redux DevTools, the data retrieval from the api is in an endless loop.
Slice:
import { createSlice } from "#reduxjs/toolkit";
const initialState = { cats: [] };
const catSlice = createSlice({
name: "cat",
initialState,
reducers: {
setCat: (state, action) => {
state.cats = action.payload;
},
},
});
export const { setCat } = catSlice.actions;
export default catSlice.reducer;
Saga:
import { call, put, takeLatest } from "redux-saga/effects";
import { setCat } from "./catSlice";
function* getCatsFetch() {
const cats = yield call(() => fetch(`https://xxxapi.com`));
const formatedCats = yield cats.json();
yield put(setCat(formatedCats));
}
function* catSaga() {
yield takeLatest("cat/setCats", getCatsFetch);
}
export default catSaga;
Command in App.js
useEffect(() => {
dispatch(setCats());
}, [dispatch]);
Can you please help me stop endless requests on the api? Thanks

The issue is your getCatsFetch saga emits a setCat action. And your catSaga is waiting for any setCat to trigger another getCatsFetch saga. So even though you're only dispatching one setCat action from your component, the saga is also emitting a setCat after it successfully runs. You're looping through the middleware.
To correct the issue, you should probably change the takeLatest to be looking for some kind of getCats action instead, then emit that action in your component. (not your saga).

Related

mqtt-react-hooks Subscriber stops working after one message

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.

Vue 3: How to implement a function that modifies all input fields in the DOM?

I'm new to Vue and want to add an onfocus function to all input fields. When I use mixin, the function is called every time a component is mounted.
createApp(App).mixin({
mounted() {
myFunction() {
document.querySelectorAll('input').doSomething()
}
}
}).mount('#app');
That makes sense and is in generally what I want, because newly added input fields should be affected, too. But then the function would iterate through the whole DOM every time a component is mounted, right? I want to avoid unnecessary iteration for fields that already have the onfocus function. So what would be best practice to do something like this?
import { createApp, h } from "vue";
import App from "./App.vue";
const app = createApp({
render: () => h(App)
});
app.mixin({
methods: {
myFunction() {
this.$el.querySelectorAll("input").forEach((el) => {
alert(el.getAttribute("name"));
});
}
},
mounted() {
this.myFunction();
}
});
app.mount("#app");

React component not receiving intermediate state when chaining actions in redux-saga

I have two actions TEST and TEST_DONE which both increment an id property in my redux state. I am using redux-saga to dispatch the second action TEST_DONE automatically whenever I dispatch the first action TEST from my component.
I expect the order of execution to go like this:
component renders with initial value of testState.id = 0
component dispatches TEST action
component re-renders with testState.id = 1
saga dispatches the TEST_DONE action
component re-renders with testState.id = 2
Instead my component only re-renders when testState.id is updated to 2. I can't see the 1 value in the getSnapshotBeforeUpdate function. It shows 0 as the previous prop.
Why does the prop jump from 0 to 2 without receiving 1 in between?
saga.js:
export function* TestSagaFunc() {
yield put({
type: actions.TEST_DONE
});
};
export default function* rootSaga() {
yield all([
yield takeEvery(actions.TEST, TestSagaFunc),
]);
};
action.js:
const actions = {
TEST: 'TEST',
TEST_DONE: 'TEST_DONE',
callTest: (id) => ({
type: actions.TEST,
payload: {
id
}
}),
};
export default actions;
reducer.js:
const initState = {
testState: {
id: 0
}
};
export default function TestReducers ( state=initState, { type, ...action}) {
switch(type) {
default:
return state;
case actions.TEST: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
};
case actions.TEST_DONE: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
}
};
};
console output from component getSnapshotBeforeUpdate
Summarizing my comments from the question:
The redux state is indeed being updated as you've seen, but a component is not guaranteed to render every intermediate state change based on the way react batches state changes. To test this you can try importing delay from redux-saga/effects and adding yield delay(1000); before calling yield put in TestSagaFunc so the two state updates don't get batched together.
This is just a trick to illustrate the effects of batching and almost certainly not what you want to do. If you need the intermediate state to be rendered you could dispatch TEST_DONE from the component being rendered with a useEffect (or componentDidUpdate) to ensure that the component went through one render cycle with the intermediate state. But there is no way to force your component to render intermediate reducer states that are batched together.

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!

Loading component data asynchronously server side with Mobx

I'm having an issue figuring out how to have a react component have an initial state based on asynchronously fetched data.
MyComponent fetches data from an API and sets its internal data property through a Mobx action.
Client side, componentDidMount gets called and data is fetched then set and is properly rendered.
import React from 'react';
import { observer } from 'mobx-react';
import { observable, runInAction } from 'mobx';
#observer
export default class MyComponent extends React.Component {
#observable data = [];
async fetchData () {
loadData()
.then(results => {
runInAction( () => {
this.data = results;
});
});
}
componentDidMount () {
this.fetchData();
}
render () {
// Render this.data
}
}
I understand that on the server, componentDidMount is not called.
I have something like this for my server:
import React from 'react';
import { renderToString } from 'react-dom/server';
import { useStaticRendering } from 'mobx-react';
import { match, RouterContext } from 'react-router';
import { renderStatic } from 'glamor/server'
import routes from './shared/routes';
useStaticRendering(true);
app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
if (err) {
console.log('Error', err);
res.status(500).send(err);
}
else if (redirect) {
res.redirect(302, redirect.pathname + redirect.search);
}
else if (props) {
const { html, css, ids } = renderStatic(() => renderToString(<RouterContext { ...props }/>));
res.render('../build/index', {
html,
css
});
}
else {
res.status(404).send('Not found');
}
})
})
I have seen many posts where an initial store is computed and passed through a Provider component. My components are rendered, but their state is not initialized. I do not want to persist this data in a store and want it to be locally scope to a component. How can it be done ?
For server side rendering you need to fetch your data first, then render. Components don't have a lifecycle during SSR, there are just render to a string once, but cannot respond to any future change.
Since your datafetch method is async, it means that it cannot ever affect the output, since the component will already have been written. So the answer is to fetch data first, then mount and render components, without using any async mechanism (promises, async etc) in between. I think separating UI and data fetch logic is a good practice for many reasons (SSR, Routing, Testing), see this blog.
Another approach is to create the component tree, but wait with serializing until all your promises have settled. That is the approach that for example mobx-server-wait uses.

Resources