Libraries used: redux and redux-saga.
On one of my components I have the useEffect hook that has the dependency set to the status of an API request. That useEffect is then triggering a toast notification based on whether the API request is successful or failed.
But, this useEffect is not firing on each yield put called by the redux-saga defined below.
My intention is the following:
Fire the useEffect in the component after the yield put requestSuccessful(...) or yield put requestFailed(...)
After that, fire the useEffect once again after the yield put initResponseFlags(...)
What's happening currently is that my useEffect is not called at all. I'm assuming it's probably because the yield(s) inside the saga change the redux state so fast (from false to true and then false again) that useEffect is not being able to catch the change.
Places.js (component)
const { places, message, waiting, success, error } = useSelector(state => ({
places: state.Places.data.places,
message: state.Places.requestStatus.message,
waiting: state.Places.requestStatus.waiting,
success: state.Places.requestStatus.success,
error: state.Places.requestStatus.error,
}));
...
useEffect(() => {
console.log("useEffect called");
if (success || error) {
// ... toast message ...
console.log("Firing");
}
}, [dispatch, success, error, message, waiting]);
My project uses the redux-saga library and it contains a generic saga for each API request being made in the app.
saga.js
function* requestStart(args, params) {
if (!params || !params.payload || !params.payload.action)
throw Object.assign(
new Error(`Get action params and payload have not been defined!`),
{ code: 400 }
);
let action = params.payload.action;
let actionPayload = params.payload.actionPayload;
let actions = params.payload.actions;
try {
if (process.env.REACT_APP_API_URL) {
if (actions?.beforeStart) {
yield put(putAction(actions?.beforeStart));
}
const { response, error } = yield call(action, actionPayload);
if (response !== undefined) {
yield put(requestSuccessful(args.namespace, response));
if (actions?.onSuccess) {
yield put(putAction(actions?.onSuccess, response));
}
} else {
yield put(requestFailed(args.namespace, error));
if (actions?.onFailure) {
yield put(putAction(actions?.onFailure, error));
}
}
}
} catch (error) {
yield put(requestFailed(args.namespace, error));
if (args.actions?.onFailure) {
yield put(putAction(params.actions?.onFailure, error));
}
} finally {
yield put(initResponseFlags(args.namespace));
}
}
actions.js
export const initResponseFlags = (namespace) => {
return {
type: `${namespace}/${INIT_RESPONSE_FLAGS}`,
};
};
export const requestStart = (namespace, payload) => {
return {
type: `${namespace}/${REQUEST_START}`,
payload: payload,
};
};
export const requestSuccessful = (namespace, payload) => {
return {
type: `${namespace}/${REQUEST_SUCCESSFUL}`,
payload: payload
};
};
export const requestFailed = (namespace, error) => {
return {
type: `${namespace}/${REQUEST_FAILED}`,
payload: error
};
};
reducer.js
import {
INIT_RESPONSE_FLAGS,
REQUEST_START,
REQUEST_SUCCESSFUL,
REQUEST_FAILED,
} from "./actionTypes";
const initialState = {
message: null,
waiting: false,
success: false,
error: false,
};
const RequestStatus = (namespace) => (state = initialState, action) => {
switch (action.type) {
case `${namespace}/${INIT_RESPONSE_FLAGS}`:
state = {
...state,
message: null,
waiting: false,
success: false,
error: false,
};
break;
case `${namespace}/${REQUEST_START}`:
state = {
...state,
waiting: true,
};
break;
case `${namespace}/${REQUEST_SUCCESSFUL}`:
state = {
...state,
waiting: false,
success: true,
error: false,
};
break;
case `${namespace}/${REQUEST_FAILED}`:
state = {
...state,
waiting: false,
message: action.payload,
success: false,
error: true
};
break;
default: break;
}
return state;
};
export default RequestStatus;
EDIT (12.10.2022. 11:08)
I've just inserted a yield delay(1); of 1ms before my yield put(initResponseFlags(...)); and it works now.
Alas, if there's some other solution (cause this seems like it's botched) - any help would be appreciated!
saga.js
function* requestStart(args, params) {
if (!params || !params.payload || !params.payload.action)
throw Object.assign(
new Error(`Get action params and payload have not been defined!`),
{ code: 400 }
);
let action = params.payload.action;
let actionPayload = params.payload.actionPayload;
let actions = params.payload.actions;
try {
if (process.env.REACT_APP_API_URL) {
if (actions?.beforeStart) {
yield put(putAction(actions?.beforeStart));
}
const { response, error } = yield call(action, actionPayload);
if (response !== undefined) {
console.log('kitica');
yield put(requestSuccessful(args.namespace, response));
if (actions?.onSuccess) {
yield put(putAction(actions?.onSuccess, response));
}
} else {
yield put(requestFailed(args.namespace, error));
if (actions?.onFailure) {
yield put(putAction(actions?.onFailure, error));
}
}
}
} catch (error) {
yield put(requestFailed(args.namespace, error));
if (args.actions?.onFailure) {
yield put(putAction(params.actions?.onFailure, error));
}
} finally {
yield delay(1);
yield put(initResponseFlags(args.namespace));
}
}
Related
I'm trying to test a useFetch custom hook. This is the hook:
import React from 'react';
function fetchReducer(state, action) {
if (action.type === `fetch`) {
return {
...state,
loading: true,
};
} else if (action.type === `success`) {
return {
data: action.data,
error: null,
loading: false,
};
} else if (action.type === `error`) {
return {
...state,
error: action.error,
loading: false,
};
} else {
throw new Error(
`Hello! This function doesn't support the action you're trying to do.`
);
}
}
export default function useFetch(url, options) {
const [state, dispatch] = React.useReducer(fetchReducer, {
data: null,
error: null,
loading: true,
});
React.useEffect(() => {
dispatch({ type: 'fetch' });
fetch(url, options)
.then((response) => response.json())
.then((data) => dispatch({ type: 'success', data }))
.catch((error) => {
dispatch({ type: 'error', error });
});
}, [url, options]);
return {
loading: state.loading,
data: state.data,
error: state.error,
};
}
This is the test
import useFetch from "./useFetch";
import { renderHook } from "#testing-library/react-hooks";
import { server, rest } from "../mocks/server";
function getAPIbegin() {
return renderHook(() =>
useFetch(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
{ method: "GET" },
1
)
);
}
test("fetch should return the right data", async () => {
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
// Overwrite mock with failure case
test("shows server error if the request fails", async () => {
server.use(
rest.get(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
async (req, res, ctx) => {
return res(ctx.status(500));
}
)
);
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
expect(result.current.data).toBe(null);
await waitForNextUpdate();
console.log(result.current);
expect(result.current.loading).toBe(false);
expect(result.current.error).not.toBe(null);
expect(result.current.data).toBe(null);
});
I keep getting an error only when running the test:
"Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
The error is coming from TestHook: node_modules/#testing-library/react-hooks/lib/index.js:21:23)
at Suspense
I can't figure out how to fix this. URL and options have to be in the dependency array, and running the useEffect doesn't change them, so I don't get why it's causing this loop. When I took them out of the array, the test worked, but I need the effect to run again when those things change.
Any ideas?
Try this.
function getAPIbegin(url, options) {
return renderHook(() =>
useFetch(url, options)
);
}
test("fetch should return the right data", async () => {
const url = "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin";
const options = { method: "GET" };
const { result, waitForNextUpdate } = getAPIbegin(url, options);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
I haven't used react-hooks-testing-library, but my guess is that whenever React is rendered, the callback send to RenderHook will be called repeatedly, causing different options to be passed in each time.
So I am making a connection to a MQTT broker via Redux. I have three actions, one making the connection, another one checking for error and one receiving the message.
Only the first one gets triggered and the other 2 do not trigger. The connection is successful.
Here is my code:
Actions
export const mqttConnectionInit = (topic) => {
return {
type: 'INIT_CONNECTION',
topic:topic
}
}
export const mqttConnectionState = (err = null) => {
return {
type: 'MQTT_CONNECTED',
payload: err
}
}
export const processMessage = (data) => dispatch => {
console.log('Receiving Message')
return {
type: 'MESSAGE_RECEIVED',
payload: data
}
}
Reducer
import { mqttConnectionState} from './mqttActions'
import { processMessage} from './mqttActions'
const initState = {
client: null,
err: null,
message : 'message'
}
const createClient = (topic) => {
const mqtt = require('mqtt')
const client = mqtt.connect('ws://localhost:9001');
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(topic, (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: ` + topic)
console.log(granted)
});
});
//messages recevied during subscribe mode will be output here
client.on('message', function (topic, message) {
// message is Buffer
console.log(message.toString())
processMessage({topic, message})
// client.end() will stop the constant flow of values
})
return client;
}
const mqttReducer = (state = initState, action) =>{
switch (action.type) {
case 'INIT_CONNECTION':
return {
...state,
client: createClient(action.topic)
}
case 'MQTT_CONNECTED':
return {
...state,
err: action.payload
}
case 'MESSAGE_RECEIVED':
return {
...state,
message: action.payload //payload:data
}
default:
return state
}
}
export default mqttReducer
Why mqttConnectionState and processMessage do not get triggered?
You can never call async logic from within a reducer! Your createClient method is entirely async logic, and so it cannot go in a reducer.
In addition, you should not put non-serializable values into the Redux store.
Instead, we recommend that persistent connections like sockets should go into middleware.
I try to error handling with redux saga. Far now its always returning 404 error when i changed the api url. I organized my reducer and redux saga. but ı can't reach to fetching item
This is my Reducer
const homeReducer = (state : Object = initialState, action : Object) => {
switch (action.type) {
case HOME.GET_MOVIES_START:
return {
...state,
ProgramsLoading: true,
fetching: true
};
case HOME.GET_MOVIES_FINISH:
return {
...state,
ProgramsLoading: false,
programs: action.programs,
fetching: true
};
case HOME.GET_MOVIES_REJECTED:
return {
...state,
ProgramsLoading: false,
fetching: false
};
default:
return state;
}
};
And It's my Redux Saga. There is call api with axios. And manipulate to data in there
function* getPrograms() {
const { data } = yield call(axios.get, "http://localhost:3000/entries");
const fetching = false;
const defaultValue = {
SeriesFilteredData: [],
MoviesFilteredData: []
};
const reducerFunction = (accumulator, currentValue) => {
if (currentValue.releaseYear < 2010) {
return accumulator;
}
if (currentValue.programType === "series") {
accumulator.SeriesFilteredData.push(currentValue);
}
else if (currentValue.programType === "movie") {
accumulator.MoviesFilteredData.push(currentValue);
}
return accumulator;
};
const results = data.reduce(reducerFunction, defaultValue);
if (results) {
yield put(homeActions.getProgramsFinish(results));
}
else {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
function* homeFlow() {
console.log(getPrograms, "getPrograms");
yield call(delay, 2000);
yield call(getPrograms);
}
export default function* homeSaga() {
yield takeEvery(HOME.GET_MOVIES_START, homeFlow);
}
How Can I handle to any errors ?
Place all your getPrograms function in a try catch and trigger (put) the homeActions.getProgramsRejected({ ProgramsLoading: false }) action in the catch block
function* getPrograms() {
try {
const { data } = yield call(axios.get, "http://localhost:3000/entries");
// ... the rest of your code
if (results) {
yield put(homeActions.getProgramsFinish(results));
}
else {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
catch(error) {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
Then you can improve it avoiding to write two identical yield put(homeActions.getProgramsRejected({ ProgramsLoading: false })); calls. Take a look
function* getPrograms() {
try {
const { data } = yield call(axios.get, "http://localhost:3000/entries");
// ... the rest of your code
if (results) {
yield put(homeActions.getProgramsFinish(results));
}
else {
throw new Error('No results');
}
}
catch(error) {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
if the axios call fails the catch block puts the homeActions.getProgramsRejected action
if it doesn't fail but there aren't results (your initial management) it throws a new error and, again, the catch block puts the homeActions.getProgramsRejected action
Let me know if it helps you 😉
I am building an react application to connect to and display data from a MQTT server.
I have implemented the basic connection code in mqtt/actions.js See below:
const client = mqtt.connect(options);
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(['btemp', 'otemp'], (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: otemp & btemp topics`)
})
})
client.on('message', function (topic, message) {
updateTemp({topic: topic, value: message.toString()})
});
const mqttConnectionState = (action, err = null) => {
return {
type: action,
payload: err
}
}
I am looking to on button press initiate the mqtt connection and then dispatch a connection success event.
However with the above code I am unsure exactly how this would work.
I could move the connect line const client = mqtt.connect(options); to a function and run that function on button click but then then the client.on functions will not be able to see the client const.
How is best to approach this?
I am using React.JS, Redux and the MQTT.JS libraries.
Update: Trying to dispatch and action when a message is received
Reducer:
const createClient = () => {
const client = mqtt.connect(options);
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(['btemp', 'otemp'], (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: otemp & btemp topics`)
});
});
client.on('message', (topic, message) => {
console.log('message received from mqtt')
processMessage({topic, message})
})
return client;
}
case MESSAGE_RECEIVED:
console.log('message received')
messageReceived(payload)
return state;
Actions:
export const processMessage = (data) => dispatch => {
console.log('Processing Message')
return {
type: 'MESSAGE_RECEIVED',
payload: data
}
}
message received from mqtt log each time a message arrives, however processMessage({topic, message}) never executes as Processing Message never logs to the console
"Actions are payloads of information that send data from your application to your store" (docs)
So you have to create the client in the Reducer (his function). Put it on the Redux state like this:
initialState = {
client: null
}
and you reducer.js file should look like this:
import {
mqttConnectionState
} from './actions'
let initialState = {
client: null ,
err: null
}
const createClient = () => {
const client = mqtt.connect(options);
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(['btemp', 'otemp'], (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: otemp & btemp topics`)
});
});
return client;
}
function app(state = initialState, action) {
switch (action.type) {
case 'INIT_CONNECTION':
return {
...state,
client: createClient()
})
case 'MQTT_CONNECTED':
return {
...state,
err: action.payload
})
default:
return state
}
}
and you actions.js:
...
const mqttConnectionInit = () => {
return {
type: 'INIT_CONNECTION'
}
}
const mqttConnectionState = (err = null) => {
return {
type: 'MQTT_CONNECTED',
payload: err
}
}
...
this way you can dispatch the action mqttConnectionInit in the onclick button event.
Dispatching Redux actions inside a reducer may clear your store. I mean, setting it's state to what you have under initialState. And require cycles are no-op.
Yesterday I did some changes in my code, because I've tried solution above and ended up with a warning "require cycles are allowed but can result in uninitialized values". I moved mqtt connection related code into the middleware.
import { DEVICE_TYPE, HOST, PASSWORD, PORT, USER_NAME } from '../utils/variables';
import { mqttConnectionInit, mqttConnectionState } from '../actions/devices';
import mqtt from 'mqtt/dist/mqtt';
import { SIGNED_IN } from '../constants/types';
const MqttMiddleware = store => next => action => {
if (action.type == SIGNED_IN) {
store.dispatch(mqttConnectionInit());
const client = mqtt.connect(`ws://${HOST}:${PORT}`, { username: USER_NAME, password: PASSWORD });
client.on('connect', function () {
let license = store.getState().auth.license;
store.dispatch(mqttConnectionState(client));
client.subscribe(`/${USER_NAME}/${license}/+/${DEVICE_TYPE}/#`);
});
client.on('message', ((topic, payload) => {
const device = JSON.parse(message(topic, payload.toString()));
console.log(device);
}));
}
next(action);
};
export function message(message, value) {
const msg = message.split('/');
return JSON.stringify({
"id": msg[3],
"user": msg[1],
"license": msg[2],
"type": msg[4],
"name": msg[5],
"value": value == "0" ? 0 : (value.match(/[A-Za-z]/) ? value : Number(value))
});
}
export default MqttMiddleware;
You can do pretty much all you want with the store.
actions.js
import { INIT_CONNECTION, MQTT_CONNECTED } from '../constants/types'
export const mqttConnectionInit = () => {
return {
type: INIT_CONNECTION
}
}
export const mqttConnectionState = (client, err = null) => {
return {
type: MQTT_CONNECTED,
error: err,
client: client,
}
}
reducers.js
import { INIT_CONNECTION, MQTT_CONNECTED } from '../constants/types';
const mqttReducer = (state = initialState, action) => {
switch (action.type) {
case INIT_CONNECTION:
return {
...state,
client: null,
};
case MQTT_CONNECTED:
return {
...state,
err: action.error,
client: action.client,
};
default:
return state;
}
}
const initialState = {
client: null,
err: null,
}
export default mqttReducer;
action.js
export function getLoginStatus() {
return async(dispatch) => {
let token = await getOAuthToken();
let success = await verifyToken(token);
if (success == true) {
dispatch(loginStatus(success));
} else {
console.log("Success: False");
console.log("Token mismatch");
}
return success;
}
}
component.js
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
});
}
However, when I write component.js code with async/await like below I get this error:
Possible Unhandled Promise Rejection (id: 0): undefined is not a function (evaluating 'this.props.dispatch(splashAction.getLoginStatus())')
component.js
async componentDidMount() {
let success = await this.props.dispatch(splashAction.getLoginStatus());
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
}
How do I await a getLoginStatus() and then execute the rest of the statements?
Everything works quite well when using .then(). I doubt something is missing in my async/await implementation. trying to figure that out.
The Promise approach
export default function createUser(params) {
const request = axios.post('http://www...', params);
return (dispatch) => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
request.then(success => onSuccess, error => onError);
};
}
The async/await approach
export default function createUser(params) {
return async dispatch => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
try {
const success = await axios.post('http://www...', params);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
}
Referenced from the Medium post explaining Redux with async/await: https://medium.com/#kkomaz/react-to-async-await-553c43f243e2
Remixing Aspen's answer.
import axios from 'axios'
import * as types from './types'
export function fetchUsers () {
return async dispatch => {
try {
const users = await axios
.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => res.data)
dispatch({
type: types.FETCH_USERS,
payload: users,
})
} catch (err) {
dispatch({
type: types.UPDATE_ERRORS,
payload: [
{
code: 735,
message: err.message,
},
],
})
}
}
}
import * as types from '../actions/types'
const initialErrorsState = []
export default (state = initialErrorsState, { type, payload }) => {
switch (type) {
case types.UPDATE_ERRORS:
return payload.map(error => {
return {
code: error.code,
message: error.message,
}
})
default:
return state
}
}
This will allow you to specify an array of errors unique to an action.
Another remix for async await redux/thunk. I just find this a bit more maintainable and readable when coding a Thunk (a function that wraps an expression to delay its evaluation ~ redux-thunk )
actions.js
import axios from 'axios'
export const FETCHING_DATA = 'FETCHING_DATA'
export const SET_SOME_DATA = 'SET_SOME_DATA'
export const myAction = url => {
return dispatch => {
dispatch({
type: FETCHING_DATA,
fetching: true
})
getSomeAsyncData(dispatch, url)
}
}
async function getSomeAsyncData(dispatch, url) {
try {
const data = await axios.get(url).then(res => res.data)
dispatch({
type: SET_SOME_DATA,
data: data
})
} catch (err) {
dispatch({
type: SET_SOME_DATA,
data: null
})
}
dispatch({
type: FETCHING_DATA,
fetching: false
})
}
reducers.js
import { FETCHING_DATA, SET_SOME_DATA } from './actions'
export const fetching = (state = null, action) => {
switch (action.type) {
case FETCHING_DATA:
return action.fetching
default:
return state
}
}
export const data = (state = null, action) => {
switch (action.type) {
case SET_SOME_DATA:
return action.data
default:
return state
}
}
Possible Unhandled Promise Rejection
Seems like you're missing the .catch(error => {}); on your promise. Try this:
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
})
.catch(err => {
console.error(err.getMessage());
}) ;
}
use dispatch(this.props.splashAction.getLoginStatus()) instead this.props.dispatch(splashAction.getLoginStatus())