I have used useEffects in my components to load data, moment the component mounts. But i am trying to optimize my code by avoiding any memmory leaks. To achieve this i am trying to use AbortController to cancel any request in any case if the component unmounts. Something like this
useEffect(() => {
let abortController;
(async () {
abortController = new AbortController();
let signal = abortController.signal;
// the signal is passed into the request(s) we want to abort using this controller
const { data } = await axios.get(
'https://random-data-api.com/api/company/random_company',
{ signal: signal }
);
setCompany(data);
})();
return () => abortController.abort();
}, []);
But i am finding it difficult to implement this because my axios request is in a service file which is called by a reducer in slice file.
Below is my useEffect of my Component.
// Component.js
import { bookDetails } from '../../features/user/userSlice'
//import reducer from my slice file
.
.
// code
useEffect(() => {
let mounted = true
if (mounted) {
dispatch(bookDetails(bookId))
}
return () => mounted = false
}, [])
Below is my reducer from my slice file which imports function from my service file.
// userSlice.js
import userService from "./userService";
export const bookDetails = createAsyncThunk(
"user/book",
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await userService.bookDetails({ id, token });
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
Below is my function from my service file
// userService.js
const bookDetails = async ({ id, token }) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.get(API_URL + `/book/${id}`, config);
return response.data;
};
I want to cancel this request in case my component unmounts from useEffect. Please Help. Thanks in advance.
Since you're using Redux Toolkit's createAsyncThunk, it sounds like you're looking for the "canceling while running" feature of createAsyncThunk. Perhaps something like the following:
export const bookDetails = createAsyncThunk(
"user/book",
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await userService.bookDetails({ id, token, signal: thunkAPI.signal });
} catch (error) {
return thunkAPI.rejectWithValue(getErrorMessage(error));
}
}
);
useEffect(() => {
const promise = dispatch(bookDetails(bookId));
return () => promise.abort();
}, [])
However, if all you're worried about is fetching data, I strongly suggest taking a look at RTK Query, React Query, or SWR. These take care of the common pattern of asynchronously fetching data, without making you write the various slices and reducers yourself, and add useful features such as caching, retrying, etc.
Make your bookDetails method accept an extra property named signal. Then pass it to the axios method.
const bookDetails = async ({ id, token, signal }) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
signal,
};
const response = await axios.get(API_URL + `/book/${id}`, config);
return response.data;
};
For this purpose, change your reducer method like this:
async ({ id, signal}, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await userService.bookDetails({ id, token, signal });
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
Finally dispatch actions like this:
abortController = new AbortController();
let signal = abortController.signal;
dispatch(bookDetails({ id: bookId, signal: signal ))
it's a good idea to have an AbortController to avoid memory leaks when using useEffect to load data this will be useful when its possible to get errors during dispatching requests to avoid requests still running when an error occurs, here's an example that you can use to cancel requests if the state doesn't load for any reason.
import { useEffect, useState } from 'react';
const MyComponent = () => {
const [data, setData] = useState(null); // set state to null
useEffect(() => {
const controller = new AbortController(); // AbortController instance
// Dispatch the request
fetch('/data', { signal: controller.signal })
.then(response => response.json()) // get response
.then(data => setData(data)) // Updating useState
.catch(e => { // Catch errors
if (e.name === 'AbortError') {
console.log("Failed to fetching data");
}
throw e;
});
// cleanup controller
return () => {
controller.abort();
};
}, []);
// Render the component
return <div>{data && data.message}</div>;
};
Related
Here I have a useState for posts of my blog webapp. I am getting the posts from the mongoose back end which works fine. But the second I set the posts variable I get an infinite loop of rerendering.
Here is the app.jsx:
import React, {useState} from 'react';
import './App.css';
import Nav from './Navbar/NavBar.jsx';
import Content from "./content/Content"
import { getPosts } from "./api/post";
function App() {
const idList = ["631a58c165b3a10ac71497e1", "631a58fa65b3a10ac71497e3"];
const [posts, setPosts] = useState([]);
setPosts(async() => await getPosts(idList));
return (
<div>
hello
</div>
);
}
export default App;
Here is the axios part:
import axios from "axios";
const getPost = (id) => {
new Promise((resolve, reject) => {
const url = "/posts/" + id
console.log(url)
axios.get(url)
.then(response => {
if (response.status === 200){
console.log("Succesfull call");
console.log(response.data);
resolve(response.data);
}
else if(response.status === 404){
reject(response);
}
})
.catch(err => {
console.log("Failed call");
reject(err);
})
});
};
const getPosts = async (idList) => {
var result = []
for(const id in idList){
try{
let post = await getPost(idList[id]);
result.push(post);
}catch(err){
console.log(err.message);
}
}
if (result.length === 0) throw {message: "No posts"};
else return result;
};
export {getPosts};
How can I run setPosts async so that the site wont refresh infinitely?
You cannot pass a function returning a promise into the state setter. It must return the new state value directly.
You'll want to use an effect hook instead
useEffect(() => {
getPosts(idList).then(setPosts);
}, []); // empty dependency array means run once on mount
Given idList appears constant, you should define it outside your component. Otherwise you'll need to work around its use as a dependency in the effect hook.
Your getPosts function falls prey to the explicit promise construction anti-pattern. Since Axios already returns a promise, you don't need to make a new one.
You can also use Promise.allSettled() to make your requests in parallel.
const getPost = async (id) => (await axios.get(`/posts/${id}`)).data;
const getPosts = async (idList) => {
const results = (await Promise.allSettled(idList.map(getPost)))
.filter(({ status }) => status === "fulfilled")
.map(({ value }) => value);
if (results.length === 0) {
throw new Error("No posts");
}
return results;
};
I am making multiple dispatch call in the homepage
Its was something like this.
useEffect(() => {
async function getStorageData() {
setLoading(true);
try {
await dispatch(fetchProductA());
await dispatch(fetchProductB());
await dispatch(fetchProductC());
await dispatch(fetchProductD());
await dispatch(fetchProductE());
} catch (err) {
console.log(err);
} finally {
setLoading(false);
}
}
getStorageData();
}, []);
The problem is that when calling the these api. I got an error when productC.
So I made a dispatch call.
I try throwing an error but that not work because when I throw an error in productC the remaining product D and E will not be called because throw end the dispatch calling
Here is my api call.
export const fetchProductC = () => dispatch => {
return axios
.get('productsapi/fetchProductC', {
headers: {
'Content-Type': 'application/json',
},
})
.then(res => {
dispatch({
type: FETCH_NEW_PRODUCTS,
payload: res.data[0]
});
})
.catch(err => {
dispatch({
type: EMPTY_NEW_PRODUCTS,
});
console.log('fetching new product error');
//throw err;
});
};
Here is the reducer
case FETCH_NEW_PRODUCTS:
return {
...state,
listC: action.payload,
};
case EMPTY_NEW_PRODUCTS:
return {
...state,
listC: [],
};
Your code after ProductC can't run because it is skipped via catch.
You can write your code like below.
await dispatch(fetchProductA()).catch(handleErr);
await dispatch(fetchProductB()).catch(handleErr);
await dispatch(fetchProductC()).catch(handleErr);
await dispatch(fetchProductD()).catch(handleErr);
await dispatch(fetchProductE()).catch(handleErr);
You should never await a dispatch in the component. Read tutorials and refactor your data flow: Redux Async Data Flow, Async Logic and Data Fetching
Redux recommends you to use thunk to do this:
// fetchTodoById is the "thunk action creator"
export function fetchTodoById(todoId) {
// fetchTodoByIdThunk is the "thunk function"
return async function fetchTodosThunk(dispatch, getState) {
// dispatch an action to set a loading state
dispatch(/*...*/)
const response = await client.get(`/fakeApi/todo/${todoId}`)
// use your response and set a success state here
dispatch(todosLoaded(response.todos))
}
}
// Your component
function TodoComponent({ todoId }) {
const dispatch = useDispatch()
const { data, state } = useSelector(/* your selector */)
const onFetchClicked = () => {
// Calls the thunk action creator, and passes the thunk function to dispatch
dispatch(fetchTodoById(todoId))
}
}
I have a todo app. Im trying to use context api(first time). I have add, delete and get functions in context. I can use add and delete but cant return the get response to state. It returns promise if i log; context. Im using async await. I tried almost everything i know but cant solve it. Where is my fault ?
Thank you.
task-context.js
import React, { useReducer } from "react";
import TaskContext from "./task-actions";
import { TaskReducer, ADD_TASK, GET_TASKS, REMOVE_TASK } from "./reducers";
const GlobalState = (props) => {
const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
const addTask = (task) => {
dispatch({ type: ADD_TASK, data: task });
};
const removeTask = (taskId) => {
dispatch({ type: REMOVE_TASK, data: taskId });
};
const getTasks = () => {
dispatch({ type: GET_TASKS });
};
return (
<TaskContext.Provider
value={{
tasks: tasks,
getTasks: getTasks,
addTask: addTask,
removeTask: removeTask,
}}
>
{props.children}
</TaskContext.Provider>
);
};
export default GlobalState;
reducers.js
import taskService from "../Services/tasks-service";
export const ADD_TASK = "ADD_TASK";
export const GET_TASKS = "GET_TASKS";
export const REMOVE_TASK = "REMOVE_TASK";
const addTask = async (data, state) => {
console.log("Adding : " + data.title);
try {
let task = {
title: data.title,
description: data.description,
comment: data.comment,
progress: data.status
};
const res = await taskService.addNewTask(task);
console.log(res);
if (res) {
getTasks();
}
} catch (err) {
console.log(err);
}
return;
};
const getTasks = async () => {
let response = {}
try {
const res = await taskService.loadTasks();
response = res.data
} catch (err) {
console.log(err);
}
return { tasks: response }
};
const removeTask = async (data) => {
try {
await taskService.deleteTask(data.id);
} catch (err) {
console.log(err);
}
};
export const TaskReducer = (state, action) => {
switch (action.type) {
case ADD_TASK:
return addTask(action.data);
case GET_TASKS:
console.log(getTasks());
return getTasks();
case REMOVE_TASK:
return removeTask(action.data);
default:
return state;
}
};
task-actions.js
import React from "react";
export default React.createContext({
addTask: (data) => {},
removeTask: (data) => {},
getTasks: () => {}
});
To start with, you are getting promises returned because you are explicitly returning promises: return addTask(action.data). All your actions are returning promises into the reducer.
A reducer should be a pure function, meaning that it does not have any side effects (call code outside its own scope), or contain any async functionality, and it should return the same data given the same inputs every single time. You've essentially got the workflow back to front.
There's a lot to unpick here so I'm going to provide pseudocode rather than try and refactor the entire service, which you will have a more complete understanding of. Starting with the reducer:
export const TaskReducer = (state, action) => {
switch (action.type) {
case ADD_TASK:
return [...state, action.data];
case GET_TASKS:
return action.data;
case REMOVE_TASK:
return state.filter(task => task.id !== action.data.id);
default:
return state;
}
};
This reducer describes how the state is updated after each action is complete. All it should know how to do is update the state object/array it is in charge of. When it comes to fetching data, calling the reducer should be the very last thing you have to do.
Now on to the actions. The add action is a problem because its not actually returning any data. On top of that, it calls getTasks when really all it ought to do is return one added task (which should be getting returned from await taskService.addNewTask). I would expect that res.data is actually a task object, in which case:
export const addTask = async (data) => {
try {
const task = {
title: data.title,
description: data.description,
comment: data.comment,
progress: data.status
};
const res = await taskService.addNewTask(task);
return res.data;
} catch (err) {
return err;
}
};
Similarly for getTasks, I'm going to assume that await taskService.loadTasks returns an array of task objects. In which case, we can simplify this somewhat:
export const getTasks = async () => {
try {
const res = await taskService.loadTasks();
return res.data;
} catch (err) {
return err;
}
};
Your removeTask action is essentially fine, although you will want to return errors instead of just logging them.
Notice we're now exporting these actions. That is so we can now call them from within GlobalState. We're running into issues with name collision so I've just underscored the imported actions for demo purposes. In reality, it might be better to move all the functionality we did in the last step into your taskService, and import that straight into GlobalState instead. Since that's implementation specific I'll leave it up to you.
import {
TaskReducer,
ADD_TASK,
GET_TASKS,
REMOVE_TASK,
addTask as _addTask,
getTasks as _getTasks,
removeTask as _removeTask,
} from "./reducers";
const GlobalState = (props) => {
const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
const addTask = async (task) => {
const added = await _addTask();
if (added instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: ADD_TASK, data: added });
};
const removeTask = async (taskId) => {
const removed = await _removeTask(taskId);
if (removed instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: REMOVE_TASK, data: taskId });
};
const getTasks = async () => {
const tracks = await _getTracks();
if (tracks instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: GET_TASKS, data: tracks });
};
...
}
Hopefully now you can see how the workflow is supposed to progress. First we call for data from our backend or other API, then we handle the response within the application (for instance, dispatching other actions to notify about errors or side effects of the new data) and then finally dispatch the new data into our state.
As stated at the beginning, what I've provided is essentially pseudocode, so don't expect it to work out of the box.
I am building a mobile app and I'm using Django REST Framework as a backend. And I am also using Redux. One of the API that I am using is to validate an OTP code. If the OTP code is matched, then I am retuning with the response from the server if this is a new user or not. If it's a new user then I'll redirect it to a registration screen, if not then I'll redirect it to the login screen, this is my problem.
I am storing the response of the server in variable named isNewUser in redux store. Then, I am accessing it Inside my component with useSelector. When I click on the button after I entered then OTP code, I dispatch two actions. First the one to validate the OTP. The second is either will be a dispatch for Login or dispatch for registration action, this is depends on the variable isNewUser which I am getting from redux store.
The problem is that, when I dispatch the first action, which is validation of the OTP and storing of the isNewUser variable, the value of this variable is not updated in my component until the next render, so I can't dispatch the second action until I click the button again so that the value of the variable is updated.
So how to fix that? I don't know if my implementation is correct or not or there is a better one.
Here is my code for the action, I didn't write the code for login and register actions yet
export const validateOTP = (otp, mobileNum) => {
return async dispatch => {
const response = await fetch("http://localhost:8000/api/validate_otp", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
otp: otp,
mobile: mobileNum
})
});
if (!response.ok) {
const errorResData = await response.json();
console.log(errorResData);
}
const resData = await response.json();
if (resData.status === false) {
throw new Error(resData.detail);
} else {
const isNewUser = resData.isNewUser;
dispatch({
type: VALIDATE_OTP,
isNewUser: isNewUser
});
}
};
};
Here is my code for the reducer:
import { VALIDATE_OTP } from "../actions/auth";
const initialState = {
isNewUser: null
};
export default (state = initialState, action) => {
switch (action.type) {
case VALIDATE_OTP: {
const isNewUserVal = action.isNewUser;
return {
...state,
isNewUser: isNewUserVal
};
}
}
return state;
};
Here is a sample code from the React Native component:
const CodeEntryScreen = props => {
const dispatch = useDispatch();
const isNewUser = useSelector(state => state.auth.isNewUser)
const [error, setError] = useState();
useEffect(() => {
if (error) {
Alert.alert("An Error Occurred", error, [{ text: "Okay" }]);
}
}, [error]);
const validateOTPHandler = async () => {
setError(null);
try {
await dispatch(authActions.validateOTP(otp, mobileNum));
console.log(isNewUser)
if(isNewUser) {
// dispatch resgister action
}
else {
// dispatch login action
}
} catch (err) {
setError(err.message);
}
};
You can fix this issue with little modifications. The easier one is this:
1) Use dispatch return value in your validateOTPHandler
In your validateOTP function, you have this at the end:
dispatch({
type: VALIDATE_OTP,
isNewUser: isNewUser
});
Make your function to return that instead:
return dispatch({
type: VALIDATE_OTP,
isNewUser: isNewUser
});
With that change, in your component, you can access to the payload of your action this way:
const validateOTPHandler = async () => {
setError(null);
try {
const { isNewUser: isNew } = await dispatch(authActions.validateOTP(otp, mobileNum));
console.log(isNew)
if(isNew) {
// dispatch resgister action
}
else {
// dispatch login action
}
} catch (err) {
setError(err.message);
}
};
That is the easier change to make it work as you want.
2) useEffect
I think this is more similar to the flow you had in mind:
Valide the code (update the store)
Re-render: you got the new value
Now do something: login or register
But do that, you need to use useEffect in order to listen the changes you made this way:
const CodeEntryScreen = props => {
const dispatch = useDispatch();
const isNewUser = useSelector(state => state.auth.isNewUser)
const [error, setError] = useState();
const [success, setSuccess] = useState(false); // true when validateOTP succeeds
useEffect(() => {
if (error) {
Alert.alert("An Error Occurred", error, [{ text: "Okay" }]);
}
}, [error]);
useEffect(() => {
if (success) {
// validateOTP succeed... let's check isNewUser :)
if (isNewUser) {
// dispatch register
} else {
// dispatch login
}
}
}, [success, isNewUser]);
const validateOTPHandler = async () => {
setError(null);
setSuccess(false);
try {
await dispatch(authActions.validateOTP(otp, mobileNum));
setSuccess(true);
} catch (err) {
setError(err.message);
}
};
How to preventing unnecessary requests when update the input?
I tried below solution.But in the App file, that search is declared but never used. I tried something like: https://alligator.io/react/live-search-with-axios/.
What is the variable let token in the fileutils.js. Should I assign let token = localStorage.getItem ('token') to this variable;?
App
import search from /.utils
class App extends Component {
constructor (props) {
super(props);
this.state = {
todos: [],
}
}
search = (query) => {
axios({
url: `/api/v1/todos/{query}`,
method: "GET"
})
.then(res => {
this.setState({
todos: res.data
});
})
.catch(error => {
console.log(error);
})
render () {
return (
<input onChange={this.search} />
)
}
}
utils.js
import axios from 'axios';
const makeRequestCreator = () => {
let token;
return (query) => {
// Check if we made a request
if(token){
// Cancel the previous request before making a new request
token.cancel()
}
// Create a new CancelToken
token = axios.CancelToken.source()
try{
const res = axios(query, {cancelToken: cancel.token})
const result = data.data
return result;
} catch(error) {
if(axios.isCancel(error)) {
// Handle if request was cancelled
console.log('Request canceled', error.message);
} else {
// Handle usual errors
console.log('Something went wrong: ', error.message)
}
}
}
}
const search = makeRequestCreator()
export default search;
You can do that with a function that delays executing of your onChange.you can use debounce function from lodash.js
// _.debounce(yourSearch function, delay time);
search(e){
let str = e.target.value;
_.debounce(() => yourFunction, 500);
}