I am trying to update the database. So I have an input field that is disabled as default. So when you click, editing is enabled and when you click outside of the input field, it gets disabled again. What I am trying to do is update when you click outside of the input field. So, my input is like this:
const InputPrice = ({ mainPricePosts, handleChange }) => {
const [disabled, setDisabled] = useState(true);
const [priceValue, setPriceValue] = useState(mainPricePosts);
function handleClick() {
if (disabled === true) {
setDisabled(false);
}
}
return (
<>
<Form.Control
type="text"
className="price_coefficient_input"
value={priceValue}
onBlur={() => {
setDisabled(true);
handleChange(priceValue);
}}
onChange={handleChange(mainPricePosts)}
readOnly={disabled}
onClick={handleClick}
/>
</>
);
};
InputPrice.propTypes = {
mainPricePosts: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired,
};
export default InputPrice;
And this is how I am trying to update but I am not sure if I am doing right to get the value from the input field:
const [updatePosts, setUpdatePosts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [show, setShow] = useState(false);
const [showError, setShowError] = useState(false);
const handleClose = () => setShow(false);
const handleCloseError = () => setShowError(false);
const fetchIndividualPosts = async ({ value, post: { mainPricePosts, key } = {} }) => {
console.log(value);
try {
setLoading(true);
const res = await Axios({
method: "POST",
url: `url`,
headers: {
"content-Type": "application/json",
},
data: {
updated_parameter: ["main_price", "small_car", key],
updated_value: value,
},
});
if (res.status === 200) {
setUpdatePosts(res.data);
}
setLoading(false);
} catch (err) {
console.log(err.response.status);
setError(err.response.data.error);
setLoading(false);
}
};
const handleChange = (mainPricePosts) => (e) => {
fetchIndividualPosts({ mainPricePosts, value: e.target.value });
};
This is also the curl how I can update the data:
curl -L -i -H "Content-Type: application/json" -X POST -d '{
"updated_parameter":["100"],
"updated_value":"0.044"
}' $ip''
so updated_value should be the updated input (the value after, outside is clicked)
100, should be the key of the input value.
Hope it is clear and you can help me about this problem.
Thanks for your help beforehand.
There are many ways you can achieve what you need, but I would use following approach.
In your InputPrice component on onBlur event I would disable input by calling setDisabled(true) and then use useEffect hook to call handleChange callback if new price value and original price values are different. Because you are calling setDisabled(true), you're actually re-rendering your InputPrice component and therefore not executing handleChange callback.
Checkout code below.
const InputPrice = ({ mainPricePosts, handleChange }) => {
const [disabled, setDisabled] = useState(true);
const [priceValue, setPriceValue] = useState(mainPricePosts);
function handleClick() {
if (disabled === true) {
setDisabled(false);
}
}
useEffect(() => {
let callUpdateCallback = false;
if (priceValue !== mainPricePosts) callUpdateCallback = true;
if (disabled && callUpdateCallback) handleChange(priceValue);
}, [disabled, priceValue, handleChange, mainPricePosts]);
return (
<>
<Form.Control
type="text"
className="price_coefficient_input"
value={priceValue}
onBlur={setDisabled(true)}
onChange={(e) => setPriceValue(e.target.value)}
readOnly={disabled}
onClick={handleClick}
/>
</>
);
};
InputPrice.propTypes = {
mainPricePosts: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired,
};
export default InputPrice;
You call this component like this
import React from "react";
import ReactDOM from "react-dom";
import InputPrice from "./InputPrice";
function App() {
const handleChange = (e) => {
console.log("app handle change", e);
// You can call your fetch here...
};
return (
<div>
<InputPrice mainPricePosts="500" handleChange={handleChange} />
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
Additionally there codesandbox that used to debug it, so if you need more details you can find it on the link below.
https://codesandbox.io/s/reactjs-playground-forked-8vwe2?file=/src/index.js:0-364
Related
I'm new to React, and I'm trying to make a recpie app with react, right know I want to save the data in json file from the add form. so I can save the data but when I want to redirect the user to the home page using useEffict with navigate. I can't go to the create page when adding navigate to the useEffict.
Create file code:
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useFetch } from "../../hooks/useFetch";
// Styles
import "./Create.css";
export default function Create() {
const [title, setTitle] = useState("");
const [method, setMethod] = useState("");
const [cookingTime, setCookingTime] = useState("");
const [newIngredient, setNewIngredient] = useState("");
const [ingredients, setIngredients] = useState([]);
const { postData, data } = useFetch("http://localhost:3000/recipes", "POST");
const ingredientsInput = useRef(null);
const navigate = useNavigate();
// Methods
const handleSubmit = (e) => {
e.preventDefault();
postData({
title,
ingredients,
method,
cookingTime: cookingTime + " minutes",
});
};
const handleAdd = (e) => {
e.preventDefault();
const ing = newIngredient.trim();
if (ing && !ingredients.includes(ing)) {
setIngredients((preIng) => [...preIng, ing]);
}
setNewIngredient("");
ingredientsInput.current.focus();
};
useEffect(() => {
if (data) {
navigate("/");
console.log(data);
}
}, [data, navigate]);
return (
<div className="create">
<form onSubmit={handleSubmit}>
<label>
<span>Recipe Title:</span>
<input
type="text"
onChange={(e) => setTitle(e.target.value)}
value={title}
required
/>
</label>
<label>
<span>Recipe ingredients:</span>
<div className="ingredients">
<input
type="text"
onChange={(e) => setNewIngredient(e.target.value)}
value={newIngredient}
ref={ingredientsInput}
/>
<button onClick={handleAdd} className="btn">
Add
</button>
</div>
</label>
{ingredients.length > -1 && (
<p>
Current ingredients:{" "}
{ingredients.map((ing) => (
<span key={ing}>{ing}, </span>
))}
</p>
)}
<label>
<span>Recipe Method:</span>
<textarea
onChange={(e) => setMethod(e.target.value)}
value={method}
required
/>
</label>
<label>
<span>Recipe Time (minutes):</span>
<input
type="number"
onChange={(e) => setCookingTime(e.target.value)}
value={cookingTime}
required
/>
</label>
<button className="btn">Submit</button>
</form>
</div>
);
}
useFetch file code:
import { useState, useEffect } from "react";
export const useFetch = (url, method = "GET") => {
const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
const [option, setOption] = useState(null);
const postData = (data) => {
setOption({
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
};
useEffect(() => {
const controller = new AbortController();
const fetchData = async (fetchOption) => {
setIsPending(true);
try {
const res = await fetch(url, {
...fetchOption,
signal: controller.signal,
});
if (!res.ok) {
throw new Error(res.statusText);
}
const data = await res.json();
setIsPending(false);
setData(data);
setError(null);
} catch (err) {
if (err.name === "AbortError") {
console.log("the fetch was aborted");
} else {
setIsPending(false);
setError("Could not fetch the data");
}
}
};
if (method === "GET") {
fetchData();
}
if (method === "POST") {
fetchData(option);
}
return () => {
controller.abort();
};
}, [url, option, method]);
return { data, isPending, error, postData };
};
I don't know from where the issue came.
The problem was from useFetch file. when I want to do a post request I shoud cheack if the option useState has a value.
Before I was just check if there is a post method:
const [option, setOptions] = useState(null);
if (method === "POST") {
fetchData(option);
}
Know I'm checking if there is a value in option
const [option, setOptions] = useState(null);
if (method === "POST" && option) {
fetchData(option);
}
You basically trying to add a variable that is not a react state variable into the useEffect on update
const [recipes, setReceipies] = useState();
useEffect(async ()=> { const {data} = awawit useFetch("http://localhost:3000/recipes", "POST")
setReceipies(data);
},[])
navigate("/");
},[recipes]);
Or ofc you can navigate all the way from the mounting useEffect
Good Luck
after you save the data, simply add this code
const history = createBrowserHistory()
history.push(`/`)
I have big apps, that use history, and I never had a problem with it.
and I recomend you to use SWR for data-fetching - React Hooks for Data Fetching.
very simple and powerfull tool:
https://swr.vercel.app/
I have a material-ui LinearDeterminate progress bar, and I would like to pass on how far along the upload is.
const LinearDeterminate = ({ uploadPercentage, setuploadPercentage }) => {
const classes = useStyles();
const [uploadPercentage, setuploadPercentage] = useState("");
console.log(uploadPercentage);
return (
<div className={classes.root}>
<LinearProgress variant="determinate" value={uploadPercentage} />
</div>
);
};
...
<UploadInput
path={`customer_creatives/assets/${customer_id}/${new Date().getTime()}`}
onChange={(value) =>
updateFieldHandler("link_to_assets")({ target: { value } })
}
value={submissionData["link_to_assets"] || ""}
label="Link to Assets"
sublabel="*Zip files before uploading"
isImage={false}
/>
<LinearDeterminate />
...
UploadInput is a custom input component that links to DropZone (Where the upload happens)
import React, { useState } from "react";
import ReactDropzone from "react-dropzone";
import axios from "axios";
import { noop } from "lodash";
import HelpDialog from "components/HelpDialog";
import { API_URL } from "config";
const Dropzone = ({
path,
onChange = noop,
children,
multiple = false,
maxSize,
sizeHelper,
...props
}) => {
const [url, setUrl] = useState("");
const [loading, setLoading] = useState("");
const [uploadPercentage, setuploadPercentage] = useState("");
const [sizeHelperOpen, setSizeHelperOpen] = useState(false);
const onDrop = ([file]) => {
const contentType = file.type; // eg. image/jpeg or image/svg+xml
console.log(file);
if (maxSize && maxSize < file.size) {
setSizeHelperOpen(true);
return;
}
const generatePutUrl = `${API_URL}/generate-put-url`;
const generateGetUrl = `${API_URL}/generate-get-url`;
const options = {
onUploadProgress: (progressEvent) => {
//console.log("progressEvent.loaded " + progressEvent.loaded)
//console.log("progressEvent.total " + progressEvent.total)
let percent = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
);
setuploadPercentage({
uploadPercentage: percent,
});
console.log(uploadPercentage);
},
params: {
Key: path,
ContentType: contentType,
},
headers: {
"Content-Type": contentType,
},
};
setUrl(URL.createObjectURL(file));
setLoading(true);
axios.get(generatePutUrl, options).then((res) => {
const {
data: { putURL },
} = res;
axios
.put(putURL, file, options)
.then(() => {
axios.get(generateGetUrl, options).then((res) => {
const { data: getURL } = res;
onChange(getURL);
setLoading(false);
});
})
.catch(() => {
setLoading(false);
});
});
};
return (
<ReactDropzone onDrop={onDrop} multiple={multiple} {...props}>
{({ getRootProps, getInputProps }) => (
<>
<div {...getRootProps()}>
<input {...getInputProps()} />
{children({ url, loading })}
</div>
<HelpDialog
open={sizeHelperOpen}
onClose={() => setSizeHelperOpen(false)}
>
{sizeHelper}
</HelpDialog>
</>
)}
</ReactDropzone>
);
};
export default Dropzone;
I'm trying to get the results from the onUploadProgress function into my progress bar. Can I use a custom hook for that? My problem with that is Dropzone already has an export. Thanks for any advice!
It looks as simple as lifting the state up. You actually already have the { uploadPercentage, setuploadPercentage } props on the LinearDeterminate component. Just put that state in the common parent of the UploadInput and the LinearDeterminate components, and then keep passing down the handler to the DropZone component
Remove the state from the LinearDeterminate component
const LinearDeterminate = ({ uploadPercentage }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<LinearProgress variant="determinate" value={uploadPercentage} />
</div>
);
};
Move it to the common parent
const [uploadPercentage, setuploadPercentage] = useState("");
...
<UploadInput
path={`customer_creatives/assets/${customer_id}/${new Date().getTime()}`}
onChange={(value) =>
updateFieldHandler("link_to_assets")({ target: { value } })
}
value={submissionData["link_to_assets"] || ""}
label="Link to Assets"
sublabel="*Zip files before uploading"
isImage={false}
setuploadPercentage={setuploadPercentage}
/>
<LinearDeterminate uploadPercentage={uploadPercentage}/>
...
UploadInput component
const UploadInput = ({ setuploadPercentage, ...allOtherCrazyProps }) => {
...
return (
<DropZone setuploadPercentage={setuploadPercentage} {...moreCrazyProps} />
);
};
And finally, in the DropZone
const Dropzone = ({
path,
onChange = noop,
children,
multiple = false,
maxSize,
sizeHelper,
setuploadPercentage, // this is the new prop
...props
}) => {
...
const options = {
onUploadProgress: (progressEvent) => {
...
setuploadPercentage(percent); // is a string?
console.log(uploadPercentage);
},
...
};
...
};
If you find it cumbersome passing the handler all the way down, you could use a Context to manage that state, but anywhere you use the UserInput you'll need to wrap it on the context provider.
I'd also say to move all the uploading logic and build something similar to downshift-js: a hook that returns all necessary props to turn an element into a droppable uploader, but you already depend on the ReactDropzone component, so I don't think can be done unless you try this other pattern https://blog.bitsrc.io/new-react-design-pattern-return-component-from-hooks-79215c3eac00
You can set the upload precent value after each axios successful call.
axios.get(generatePutUrl, options).then(res => {
const {
data: { putURL }
} = res;
/* Set precent to 33% here */
axios
.put(putURL, file, options)
.then(() => {
/* Set precent to 66% */
axios.get(generateGetUrl, options).then(res => {
/* Set precent to 99% here */
const { data: getURL } = res;
onChange(getURL);
setLoading(false);
});
})
.catch(() => {
setLoading(false);
});
});
Hope that helps.
I'm currently working on a project to implement a website to check the weather forecast.
I'm trying to get the value from the input field and when I click the submit button, this value should be set to cityName. What do I have to change in order to make this work?
import { useState, useEffect } from "react"
export function WeatherInfo() {
const token: string = '7ebe7c2a03cd48c090a193437'
async function getCurrentWeather(cityName: string): Promise<any> {
const response = await fetch(`http://api.weatherapi.com/v1/current.json?key=${token}&q=${cityName}`)
const data = await response.json()
console.log(data)
return data
}
const [cityName, setCityName]: any = useState('')
const [cityWeather, setCityWeather] = useState({})
const [value, setValue] = useState('')
const handleChange = (event: any) => {
setValue(event.target.value)
}
const handleSubmit = (event: any) => {
event.preventDefault()
setCityName(value)
}
useEffect(() => {
async function fetchData() {
const cityWeather = await getCurrentWeather(cityName)
}
fetchData()
})
return (
<div >
<form onSubmit={handleSubmit}>
<input onChange={handleChange} placeholder="Type here" />
<button>Search</button>
</form>
</div>
);
}
You should add a dependency array to your effect hook so that it triggers whenever cityName changes.
Updating the cityWeather state should only be done via the setCityWeather function.
useEffect(() => {
if (cityName) { // only fetch when you've got a value
getCurrentWeather(cityName).then(setCityWeather);
}
}, [cityName]);
You should also try to use as few any types as possible, preferably none
// define stand-alone functions outside your components
// eg weather-api.ts
const token = "your-api-key";
export interface CurrentWeather {
temp_c: number;
feelslike_c: number;
// etc
}
export async function getCurrentWeather(
cityName: string
): Promise<CurrentWeather> {
// safely encode URL query params
const params = new URLSearchParams({
key: token,
q: cityName,
});
const response = await fetch(
`http://api.weatherapi.com/v1/current.json?${params}`
);
// don't forget to check for errors
if (!response.ok) {
throw response;
}
return response.json(); // will be cast to the `CurrentWeather` type
}
import { useState, useEffect, FormEventHandler } from "react";
import { getCurrentWeather, CurrentWeather } from "./weather-api";
export function WeatherInfo() {
const [cityName, setCityName] = useState("");
const [cityWeather, setCityWeather] = useState<CurrentWeather>(); // default undefined
const [value, setValue] = useState("");
useEffect(() => {
getCurrentWeather(cityName).then(setCityWeather).catch(console.error);
}, [cityName]);
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setCityName(value);
};
return (
<div>
{cityWeather && (
<p>
The current temperature in {cityName} is {cityWeather.temp_c} °C
</p>
)}
<form onSubmit={handleSubmit}>
<input
onChange={(e) => setValue(e.target.value)}
placeholder="Type here"
/>
<button>Search</button>
</form>
</div>
);
}
I want to implement something like infinity scrolling on my application. This is my code:
import React, { useState, useEffect } from "react";
import AsyncSelect from "react-select/async";
const WithPromises = () => {
const [page, setPage] = useState(1);
const [allData, setAllData] = useState([]); //here should be added all data
const [scrolll, setScrolll] = useState();
const filterData = (inputValue) => {
const req = fetch(
`https://jsonplaceholder.typicode.com/todos?_limit=15&_page=${page}`
)
.then((response) => response.json())
.then((res) => {
const fetchedData = res.map(({ title }) => {
return {
label: title,
value: title
};
});
page > 1 && setAllData([...allData, ...fetchedData]);
return [...fetchedData, allData];
});
return req;
};
const promiseOptions = (inputValue) => {
return filterData(inputValue);
};
const scroll = (e) => {
console.log(e);
setScrolll(e);
promiseOptions();
};
useEffect(() => {
setPage(page + 1);
}, [scrolll, page]);
return (
<AsyncSelect
cacheOptions
onMenuScrollToBottom={scroll}
isClearable={true}
isSearchable={true}
defaultOptions
loadOptions={promiseOptions}
/>
);
};
export default WithPromises;
How you can see, i increment the page when the scroll section is at the bottom: setPage(page + 1);. This value is added in the api https://jsonplaceholder.typicode.com/todos?_limit=15&_page=${page}.
Also i want to concat all data from each scroll here: return [...fetchedData, allData]. At the end i need to achieve something like this:
User scrolls down and in the scroll section are added mew values, but previous should't dissapear, so on every scrolling when the scroll bar is at the bottom the new data should be added at the bottom of the select.
Issue: I can't achieve what i described above and i don't know the issue.
Question: How to solve the issue in my situation?
demo: https://codesandbox.io/s/codesandboxer-example-forked-zsj6i?file=/example.js:0-1262
import React, { useState, useEffect } from "react";
import AsyncSelect from "react-select/async";
const WithPromises = () => {
const [page, setPage] = useState(1);
const [items, onItemsChange] = useState([]);
useEffect(() => {
fetchData(page);
}, [page]);
const fetchData = async (pageIdx) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos?_limit=15&_page=${pageIdx}`
).then((r) => r.json());
const resItems = res.map(({ title }) => ({
label: title,
value: title
}));
onItemsChange([...items, ...resItems]);
};
const loadOptions = () => items;
// for search functionality use something like this:
// const loadOptions = async (inputStr) => {
// const searchRes = await fetch(`${yourUrl}?search=${inputStr}`).then(r => r.json());
// return searchRes;
// };
const handleBottomScroll = () => {
setPage((prevVal) => prevVal + 1);
};
return (
<AsyncSelect
cacheOptions
isClearable={true}
isSearchable={true}
defaultOptions={items}
onMenuScrollToBottom={handleBottomScroll}
loadOptions={loadOptions}
/>
);
};
export default WithPromises;
I have this code
import ReactDOM from "react-dom";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>
<Link to="/">Netflix</Link>
<Route path="/" component={Miliko} />
</div>
</Router>
);
}
const Miliko = ({ match }) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const Res = await fetch("https://foo0022.firebaseio.com/New.json");
const ResObj = await Res.json();
const ResArr = await Object.values(ResObj).flat();
setData(ResArr);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
console.log(data);
}, [match]);
return <div>{`${isLoading}${isError}`}</div>;
};
function App() {
return (
<div className="App">
<ParamsExample />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I created three links that open the Miliko component. but when I quickly click on the links I get this error:
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I think the problem is caused by dismount before async call finished.
const useAsync = () => {
const [data, setData] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
if (!mountedRef.current) return null
setData(res)
return res
})
}, [])
useEffect(() => {
return () => {
mountedRef.current = false
}
}, [])
}
mountedRef is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip them.
This should be the main reason to not end up with a memory leak (access cleanedup memory) issue.
Demo
https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync
https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync
Update
The above answer leads to the following component that we use inside our team.
/**
* A hook to fetch async data.
* #class useAsync
* #borrows useAsyncObject
* #param {object} _ props
* #param {async} _.asyncFunc Promise like async function
* #param {bool} _.immediate=false Invoke the function immediately
* #param {object} _.funcParams Function initial parameters
* #param {object} _.initialData Initial data
* #returns {useAsyncObject} Async object
* #example
* const { execute, loading, data, error } = useAync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* funcParams: { data: '1' },
* initialData: 'Hello'
* })
*/
const useAsync = (props = initialProps) => {
const {
asyncFunc, immediate, funcParams, initialData
} = {
...initialProps,
...props
}
const [loading, setLoading] = useState(immediate)
const [data, setData] = useState(initialData)
const [error, setError] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(params => {
setLoading(true)
return asyncFunc({ ...funcParams, ...params })
.then(res => {
if (!mountedRef.current) return null
setData(res)
setError(null)
setLoading(false)
return res
})
.catch(err => {
if (!mountedRef.current) return null
setError(err)
setLoading(false)
throw err
})
}, [asyncFunc, funcParams])
useEffect(() => {
if (immediate) {
execute(funcParams)
}
return () => {
mountedRef.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
execute,
loading,
data,
error
}
}
Update 2022
This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.
useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.
In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.
export const useUserData = () => {
const initialState = {
user: {},
error: null
}
const [state, setState] = useState(initialState);
useEffect(() => {
// clean up controller
let isSubscribed = true;
// Try to communicate with sever API
fetch(SERVER_URI)
.then(response => response.json())
.then(data => isSubscribed ? setState(prevState => ({
...prevState, user: data
})) : null)
.catch(error => {
if (isSubscribed) {
setState(prevState => ({
...prevState,
error
}));
}
})
// cancel subscription to useEffect
return () => (isSubscribed = false)
}, []);
return state
}
You can read up more from this blog juliangaramendy
Without #windmaomao answer, I could spend other hours trying to figure out how to cancel the subscription.
In short, I used two hooks respectively useCallback to memoize function and useEffect to fetch data.
const fetchSpecificItem = useCallback(async ({ itemId }) => {
try {
... fetch data
/*
Before you setState ensure the component is mounted
otherwise, return null and don't allow to unmounted component.
*/
if (!mountedRef.current) return null;
/*
if the component is mounted feel free to setState
*/
} catch (error) {
... handle errors
}
}, [mountedRef]) // add variable as dependency
I used useEffect to fetch data.
I could not call the function inside effect simply because hooks can not be called inside a function.
useEffect(() => {
fetchSpecificItem(input);
return () => {
mountedRef.current = false; // clean up function
};
}, [input, fetchSpecificItem]); // add function as dependency
Thanks, everyone your contribution helped me to learn more about the usage of hooks.
fetchData is an async function which will return a promise. But you have invoked it without resolving it. If you need to do any cleanup at component unmount, return a function inside the effect that has your cleanup code. Try this :
const Miliko = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState('http://hn.algolia.com/api/v1/search?query=redux');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
return function() {
/**
* Add cleanup code here
*/
};
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
I would suggest reading the official docs where it is clearly explained along with some more configurable parameters.
Folowing #Niyongabo solution, the way I ended up that fixed it was:
const mountedRef = useRef(true);
const fetchSpecificItem = useCallback(async () => {
try {
const ref = await db
.collection('redeems')
.where('rewardItem.id', '==', reward.id)
.get();
const data = ref.docs.map(doc => ({ id: doc.id, ...doc.data() }));
if (!mountedRef.current) return null;
setRedeems(data);
setIsFetching(false);
} catch (error) {
console.log(error);
}
}, [mountedRef]);
useEffect(() => {
fetchSpecificItem();
return () => {
mountedRef.current = false;
};
}, [fetchSpecificItem]);
Create a mutable ref object and set it to true, and during clean-up toggle its value, to ensure that the component has been unmouted.
const mountedRef = useRef(true)
useEffect(() => {
// CALL YOUR API OR ASYNC FUNCTION HERE
return () => { mountedRef.current = false }
}, [])
const [getAllJobs, setgetAlljobs] = useState();
useEffect(() => {
let mounted = true;
axios.get('apiUrl')
.then(function (response) {
const jobData = response.data;
if (mounted) {
setgetAlljobs(jobData)
}
})
.catch(function (error) {
console.log(error.message)
})
return () => mounted = false;
}, [])
set a variable mounted to true->
then if it is true, mount the function->
in the bottom you return it to unmount it
My case was pretty different from what this questions wants. Still I got the same error.
My case was because I had a 'list', which was rendered by using .map from array. And I needed to use .shift. (to remove first item in array)
If array had just one item, it was ok, but since it had 2 of them -> the first one got 'deleted/shifted' and because I used key={index} (while index was from .map), it assumed, that the second item, which later was first, was the same component as the shifted item..
React kept info from the first item (they were all nodes) and so, if that second node used useEffect(), React threw error, that the component is already dismounted, because the former node with index 0 and key 0 had the same key 0 as the second component.
The second component correctly used useEffect, but React assumed, that it is called by that former node, which was no longer on the scene -> resulting in error.
I fixed this by adding different key prop value (not index), but some unique string.
you can wrap any action as a callback inside checkUnmount
const useUnmounted = () => {
const mountedRef = useRef(true);
useEffect(
() => () => {
mountedRef.current = false;
},
[],
);
const checkUnmount = useCallback(
(cb = () => {}) => {
try {
if (!mountedRef.current) throw new Error('Component is unmounted');
cb();
} catch (error) {
console.log({ error });
}
},
[mountedRef.current],
);
return [checkUnmount, mountedRef.current];
};
import React, { useCallback, useEffect, useRef, useState } from "react";
import { userLoginSuccessAction } from "../../../redux/user-redux/actionCreator";
import { IUser } from "../../../models/user";
import { Navigate } from "react-router";
import XTextField from "../../../x-lib/x-components/x-form-controls/XTextField";
import { useDispatch } from "react-redux";
interface Props {
onViewChange?: (n: number) => void;
userInit?: (user: IUser) => void;
}
interface State {
email: string;
password: string;
hasError?: boolean;
errorMessage?: string;
}
const initialValue = {
email: "eve.holt#reqres.in",
password: "cityslicka",
errorMessage: "",
};
const LoginView: React.FC<Props> = (props) => {
const { onViewChange } = props;
const [state, setState] = useState(initialValue);
const mountedRef = useRef(true);
const dispatch = useDispatch();
const handleEmailChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
email: val,
}));
},
[state.email]
);
const handlePasswordChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
password: val,
}));
},
[state.password]
);
const onUserClick = useCallback( async () => {
// HTTP Call
const data = {email: state.email , password: state.password}
try{
await dispatch(userLoginSuccessAction(data));
<Navigate to = '/' />
setState( (state)=>({
...state,
email: "",
password: ""
}))
}
catch(err){
setState( (state)=>({
...state,
errorMessage: err as string
}))
}
},[mountedRef] )
useEffect(()=>{
onUserClick();
return ()=> {
mountedRef.current = false;
};
},[onUserClick]);
const Error = (): JSX.Element => {
return (
<div
className="alert alert-danger"
role="alert"
style={{ width: "516px", margin: "20px auto 0 auto" }}
>
{state.errorMessage}
</div>
);
};
return (
<div>
<div>
email: "eve.holt#reqres.in"
<span style={{ paddingRight: "20px" }}></span> password: "cityslicka"{" "}
</div>
{state.errorMessage && <Error />}
<form className="form-inline">
<div className="form-group">
<XTextField
label="email"
placeholder="E-Posta"
value={state.email}
onChange={handleEmailChange}
/>
</div>
<div className="form-group my-sm-3">
<XTextField
type="password"
label="password"
placeholder="Şifre"
value={state.password}
onChange={handlePasswordChange}
/>
</div>
<button type="button" className="btn btn-primary" onClick = {onUserClick} >
Giriş Et
</button>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(3);
}}
>
Şifremi Unuttum!
</a>
</form>
<p>
Hələdə üye deyilsiniz? <br />
pulsuz registir olmak üçün
<b>
<u>
<a
style={{ fontSize: "18px" }}
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(2);
}}
>
kilik edin.
</a>
</u>
</b>
</p>
</div>
);
};
export default LoginView;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
For this problem I used a tricky way
first I deploy a state like this
const [routing,setRouting] = useState(false)
then when my works finished I changed it to true
and change my useEffect like this
useEffect(()=>{
if(routing)
navigation.navigate('AnotherPage')
),[routing]}