Problem with react state update on an unmounted component - reactjs

well, when I want to update an item I call UseEffect and make an asynchronous call to my endpoint, but I want to solve the problem when the id doesn't exist in the db, it throws me the following error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
export const AddOrUpdateItem = () => {
const {id} = useParams();
const [itemToUpdate, setItemToUpdate] = useState(null);
const history = useHistory();
useEffect(() => {
if(id) {
const fetchData = async() => {
try {
const resp = await axios.get(`${ITEMS_ENDPOINT}/${id}`);
setItemToUpdate(resp.data);
} catch (error) {
console.log(error);
history.push('/articulos'); //I think here is the problem
}
};
fetchData();
}
}, [id]);
return (
<Box mt={5}>
<Paper elevation={7}>
<Card className="card-root" variant="outlined">
<CardContent>
<h2>{id !== undefined ? 'Actualizar artículo' : 'Registrar artículo'}</h2>
<hr/>
<ItemForm
id={id}
item={itemToUpdate}
/>
</CardContent>
</Card>
</Paper>
</Box>
)
}

The minimal fix might be as follows:
useEffect(() => {
const source = axios.CancelToken.source()
if(id) {
const fetchData = async() => {
try {
const resp = await axios.get(`${ITEMS_ENDPOINT}/${id}`, {cancelToken: source.token});
setItemToUpdate(resp.data);
} catch (error) {
console.log(error);
history.push('/articulos');
}
};
fetchData();
}
return ()=> source.cancel() // <<<<<<<<<<<<<<
}, [id]);
Using a custom hook (Live demo):
import React from "react";
import { useState } from "react";
import {
useAsyncEffect,
CanceledError,
E_REASON_UNMOUNTED
} from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
setText("fetching...");
try {
const json = (yield cpAxios(props.url).timeout(props.timeout)).data;
setText(`Success: ${JSON.stringify(json)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
}
},
[props.url]
);
return (
<div className="component">
<div>{text}</div>
<button className="btn btn-warning" onClick={cancel}>
Cancel request
</button>
</div>
);
}
Demo with internal state usage (Live demo):
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent(props) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(props.url)).data;
},
{ states: true, deps: [props.url] }
);
return (
<div className="component">
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
</div>
<button className="btn btn-warning" onClick={cancel} disabled={done}>
Cancel async effect
</button>
</div>
);
}

Related

Redux updating state with null before actual data

I am dispatching an action that is supposed to take an input from the user and store it in a database. However, when I inspect my posts state in redux after the action is dispatched, there is a null value appended to the state array before the actual post. This is preventing me from working with the actual data in the posts array. Basically I'm wondering how to prevent null from being appended each time I dispatch a new post. Here are the relevant code snippets and images.
Post Reducer:
import { enableAllPlugins, produce } from 'immer';
enableAllPlugins();
const initialState = {
posts: [],
loading: false,
error: false,
uploading: false,
};
const postReducer = produce((draftstate, action = {}) => {
switch (action.type) {
case 'UPLOAD_START':
draftstate.loading = true;
draftstate.error = false;
case 'UPLOAD_SUCCESS':
draftstate.posts.push(action.data);
draftstate.uploading = false;
draftstate.error = false;
case 'UPLOAD_FAIL':
draftstate.uploading = false;
draftstate.error = true;
default:
return draftstate;
}
}, initialState);
export default postReducer;
Upload Post action:
export const uploadPost = (data) => async (dispatch) => {
dispatch({ type: 'UPLOAD_START' });
try {
const newPost = await UploadApi.uploadPost(data);
console.log('new post before', newPost);
dispatch({ type: 'UPLOAD_SUCCESS', data: newPost.data });
} catch (error) {
console.log(error);
dispatch({ type: 'UPLOAD_FAIL' });
}
};
Share Post code:
import React, { useState, useRef } from "react";
import ProfileImage from "../../img/profileImg.jpg";
import "./PostShare.css";
import { UilScenery } from "#iconscout/react-unicons";
import { UilPlayCircle } from "#iconscout/react-unicons";
import { UilLocationPoint } from "#iconscout/react-unicons";
import { UilSchedule } from "#iconscout/react-unicons";
import { UilTimes } from "#iconscout/react-unicons";
import { useSelector, useDispatch } from "react-redux";
import { uploadImage, uploadPost } from "../../actions/uploadAction";
const PostShare = () => {
const loading = useSelector((state) => state.postReducer.uploading);
const [image, setImage] = useState(null);
const imageRef = useRef();
const desc = useRef();
const dispatch = useDispatch();
const { user } = useSelector((state) => state.authReducer.authData);
// handle Image Change
const onImageChange = (event) => {
if (event.target.files && event.target.files[0]) {
let img = event.target.files[0];
setImage(img);
}
};
const reset = () => {
setImage(null);
desc.current.value = "";
};
const handleSubmit = async (e) => {
e.preventDefault();
const newPost = {
userId: user._id,
desc: desc.current.value,
};
if (image) {
const data = new FormData();
const filename = Date.now() + image.name;
data.append("name", filename);
data.append("file", image);
newPost.image = filename;
console.log(newPost);
try {
dispatch(uploadImage(data));
} catch (error) {
console.log(error);
}
}
dispatch(uploadPost(newPost));
reset();
};
return (
<div>
<div className="PostShare">
<img src={ProfileImage} alt="" />
<div>
<input
ref={desc}
required
type="text"
placeholder="What's happening"
/>
<div className="postOptions">
<div
className="option"
style={{ color: "var(--photo)" }}
onClick={() => imageRef.current.click()}
>
<UilScenery />
Photo
</div>
<div className="option" style={{ color: "var(--video" }}>
<UilPlayCircle />
Video
</div>
<div className="option" style={{ color: "var(--location)" }}>
<UilLocationPoint />
Location
</div>
<div className="option" style={{ color: "var(--shedule)" }}>
<UilSchedule />
Schedule
</div>
<button
className="button ps-button"
onClick={handleSubmit}
disabled={loading}
>
{loading ? "Uploading..." : "Share"}
</button>
<div style={{ display: "none" }}>
<input
type="file"
name="myImage"
ref={imageRef}
onChange={onImageChange}
/>
</div>
</div>
{image && (
<div className="previewImage">
<UilTimes onClick={() => setImage(null)} />
<img src={URL.createObjectURL(image)} alt="" />
</div>
)}
</div>
</div>
</div>
);
};
export default PostShare;
I would be glad to provide any other details if that helps.
Update with other portions of code:
Dispatcher of RETRIEVING_SUCCESS:
import * as PostApi from '../api/PostRequest';
export const getTimelinePosts = (id) => async (dispatch) => {
dispatch({ type: 'RETRIEVING_START' });
try {
const { data } = await PostApi.getTimelinePosts(id);
dispatch({ type: 'RETRIEVING_SUCCESS', data: data });
} catch (error) {
dispatch({ type: 'RETRIEVING_FAIL' });
console.log(error);
}
};
getTimelinePosts usage:
import React, { useEffect } from 'react';
import './Posts.css';
import { PostsData } from '../../Data/PostsData';
import { useDispatch, useSelector } from 'react-redux';
import { getTimelinePosts } from '../../actions/postAction';
import Post from '../Post/Post';
const Posts = () => {
const dispatch = useDispatch();
const { user } = useSelector((state) => state.authReducer.authData);
let { posts, loading } = useSelector((state) => state.postReducer);
console.log('posts content', posts);
useEffect(() => {
dispatch(getTimelinePosts(user._id));
}, []);
return (
<div className="Posts">
{/* {posts.map((post, id) => {
return <Post data={post} id={id}></Post>;
})} */}
</div>
);
};
export default Posts;
in postReducer, let's remove the default on the switch statement, we don't need it on reducer because other actions will come here and the code make all states return the initial state.

UseEffect can't performe state update

I am getting this UseEffct error that causes my window to render an empty page
error ="Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
my Component Code
import React, { useEffect, useState } from "react";
import { instance } from "../Data/axios";
const Row = ({ title, fetchURL }) => {
const [movies, setMovies] = useState([]);
// A snippet of code that runs on a certain condition
useEffect(() => {
async function fetchData() {
const data = await instance.get(fetchURL);
setMovies(data.data.results);
}
fetchData();
}, [fetchURL]);
const base_url = "https://image.tmdb.org/t/p/original/";
console.log(movies);
return (
<div className="rows">
<h3>{title}</h3>
<div className="rows_poster">
{movies.map((movie) => {
<div key={movie.id}>
<img src={`${base_url}${movie.poster_path}`} />
</div>;
})}
</div>
</div>
);
};
export default Row;
This has worked for me:
useEffect(() => {
let mounted = true;
async function fetchData(){
const data = await instance.get(fetchURL);
if (mounted) {
setMovies(data.data.results);
}
};
fetchData();
return ()=> {mounted = false}
}, []);

Avoiding React Race condition with AbortController not working

i am trying to mimic React useEffect race condition and handle that with AbortController. I can never hit the catch block ( i guess ) because the setTimeOut is called post the fetch request. My question is how can i rewrite this code to put fetch inside setTimeout and still be able to use AbortController to cancel the request?
import './App.css';
import {useState,useEffect} from 'react'
function App() {
const [country, setCountry] = useState('N.A.')
const [capital, setCapital] = useState('')
useEffect(() => {
const ctrl = new AbortController();
const load = async() =>{
try
{
//debugger
const response = await fetch(`https://restcountries.eu/rest/v2/capital/${capital}`,
{signal:ctrl.signal})
const jsonObj = await response.json()
setTimeout( ()=> {setCountry(jsonObj[0].name)} , Math.random()*10000)
}
catch(err)
{
console.log(err)
}
}
load();
return () =>{
ctrl.abort()
};
}, [capital])
return (
<div>
<button onClick={()=>setCapital("Berlin")} >Berlin</button>
<button onClick={()=>setCapital("Paris")} >Paris</button>
<button onClick={()=>setCapital("Madrid")} >Madrid</button>
<div>
{country}
</div>
</div>
);
}
export default App;
Hmm... just put that function inside setTimeout calling and don't forget to clean up the timer on unmount (Demo).
import React, { useState, useEffect } from "react";
export default function TestComponent(props) {
const [country, setCountry] = useState("N.A.");
const [capital, setCapital] = useState("");
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const ctrl = new AbortController();
const timer = setTimeout(async () => {
try {
if (!capital) {
return;
}
const response = await fetch(
`https://restcountries.eu/rest/v2/capital/${capital}`,
{ signal: ctrl.signal }
);
// if (!isMounted) return; // can be omitted here
const jsonObj = await response.json();
isMounted && setCountry(jsonObj[0].name);
} catch (err) {
console.log(err);
isMounted && setError(err);
}
}, Math.random() * 10000);
return () => {
clearTimeout(timer);
isMounted = false;
ctrl.abort();
};
}, [capital]);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<button onClick={() => setCapital("Berlin")}>Berlin</button>
<button onClick={() => setCapital("Paris")}>Paris</button>
<button onClick={() => setCapital("Madrid")}>Madrid</button>
<div>Country: {error ? <b>{error.toString()}</b> : country}</div>
</div>
);
}
Or you can do the same with custom libs (Demo):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CPromise, CanceledError } from "c-promise2";
import cpFetch from "cp-fetch";
export default function TestComponent(props) {
const [country, setCountry] = useState("N.A.");
const [capital, setCapital] = useState("");
const [error, setError] = useState(null);
const cancel = useAsyncEffect(
function* () {
setError(null);
if (!capital) {
return;
}
yield CPromise.delay(Math.random() * 10000);
try {
const response = yield cpFetch(
`https://restcountries.eu/rest/v2/capital/${capital}`
).timeout(props.timeout);
const jsonObj = yield response.json();
setCountry(jsonObj[0].name);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
console.log(err);
setError(err);
}
},
[capital]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<button onClick={() => setCapital("Berlin")}>Berlin</button>
<button onClick={() => setCapital("Paris")}>Paris</button>
<button onClick={() => setCapital("Madrid")}>Madrid</button>
<button onClick={cancel}>Cancel request</button>
<div>Country: {error ? <b>{error.toString()}</b> : country}</div>
</div>
);
}

