React Redux - How to make a double dispatch - reactjs

I'm fetch some data from my API and it correctly works. But when a double dispatch on the same page the API doesn't work anymore. It's better code to explain it:
Server:
router.get("/", (req, res) => {
let sql = "SELECT * FROM design_categories";
let query = connection.query(sql, (err, results) => {
if (err) throw err;
res.header("Access-Control-Allow-Origin", "*");
res.send(results);
});
});
router.get("/", (req, res) => {
let sql = "SELECT * FROM food_categories";
let query = connection.query(sql, (err, results) => {
if (err) throw err;
res.header("Access-Control-Allow-Origin", "*");
res.send(results);
});
});
They work.
action.js
export const fetchDesignCat = () => {
setLoading()
return async dispatch => {
const response = await axios
.get("http://localhost:5000/api/designcategories")
.then(results => results.data)
try {
await dispatch({ type: FETCH_DESIGN_CAT, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
export const fetchFoodCat = () => {
setLoading()
return async dispatch => {
const response = await axios
.get("http://localhost:5000/api/foodcategories")
.then(results => results.data)
try {
await dispatch({ type: FETCH_FOOD_CAT, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
Both of them work perfectly.
reducer.js
const initalState = {
db: [],
loading: true,
designcat: [],
foodcat: [],
}
export default (state = initalState, action) => {
switch (action.type) {
// different cases
case FETCH_DESIGN_CAT:
return {
designcat: action.payload,
loading: false,
}
case FETCH_FOOD_CAT:
return {
food: action.payload,
loading: false,
}
}
The reducer updates the states perfectly.
Page settings.js
const Settings = ({ designcat, foodcat, loading }) => {
const dispatch = useDispatch()
// ... code
useEffect(() => {
dispatch(fetchDesignCat()) // imported action
dispatch(fetchFoodCat()) // imported action
// eslint-disable-next-line
}, [])
// ... code that renders
const mapStateToProps = state => ({
designcat: state.appDb.designcat,
foodcat: state.appDb.foodcat,
loading: state.appDb.loading,
})
export default connect(mapStateToProps, { fetchDesignCat, fetchFoodCat })(
Settings
)
Now there's the problem. If I use just one dispatch it's fine I get one or the other. But if I use the both of them look like the if the second overrides the first. This sounds strange to me.
From my ReduxDevTools
For sure I'm mistaking somewhere. Any idea?
Thanks!

Your reducer does not merge the existing state with the new state, which is why each of the actions just replace the previous state. You'll want to copy over the other properties of the state and only replace the ones your specific action should replace. Here I'm using object spread to do a shallow copy of the previous state:
export default (state = initalState, action) => {
switch (action.type) {
case FETCH_DESIGN_CAT:
return {
...state, // <----
designcat: action.payload,
loading: false,
}
case FETCH_FOOD_CAT:
return {
...state, // <----
food: action.payload,
loading: false,
}
}
}
Since the code is abbreviated, I'm assuming you're handling the default case correctly.
As an additional note, since you're using connect with the Settings component, you don't need to useDispatch and can just use the already connected action creators provided via props by connect:
const Settings = ({
designcat,
foodcat,
loading,
fetchDesignCat,
fetchFoodCat,
}) => {
// ... code
useEffect(() => {
fetchDesignCat();
fetchFoodCat();
}, [fetchDesignCat, fetchFoodCat]);
// ... code that renders
};
There's also a race condition in the code which may or may not be a problem to you. Since you start both FETCH_DESIGN_CAT and FETCH_FOOD_CAT at the same time and both of them set loading: false after finishing, when the first of them finishes, loading will be false but the other action will still be loading its data. If this case is known and handled in code (i.e., you don't trust that both items will be present in the state if loading is false) that's fine as well.
The solution to that would be either to combine the fetching of both of these categories into one thunk, or create separate sub-reducers for them with their own loading state properties. Or of course, you could manually set and unset loading.

Related

Redux store cannot update

I have a button trigger project create. What I want to do is when I click button, it will create a new project, and if there is no issue with product, it will go to the next step.
The dispatch on the click handler is working, I can see the redux runs the faild reducer if there is error, but the projectData.successStatus inside the handler cannot get the latest value which the the successStatus I want it to be false. It's still the previsous retrive project list success Status. So the nextStep() condition is not working.
Can someone help me find what's wrong?
This is the handler button:
const handleNextButton = useCallback(() => {
if (newProjectName) {
const newProjectWithProjectName = {
...newProject,
projectName: newProjectName,
}
dispatch(createNewProjectReq(newProjectWithProjectName)) // create new project
if (projectData.successStatus) {
nextStep()
}
}
}, [newProjectName, projectData])
On the action, I have request, add, fail:
export const createNewProjectReq = (newProject) => async (dispatch) => {
dispatch({ type: PROJECT_SENDING_REQUEST })
try {
const result = await createNewProject(newProject)
const { project, message } = result.data.data
dispatch({
type: PROJECT_LIST_ADD,
payload: { project, message },
})
} catch (error) {
dispatch({ type: PROJECT_REQUEST_FAIL, payload: error.data.message })
}
}
Reducer switch:
switch (action.type) {
case PROJECT_SENDING_REQUEST:
console.log("PROJECT_SENDING_REQUEST")
return {
...state,
loading: true,
successStatus: false,
}
case PROJECT_LIST_SUCCESS:
console.log("PROJECT_LIST_SUCCESS")
return {
loading: false,
projects: action.payload.projectListGroupByProjectId,
successStatus: true,
message: action.payload.message,
}
case PROJECT_LIST_ADD:
console.log("PROJECT_LIST_ADD")
return {
...state,
loading: false,
projects: [...state.projects, action.payload.project],
successStatus: true,
message: action.payload.message,
}
case PROJECT_REQUEST_FAIL: {
console.log("PROJECT_REQUEST_FAIL")
return {
...state,
loading: false,
successStatus: false,
message: action.payload,
}
}
default:
return state
}
Issues
handleNextButton callback is synchronous code.
Reducer functions are also synchronous code.
State is generally considered const during a render cycle, i.e. it won't ever change in the middle of a render cycle or synchronous code execution.
Because of these reason the projectData state will not have been updated yet, the conditional check happens before the actions are processed.
Solution
Since you really are just interested in the success of the action so you can go to the next step you can return a boolean value from the asynchronous action and await it or Promise-chain from it.
export const createNewProjectReq = (newProject) => async (dispatch) => {
dispatch({ type: PROJECT_SENDING_REQUEST });
try {
const result = await createNewProject(newProject);
const { project, message } = result.data.data;
dispatch({
type: PROJECT_LIST_ADD,
payload: { project, message },
});
return true; // <-- success status
} catch (error) {
dispatch({ type: PROJECT_REQUEST_FAIL, payload: error.data.message });
return false; // <-- failure status
}
}
...
const handleNextButton = useCallback(() => {
if (newProjectName) {
const newProjectWithProjectName = {
...newProject,
projectName: newProjectName,
}
dispatch(createNewProjectReq(newProjectWithProjectName))
.then(success => {
if (success) {
nextStep();
}
});
}
}, [newProjectName, projectData]);
Demo
Simple demo with click handler calling asynchronous function with 50% chance to succeed/fail.

useEffect infinite loop occurs only while testing, not otherwise - despite using useReducer

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.

How to use an action within reducer?

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.

Best Practice for handling consecutive identical useFetch calls with React Hooks?

Here's the useFetch code I've constructed, which is very much based upon several well known articles on the subject:
const dataFetchReducer = (state: any, action: any) => {
let data, status, url;
if (action.payload && action.payload.config) {
({ data, status } = action.payload);
({ url } = action.payload.config);
}
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: data,
status: status,
url: url
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
data: null,
status: status,
url: url
};
default:
throw new Error();
}
}
/**
* GET data from endpoints using AWS Access Token
* #param {string} initialUrl The full path of the endpoint to query
* #param {JSON} initialData Used to initially populate 'data'
*/
export const useFetch = (initialUrl: ?string, initialData: any) => {
const [url, setUrl] = useState<?string>(initialUrl);
const { appStore } = useContext(AppContext);
console.log('useFetch: url = ', url);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
status: null,
url: url
});
useEffect(() => {
console.log('Starting useEffect in requests.useFetch', Date.now());
let didCancel = false;
const options = appStore.awsConfig;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
let response = {};
if (url && options) {
response = await axios.get(url, options);
}
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
// We won't force an error if there's no URL
if (!didCancel && url !== null) {
dispatch({ type: 'FETCH_FAILURE', payload: error.response });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url, appStore.awsConfig]);
return [state, setUrl];
}
This seems to work fine except for one use case:
Imagine a new Customer Name or UserName or Email Address is typed in - some piece of data that has to be checked to see if it already exists to ensure such things remain unique.
So, as an example, let's say the user enters "My Existing Company" as the Company Name and this company already exists. They enter the data and press Submit. The Click event of this button will be wired up such that an async request to an API Endpoint will be called - something like this: companyFetch('acct_mgmt/companies/name/My%20Existing%20Company')
There'll then be a useEffect construct in the component that will wait for the response to come back from the Endpoint. Such code might look like this:
useEffect(() => {
if (!companyName.isLoading && acctMgmtContext.companyName.length > 0) {
if (fleetName.status === 200) {
const errorMessage = 'This company name already exists in the system.';
updateValidationErrors(name, {type: 'fetch', message: errorMessage});
} else {
clearValidationError(name);
changeWizardIndex('+1');
}
}
}, [companyName.isLoading, companyName.isError, companyName.data]);
In this code just above, an error is shown if the Company Name exists. If it doesn't yet exist then the wizard this component resides in will advance forward. The key takeaway here is that all of the logic to handle the response is contained in the useEffect.
This all works fine unless the user enters the same Company Name twice in a row. In this particular case, the url dependency in the companyFetch instance of useFetch does not change and thus there is no new request sent to the API Endpoint.
I can think of several ways to try to solve this but they all seem like hacks. I'm thinking that others must have encountered this problem before and am curious how they solved it.
Not a specific answer to your question, more of another approach: You could always provide a function to trigger a refetch by the custom hook instead of relying of the useEffect to catch all different cases.
If you want to do that, use useCallback in your useFetch so you don't create an endless loop:
const triggerFetch = useCallback(async () => {
console.log('Starting useCallback in requests.useFetch', Date.now());
const options = appStore.awsConfig;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
let response = {};
if (url && options) {
response = await axios.get(url, options);
}
dispatch({ type: 'FETCH_SUCCESS', payload: response });
} catch (error) {
// We won't force an error if there's no URL
if (url !== null) {
dispatch({ type: 'FETCH_FAILURE', payload: error.response });
}
}
};
fetchData();
}, [url, appStore.awsConfig]);
..and at the end of the hook:
return [state, setUrl, triggerFetch];
You can now use triggerRefetch() anywhere in your consuming component to programmatically refetch data instead of checking every case in the useEffect.
Here is a complete example:
CodeSandbox: useFetch with trigger
To me this slightly related to thing "how to force my browser to skip cache for particular resource" - I know, XHR is not cached, just similar case. There we may avoid cache by providing some random meaningless parameter in URL. So you can do the same.
const [requestIndex, incRequest] = useState(0);
...
const [data, updateURl] = useFetch(`${url}&random=${requestIndex}`);
const onSearchClick = useCallback(() => {
incRequest();
}, []);

Dispatch multiple actions using Redux Thunk and the await/async syntax to track loading

I am currently editing some reducers to be able to track the loading state of axios operations. Most of my async syntax is written in async/await fashion and would like to keep it that way to keep my code organized.
I am not sure how to dispatch two action creators one after the other: the first one to fire off the FETCHING_USER action type and keep track of the reduced isFetching state, while the other one to fire off the actual axios GET request. The code currently looks like this to get the API request:
export const fetchUser = () => async dispatch => {
const res = await axios.get(`${API_URL}/api/current_user`, {
headers: { authorization: localStorage.getItem("token") }
});
dispatch({ type: FETCH_USER, payload: res.data });
};
I am not sure how to dispatch the FETCHING_USER and then fire off the FETCH_USER action.
First you need to modify your reducer to have isFetching statement and requesting and receiving data cases:
const INITIAL_STATE = { isFetching: false, data: [] };
export default(state = INITIAL_STATE, action) => {
switch(action.type) {
case REQUEST_USER: {
return {...state, isFetching: true};
}
case RECEIVE_USER: {
return {...state, isFetching: false, data: action.payload};
}
default: return state;
}
}
Then modify your action to use try/catch statements:
export const fetchUser = () => async dispatch => {
dispatch({ type: REQUEST_USER });
try {
const res = await axios.get(`${API_URL}/api/current_user`, {
headers: { authorization: localStorage.getItem("token") }
});
dispatch({ type: RECEIVE_USER, payload: res.data });
}
catch(e){
//dispatch your error actions types, (notifications, etc...)
}
};
Then in component you can use condition like: isFetching ? //show loader : //show content (data[])

Resources