useEffect within a custom hook doesn't rerender on delete - reactjs

I am trying to use hooks and implement a custom hook for handling my data fetching after every update I send to the API.
My custom hook, however, doesn't fire on change like I want it too. Delete has to be clicked twice for it to rerender. Note: I removed some functions from this code as they don't pertain to the question.
import React, { useState, useEffect } from "react"
import {Trash} from 'react-bootstrap-icons'
import InlineEdit from 'react-ions/lib/components/InlineEdit'
function Board(){
const [render, setRender] = useState(false)
const [boards, setBoards] = useState([]);
const [isEditing, setEdit] = useState(false)
const [value, setValue] = useState("")
const[newValue, setNewValue] = useState("")
const [error, setError] = useState("")
function useAsyncHook(setState, trigger) {
const [result] = useState([]);
const [loading, setLoading] = useState("false");
useEffect(() => {
async function fetchList() {
try {
setLoading("true");
const response = await fetch(
`http://localhost:8080/api/boards`
);
const json = await response.json();
setState(json)
} catch (error) {
//console.log(error)
setLoading("null");
}
}
fetchList()
}, [trigger]);
return [result, loading];
}
useAsyncHook(setBoards, render)
const handleDelete = (id) => {
console.log("delete clicked")
setLoading(true);
fetch(`http://localhost:8080/api/boards/` + id, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
},
})
setRender (!render)
}
return(
<div>
<ul>
{boards.map(board => (
<li key={board.id}>
<InlineEdit value={board.size} isEditing={isEditing} changeCallback={(event)=>handleSave (event, board.id)} />
<Trash onClick={()=>handleDelete(board.id)}/>
</li>
</ul>
</div>
);
}
export default Board

OPTION 1:
Maybe you wanna have a hook that tells you when to fetch the board, right? For example:
const [auxToFetchBoard, setAuxToFetchBoard] = useState(false);
Then, in a useEffect you execute the function fetchBoard everytime that hook changes:
useEffect(fetchBoard, [auxToFetchBoard]);
Finally, in your handleDelete function, if your delete request returns correctly, you have to update auxToFetchBoard. Something like this:
const handleDelete = (id) => {
setIsLoading(true);
setError("");
fetch(yourURL, yourOptions)
.then(res => {
// check if response is correct and
setIsLoading(false);
setAuxToFetchBoard(!auxToFetchBoard);
})
.catch(() => {
setIsLoading(false);
setError("Error while deleting stuff");
});
};
Note: I changed the names of isLoading and setIsLoading because they are more explicit.
OPTION 2:
Instead of fetching the board again and again, you can update your board (in this case your code would be in 8th line inside the handleDeletefunction).
Hope it helps.

Related

Why useState is not accepting input received from the GET request?

I am trying to get an object from the server in the form {Name: true, Place: false, Animal: true, Thing: true} save this data into categoryDetail then extract it using categoryDetail.Name and then pass it to the useState. But somehow useState is not accepting this data.
Here is the code:
const [categoryDetail, setCategoryDetail] = useState({});
useEffect(() => {
axios.get('http://localhost:8080/feeds/category')
.then(response => {
if (JSON.stringify(categoryDetail)
!== JSON.stringify(response.data.category)) {
setCategoryDetail(response.data.category);
}
})
.catch(err => {
console.log(err);
})
})
console.log(categoryDetail.Name); // 👉 this gives ``true``
const [name, setName] = useState(categoryDetail.Name);
const [place, setPlace] = useState(categoryDetail.Place);
const [animal, setAnimal] = useState(categoryDetail.Animal);
const [thing, setThing] = useState(categoryDetail.Thing);
console.log(name); // 👉but here i am getting ``undefined``
(I have commented on the value I am getting)
Please guide me on why is this happening and what to do so that useState accepts the data receive by the server.Also let me know if more information is required.
Try make your code like this
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const App = () => {
const [categoryDetail, setCategoryDetail] = useState({});
const [name, setName] = useState('');
// const [place, setPlace] = useState('');
// const [animal, setAnimal] = useState('');
// const [thing, setThing] = useState('');
useEffect(() => {
const fetchCategories = async () => {
await axios
.get('https://mocki.io/v1/5a61740b-d272-4943-abe3-908628510020')
.then((response) => {
setCategoryDetail(response.data.categories[0]);
setName(response.data.categories[0].categoryName);
});
};
fetchCategories();
}, []);
// https://mocki.io/v1/5a61740b-d272-4943-abe3-908628510020
return (
<>
<p>{name}</p>
<p>{JSON.stringify(categoryDetail)}</p>
</>
);
};
export default App;
And as you see , I am doing call to setName after fetchCategories() inside of async/await call , put other state setters there
You set default value categoryDetail.Name for useState which will be never modified in renderings (I'm doubting that it's possibly undefined or an error if categoryDetail data is not there)
If you want to get name data from categoryDetail. You can set state after receiving response from useEffect
const [name, setName] = useState(''); //to be safe, set it empty
useEffect(()=>{
axios.get('http://localhost:8080/feeds/category')
.then(response=>{
if (JSON.stringify(categoryDetail) !== JSON.stringify(response.data.category)) {
setName(response.data.category.name); //set `name` into the state
setCategoryDetail(response.data.category);
}
})
.catch(err=>{
console.log(err);
})
})
From your logic, seemingly you're trying to access one by one field from categoryDetail state which is not preferable
If you want to get name, you just simply get it from categoryDetail.Name which is already set in the state
const[categoryDetail, setCategoryDetail]=useState({});
useEffect(()=>{
axios.get('http://localhost:8080/feeds/category')
.then(response=>{
if (JSON.stringify(categoryDetail) !== JSON.stringify(response.data.category)) {
setCategoryDetail(response.data.category);
}
})
.catch(err=>{
console.log(err);
})
})
//make sure your categoryDetail is not undefined
if(categoryDetail) {
console.log(categoryDetail.Name);
}

How to process data received from an AJAX request in React

I have a custom hook named "useFetch" which makes an AJAX request and stores the result in the state. I simply want to format the data received from the ajax using a function in my component but not sure how to do this since the function needs to be called only after the data is received.
An example is below:
import React, { Component, useState } from "react";
import useFetch from "../../../Hooks/useFetch";
const Main = () => {
const { data, isPending, error } = useFetch(
"http://127.0.0.1:8000/api/historic/1"
);
function formatData(data){
//Do some processing of the data after it's been received
}
//This doesn't work of course because it runs before the data has been received
const formatted_data=formatData(data);
return (
//Some display using the formatted data
);
};
export default Main;
This is the custom hook, useFetch, which is used in the above component. I'd prefer to not have to do the formatting in here because the formatting is specifically related to the above component and this custom hook is designed to have more universal utility.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isPending, setisPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
fetch(url, { signal: abortCont.signal })
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw Error("could not fetch data for that resource");
}
})
.then((data) => {
setData(data);
setisPending(false);
setError(null);
})
.catch((er) => {
if (er.name === "AbortError") {
console.log("fetch aborted");
} else {
setError(er.message);
setisPending(false);
}
});
return () => abortCont.abort();
}, [url]);
return { data, isPending, error };
};
export default useFetch;
You should wrap it with useEffect hook with data as it's deps.
const [formattedData, setFormattedData] = useState();
useEffect(() => {
if (!data) return;
const _formattedData = formatData(data);
setFormattedData(_formattedData);
}, [data]);