onclick run function react not working reactjs

I have the following problem, I have a function but after a few seconds or minutes it runs on its own without clicking.
If someone can help me that I am failing, when I click the function works fine but after a few seconds or minutes it executes itself
Thank you
there I leave the code
import React, { useContext, Fragment, useState,useEffect} from 'react';
import clienteAxios from '../../config/axios';
import { withRouter } from 'react-router-dom';
import { CRMContext } from '../../context/CRMContext';
const ListadoPedidos =(props) => {
const [ auth, setAuth ] = useContext(CRMContext);
const [pedidos, setPedidos] = useState([]);
const detallePedido = id => {
props.history.push(`/detalle-pedido/${id}`);
}
const eliminarPedido = () => {
}
useEffect( () => {
if(auth.token !== '') {
//query a la API
const consultarAPI = async () => {
try {
const pedidosConsulta = await clienteAxios.get(`/pedidoscliente/${auth.clienteId}`);
setPedidos(pedidosConsulta.data);
} catch (error) {
if(error.response.status === 500) {
props.history.push('/iniciar-sesion');
}
}
}
consultarAPI();
} else {
props.history.push('/iniciar-sesion');
}
}, [] );
return(
<Fragment>
<div class="row justify-content-end">
<button type="button"
class="btn btn-dark mr-2"
onClick={() => detallePedido(pedido.id)}
>
Ver detalle</button>
</div>
</Fragment>
)
}
export default withRouter(ListadoPedidos);

useMutation update,the cache changed,but UI not change

I useMutation to send message ,but the message list in chat window not change. I found that the cache has changed . Please help , I can't understand.
The useQuery not work . UI have no change :(
But~! When I put them in one js file. it works.... why???
The version I used is #apollo/react-hooks 3.1.1
Parent window.js
import React from 'react';
import { useQuery } from "#apollo/react-hooks";
import { GET_CHAT } from "#/apollo/graphql";
import ChatInput from "#/pages/chat/components/input";
const ChatWindow = (props) => {
const { chatId, closeChat } = props;
const { data, loading, error } = useQuery(GET_CHAT, { variables: { chatId: chatId } });
if (loading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
const { chat } = data;
return (
<div className="chatWindow" key={'chatWindow' + chatId}>
<div className="header">
<span>{chat.users[1].username}</span>
<button className="close" onClick={() => closeChat(chatId)}>X</button>
</div>
<div className="messages">
{chat.messages.map((message, j) =>
<div key={'message' + message.id} className={'message ' + (message.user.id > 1 ? 'left' : 'right')}>
{message.text}
</div>
)}
</div>
<div className="input">
<ChatInput chatId={chatId}/>
</div>
</div>
);
};
export default ChatWindow;
Child input.js
import React, { useState } from 'react';
import { useApolloClient, useMutation } from "#apollo/react-hooks";
import { ADD_MESSAGE, GET_CHAT } from "#/apollo/graphql";
const ChatInput = (props) => {
const [textInput, setTextInput] = useState('');
const client = useApolloClient();
const { chatId } = props;
const [addMessage] = useMutation(ADD_MESSAGE, {
update(cache, { data: { addMessage } }) {
const { chat } = client.readQuery({
query: GET_CHAT,
variables: {
chatId: chatId
}
});
chat.messages.push(addMessage);
client.writeQuery({
query: GET_CHAT,
variables: {
chatId: chatId
},
data: {
chat
}
});
}
});
const onChangeInput = (event) => {
event.preventDefault();
setTextInput(event.target.value);
};
const handleKeyPress = (event, chatId, addMessage) => {
if (event.key === 'Enter' && textInput.length) {
addMessage({
variables: {
message: {
text: textInput,
chatId: chatId
}
}
});
setTextInput('');
}
};
return (
<input type="text"
value={textInput}
onChange={(event) => onChangeInput(event)}
onKeyPress={(event) => handleKeyPress(event, chatId, addMessage)}
/>
);
};
export default ChatInput;
You probably solved the issue by now, but for the record:
Your code mutates the chat state:
chat.messages.push(addMessage);
State should not be mutated (see the React setState Docs for more details).
Contruct a new array instead:
const newChat = [...chat, addMessage]

Resources