useState variable is called before useEffect API call

From what I understand useEffect hook runs last as a sideEffect. I am attempting to console log data.main.temp. I can understand that it doesn't know what that is yet, because it is fetching the data from the api in the useEffect hook which runs after.
How would I be able to access or console log data.main.temp AFTER the api call? (I feel like setTimout is the cheating way?)
import React, { useState, useEffect } from "react";
import Button from "../UI/Button";
import styles from "./Weather.module.css";
import moment from "moment";
import Card from "../UI/Card";
export default function Weather() {
//State Management//
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
const [data, setData] = useState([]);
//openWeather API key
const key = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
useEffect(() => {
const fetchData = async () => {
//get coordinates//
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
//fetch openWeather api//
await fetch(`https://api.openweathermap.org/data/2.5/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${key}`)
.then((res) => res.json())
.then((result) => {
setData(result);
console.log(result);
});
};
fetchData();
}, [lat, long]);
//Examples of what I want, they run too early before api//
console.log(data.main.temp);
const Farenheit = data.main.temp * 1.8 + 32;
return (
<Card>
{typeof data.main != "undefined" ? (
<div className={`${styles.weatherContainer} ${styles.clouds}`}>
<h2>Weather</h2>
<p>{data.name}</p>
<p>{data.main.temp * 1.8 + 32} °F</p>
<p>{data.weather[0].description}</p>
<hr></hr>
<h2>Date</h2>
<p>{moment().format("dddd")}</p>
<p>{moment().format("LL")}</p>
</div>
) : (
<div></div>
)}
</Card>
);
}
You're right, the effect function is run after the first render which means you need to wait somehow until your api call is done. One common way to do so is to introduce another state flag which indicate whether the data is available or not.
Another thing which does not follow react good practices is the fact that you're effect function does more than one thing.
I also added trivial error handling and cleaned up mixed promises and async await
here is your refactored code
import React, { useState, useEffect } from "react";
import Button from "../UI/Button";
import styles from "./Weather.module.css";
import moment from "moment";
import Card from "../UI/Card";
//openWeather API key
const key = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
export default function Weather() {
//State Management//
const [lat, setLat] = useState();
const [long, setLong] = useState();
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
}, []);
useEffect(() => {
const fetchData = async () => {
if (lat && long && key) {
try {
setLoading(true);
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${key}`
);
const data = await response.json();
setData(data);
setLoading(false);
} catch (err) {
setError(err);
setLoading(false);
}
}
};
fetchData();
}, [lat, long]);
if (error) {
return <div>some error occurred...</div>;
}
return (
<Card>
{loading || !data ? (
<div>loading...</div>
) : (
<div className={`${styles.weatherContainer} ${styles.clouds}`}>
<h2>Weather</h2>
<p>{data.name}</p>
<p>{data.main.temp * 1.8 + 32} °F</p>
<p>{data.weather[0].description}</p>
<hr></hr>
<h2>Date</h2>
<p>{moment().format("dddd")}</p>
<p>{moment().format("LL")}</p>
</div>
)}
</Card>
);
}
You can use another useEffect, which depends on changing the data state
useEfect(() => {
if (data) {
// do something with data
}
}, [data])
You can create a simple function and call it in your API call response and pass in the data directly from the api response, that way you will have access to the data immediately there's a response.
E.g
...
.then((result) => {
setData(result);
getDataValue(result) // this function will be called when the response comes in and you can use the value for anything
console.log(result);
});
METHOD 2:
You can use a useEffect hook to monitor changes in the data state, so that whenever there's an update on that state, you can use the value to do whatever you want. This is my less preferred option.
useEffect(() => {
//this hook will run whenever data changes, the initial value of data will however be what the initial value of the state is
console.log(data) //initial value = [] , next value => response from API
},[data])

implement useFetch react hook to work inside submit function

I have a lot of react experience but I'm new to hooks.
I have the following useFetch hook that I modified after this useAsync hook:
import { useState, useEffect, useCallback } from 'react'
export default function useFetch(url, options, { immediate }) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [isPending, setIsPending] = useState(false)
const executeFetch = useCallback(async () => {
setIsPending(true)
setData(null)
setError(null)
await fetch(url, options)
.then((response) => response.json())
.then((response) => setData(response))
.catch((err) => setError(err))
.finally(() => setIsPending(false))
return { data, error, isPending }
}, [url, options, data, error, isPending])
useEffect(() => {
if (immediate) {
executeFetch()
}
}, [executeFetch, immediate])
return { data, error, isPending, executeFetch }
}
My problem is I want to use it inside a submit function, and hooks don't work inside other functions, like so (reduced version of the code for brevity):
export default function SignupModal({ closeModal }) {
const { executeFetch } = useFetch(url, {options},
{ immediate: false }
)
async function handleSubmit(evt) {
evt.preventDefault()
const { data, error, isPending } = await executeFetch()
}
...
}
currently I'm intentionaly throwing an error in the call, but the error variable remains null.
What am I missing here?
Is this even possible with hooks?
Thanks in advance!
React hook can only be used in the body of your component not inside another function. executeFetch itself is returning { data, error, isPending } and this makes it a nested hook so you can't use it inside your handleSubmit.
useFetch is already returning { data, error, isPending, executeFetch } so executeFetch doesn't need to return again. You can access all these data from the useFetch hook. When you call executeFetch data in your component, data, error and isPending will be updated by setState which will cause your hook to return a new set of values for any of these values that get updated.
export default function useFetch(url, options, { immediate }) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [isPending, setIsPending] = useState(false)
const executeFetch = useCallback(async () => {
setIsPending(true)
setData(null)
setError(null)
await fetch(url, options)
.then((response) => response.json())
.then((response) => setData(response))
.catch((err) => setError(err))
.finally(() => setIsPending(false))
}, [url, options, data, error, isPending])
useEffect(() => {
if (immediate) {
executeFetch()
}
}, [executeFetch, immediate])
return { data, error, isPending, executeFetch }
}
export default function SignupModal({ closeModal }) {
const { executeFetch, data, error, isPending } = useFetch(url, {options},
{ immediate: false }
)
async function handleSubmit(evt) {
evt.preventDefault()
await executeFetch()
}
...
// Example in your return function
{error != null && <Error />}
<Button state={isPending ? 'processing' : 'normal'}
}
Updated based on the comment
If you need to have an access to data or error inside your handleSubmit function, you will need to return the promise's response/error in your hook so then you should be able to access data/error inside your handleSubmit as well.
Also I recommend to pass options or any other variable data that are subject to change before user triggers handleSubmit to the executeFetch as an argument so executeFetch can always get the latest data.
CodeSandBox Example 1
CodeSandBox Example 2
const useFetch = url => {
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const [data, setData] = useState(null);
const executeFetch = useCallback(
// Here you will access to the latest updated options.
async ({ options }) => {
setIsPending(true);
setError(null);
return await fetch(url, options)
.then(response => response.json())
.then(response => {
setData(response);
return response;
})
.catch(err => {
setError(err.message)
return err;
})
.finally(() => setIsPending(false));
},
[url, setIsPending, setError]
);
return { data, error, isPending, executeFetch }
};
const { data, executeFetch, error, isPending } = useFetch("URL");
const handleSubmit = useCallback(async (event) => {
event.preventDefault();
// I am passing hardcoded { id: 1 } as an argument. This can
// be a value from the state ~ user's input depending on your
// application's logic.
await executeFetch({ id: 1 }).then(response => {
// Here you will access to
// data or error from promise.
console.log('RESPONSE: ', response);
})
}, [executeFetch]);
Another recommendations is to not pass a boolean to trigger executeFetch immediately inside your hook, it's up to the caller to decide whether to run the executeFetch immediately or not.
const { executeFetch, ... } = useFetch(....);
// you can call it immediately after setting the hook if you ever needed
await executeFetch()

How to correctly call useFetch function?

I've successfully implemented a useFetch function to call an API Endpoint. It works perfectly if I add code like this to the root of a functional React component like this:
const [{ data, isLoading, isError }] = useFetch(
'http://some_api_endpoint_path'
);
export const useFetch = (url) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await axios.get(url);
setData(response.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }];
};
But let's say I want to check if a newly entered username exists, say upon the firing of an onBlur event of an input element. When I've tried implementing this, I get this error:
React Hook "useFetch" is called in function "handleBlur" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
I even tried this approach:
const [isChanged, setIsChanged] = useState(false);
useEffect(() => {
useFetch(
'http://some_api_endpoint_path'
);
}, [isChanged]);
But got the same error.
Then I tried this simplified version, which doesn't do anything useful but I was testing the React Hooks Rules:
useEffect(() => {
useFetch(
'http://some_api_endpoint_path'
);
}, []);
And still I got the same error.
In these last 2 cases especially, I feel that I am following the Rules of Hooks but apparently not!
What is the correct way to call useFetch in such a situation?
I suppose you call useFetch this way, right?
const onBlur = () => {
const [{ data, isLoading, isError }] = useFetch(
'http://some_api_endpoint_path'
);
...
}
If true, this is wrong. Check this link out:
🔴 Do not call in event handlers.
You may implement this way:
// Pass common initial for all fetches.
export const useFetch = (awsConfig, apiRoot, apiPathDefault) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// Just pass the variables that changes in each new fetch requisition
const fetchData = async (apiPath) => {
setIsError(false);
setIsLoading(true);
try {
const response = await axios.get(apiRoot + apiPath);
setData(response.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData(apiRoot + apiPathDefault);
}, [awsConfig, apiRoot, apiPathDefault]);
return [{ data, isLoading, isError }, fetchData];
};
And whenever you want to fetch again, you just call fetchData:
const [{ data, isLoading, isError }, fetchData] = useFetch(API_ROOT(), appStore.awsConfig, defaultPath);
const onBlur = () => {
fetchData(newPath);
...
}
I've used the same principle that Apollo team used when created useLazyQuey (open this link and search for useLazyQuery, please). Also, note that I pass all common and immutable variables when I call the hooks and pass just the mutable ones in the single fetch.

Resources