Can not change state of object [duplicate] - reactjs

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I am trying to fetch a single object, and it fetches, I get correct data in response.data, but i can't set it to state. It just remains as a null object. What am I doing wrong?
Post is a json object with several fields like: post_id, post_title, post_content and etc.
const [post, setPost] = useState({})
let id = match.params.id
useEffect(() => {
axios.get(`${BASE_URL}/post?id=${id}`)
.then(response => {
console.log(response.data)
setPost(response.data)
})
.then(() => {
console.log("post: ", post)
}
)

setAction like your setPost are asynchronous, as stated in the official REACT documentation (https://reactjs.org/docs/hooks-reference.html#usestate); this means that, once the setAction is executed, you don't know when it will be actually executed and terminated: you will know because the component will re-render.
In your case, if you'd like to perform action AFTER post has got the new value, you would need the useEffect hook (https://reactjs.org/docs/hooks-reference.html#useeffect):
React.useEffect(() => {
console.log(post);
}, [post]);
By the way, I think you would want the data inside the response, so you would probably save the JSON retrieved from the body of the HTTP Response, that you can get using response.json().
EDIT: As stated in the comment from Siradji Awoual, what I wrote about response and response.json() is not valid for Axios (but it still is for fetch API).

Setting a state is asynchronous. That means you don't know exactly when that action will finish executing.
If I were you, I would use something like useEffect to check if the state is being set.
React.useEffect(() => console.log(post), [post])

Using axios.get is low-level and requires that you hook up a bunch of extra stuff to get things working correctly. Instead, try writing custom hooks to abstract this logic away -
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Using useAsync looks like this -
const MyApp = () => {
const { loading, error, result } =
useAsync(_ => axios.get("./foo.json").then(res => res.json()))
if (loading)
return <p>loading...</p>
if (error)
return <p>error: {error.message}</p>
return <pre>result: {result}</pre>
}
But you will probably have many components that fetch JSON, right? We can make an even higher level custom hook, useJSON that is a specialization of useAsync -
const fetchJson = (url = "") =>
axios.get(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") =>
useAsync(fetchJson, [url]) // <-- useAsync
const MyApp = () => {
const { loading, error, result } =
useJson("./foo.json") // <-- dead simple
if (loading)
return <p>loading...</p>
if (error)
return <p>error: {error.message}</p>
return <pre>result: {result}</pre>
}
See the custom hooks in action in this functioning code snippet -
const { useState, useEffect } =
React
// fake fetch slows response down so we can see loading
const _fetch = (url = "") =>
fetch(url).then(x =>
new Promise(r => setTimeout(r, 2000, x)))
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
const fetchJson = (url = "") =>
_fetch(url).then(r => r.json())
const useJson = (url = "") =>
useAsync(fetchJson, [url])
const MyComponent = ({ url = "" }) => {
const { loading, error, result } =
useJson(url)
if (loading)
return <pre>loading...</pre>
if (error)
return <pre style={{color: "tomato"}}>error: {error.message}</pre>
return <pre>result: {JSON.stringify(result, null, 2)}</pre>
}
const MyApp = () =>
<main>
ex 1 (success):
<MyComponent url="https://httpbin.org/get?foo=bar" />
ex 2 (error):
<MyComponent url="https://httpbin.org/status/500" />
</main>
ReactDOM.render(<MyApp />, document.body)
pre {
background: ghostwhite;
padding: 1rem;
white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

Related

useEffect not seeing dependency change on single click

I am making a call to an Api using the following hook. It returns 10 pictures at a time.
export const useFetchData = (url, page) => {
const [error, setError] = useState(null)
const [apiData, setApiData] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const res = await axios.get(*****);
const data = await res.data
setApiData(data)
} catch (e) {
setError(e)
} finally {
setLoading(false)
}
}
fetchData()
}, [page, url])
return { apiData, loading, error }
}
I am trying to do pagination in the following component by changing the state value of page by using the nextPage and backPage functions.
let [page, setPage] = useState(1);
let { apiData, loading, error } = useFetchData(url, page);
const nextPage = () => {
setPage(page ++);
};
const backPage = () => {
setPage(page --);
};
return (
<div className="photo-display__buttons-container">
<button onMouseDown={()=>backPage()}>Back</button>
<button onClick={()=>nextPage()}>Next</button>
</div>
<main className="photo-display">
<div className="photo-display__container">
{apiData?.photos.map((photo) => (
<Photo key={photo.id} photo={photo} />
))}
</div>
</div>
);
};
export default App;
By extensive console logging I am able to see that the state value is changed and the hook is called but the try catch does not execute on a single click.
Only if it is double clicked does the try catch execute. The state value is temporarily changed to reflect the double increase but after the hook is called in goes back to the correct value.
Why? and How do i get it to work on a single click?
When you do setPage you are using a postfix ++, which means the original value will be returned (and then incremented). You need to use a prefix ++ so that it is incremented first, then passed in to setState, or just skip the ++ entirely and do setState(i + 1).
Eg (postfix):
let i = 0;
console.log(i++);
Eg (prefix):
let i = 0;
console.log(++i);
Try changing setPage(page ++) to setPage(page+1).

async custom hooks does't provide updated data

I have a simple hook to help me handle a POST request. With the following code, I expect unsub will be true after the POST is done. Can anyone point out anything I could have done wrong?
Custom Hook
const useUnsubscribeEmail = () => {
const [userId, setUserId] = useState(null);
const [unsub, setUnSub] = useState();
const UNSUB_URL = '/web-registry-api/v1/reviews/unsubscription';
useEffect(() => {
if (userId) {
// async POST call
(async () => {
try {
await ApiService.post(`${UNSUB_URL}/${userId}`);
// update unsub value
setUnSub(true);
} catch (error) {
console.error(error)
}
})();
}
}, [userId]);
return [unsub, setUserId];
};
export default useUnsubscribeEmail;
Component
const ReviewUnsubscription = () => {
const { userId } = useParams();
const [unsub, unsubscribeEmail] = useUnsubscribeEmail();
return (
<MinimumLayout>
<div className={styles.content}>
<h1>Unsubscribe from email reminders to review products you’ve received from Zola?{unsub}</h1>
{/* unsub here is still undefined */}
<Button disabled={unsub} onClick={() => { unsubscribeEmail(userId); }} variant="primary" className={styles.button}>Unsubscribe</Button>
</div>
</MinimumLayout>
);
};
unsub is still going to be undefined until you click the button as you have not set a default state for it in your hook.
change : const [unsub, setUnSub] = useState(); to const [unsub, setUnSub] = useState(false); is what I would recommend
I tested on my side and works just fine; However, I cannot test the APIService.post.

Inifinite loop when saving an object from async await

When I create and object out of async/await operation...
export const getData = async datas => {
const a1 = await getData1(datas);
return { a1 };
};
...and then save it with useState...
import { useState, useEffect } from "react";
import { getData } from "./getData";
export const useData = ababab => {
const [data, setData] = useState();
useEffect(() => {
const loadData = async () => {
const newData = await getData(ababab);
setData(newData);
};
console.log(Date.now().toString());
loadData();
}, [ababab]);
return data;
};
...I get an infinite loop. I don't get it.
If you comment out the setData - it won't loop.
If you return just a1, it will not loop.
Here is where useData is used:
import React from "react";
import "./styles.css";
import { useAbabab } from "./usaAbabab";
import { useData } from "./useData";
export default function App() {
const ababab = useAbabab();
const data = useData(ababab);
return (
<div className="App">
<h1>Hello CodeSandbox {data && data.a1}</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
And here are the contents of useAbabab:
import { useState } from "react";
export const useAbabab = () => {
const [aaa, setAaa] = useState(0);
const [bbb, setBbb] = useState(5);
return { aaa, bbb, setAaa, setBbb };
};
Code Sandbox example
As you've probably gathered, the infinite loop is caused by the useEffect in useData, which is triggered by a change to ababab (can be shown by removing ababab from the dependency array).
While ababab is really just two full useState outputs together in an object, the object itself is redefined on each render, triggering the useEffect to run.
The simplest way I can think to fix this is to wrap the return value of useAbabab in a useMemo, like this:
import { useState, useMemo } from "react";
export const useAbabab = () => {
const [aaa, setAaa] = useState(0);
const [bbb, setBbb] = useState(5);
return useMemo(() => ({ aaa, bbb, setAaa, setBbb }), [aaa, bbb]);
};
It's hard to tell precisely what your code is doing because of the ababa variable names, but from what I can read in your code, it looks like you're want a generic hook around an asynchronous resource -
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Using our custom hook usAsync looks like this -
function App() {
const ababab =
useAbabab()
const { loading, error, result } =
useAsync(getData, [ababab]) // async function, args to function
if (loading)
return <p>Loading...</p>
if (error)
return <p>Error: {error.message}</p>
return <div>Got data: {result}</div>
}
useAsync is a versatile generic hook that can be specialized in other useful ways -
const fetchJson = (url = "") =>
fetch(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") =>
useAsync(fetchJson, [url]) // <-- useAsync
const MyComponent = ({ url = "" }) => {
const { loading, error, result } =
useJson(url) // <-- dead simple
if (loading)
return <pre>loading...</pre>
if (error)
return <pre className="error">error: {error.message}</pre>
return <pre>result: {result}</pre>
}
ReactDOM.render(
<MyComponent url="https://httpbin.org/get?foo=bar" />,
document.body
)
Run the snippet below to see useAsync and useJson working in your own browser -
const { useState, useEffect } =
React
// fake fetch slows response down so we can see loading
const _fetch = (url = "") =>
fetch(url).then(x =>
new Promise(r => setTimeout(r, 2000, x)))
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
const fetchJson = (url = "") =>
_fetch(url).then(r => r.json())
const useJson = (url = "") =>
useAsync(fetchJson, [url])
const MyComponent = ({ url = "" }) => {
const { loading, error, result } =
useJson(url)
if (loading)
return <pre>loading...</pre>
if (error)
return <pre style={{color: "tomato"}}>error: {error.message}</pre>
return <pre>result: {JSON.stringify(result, null, 2)}</pre>
}
const MyApp = () =>
<main>
ex 1 (success):
<MyComponent url="https://httpbin.org/get?foo=bar" />
ex 2 (error):
<MyComponent url="https://httpbin.org/status/500" />
</main>
ReactDOM.render(<MyApp />, document.body)
pre {
background: ghostwhite;
padding: 1rem;
white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

useEffect within a custom hook doesn't rerender on delete

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.

Too many re-renders with React Hooks

Similar questions have been asked but I haven't found a solution for this particular one. I have one component which renders all boards and I am using a custom useFetch hook to fetch all boards.
const BoardsDashboard = () => {
let [boards, setBoards] = useState([]);
const { response } = useFetch(routes.BOARDS_INDEX_URL, {});
setBoards(response);
return (
<main className="dashboard">
<section className="board-group">
<header>
<div className="board-section-logo">
<span className="person-logo"></span>
</div>
<h2>Personal Boards</h2>
</header>
<ul className="dashboard-board-tiles">
{boards.map(board => (
<BoardTile title={board.title} id={board.id} key={board.id} />
))}
<CreateBoardTile />
</ul>
</section>
</main>
);
};
const useFetch = (url, options) => {
const [response, setResponse] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
I am getting too many re-renders due to setBoards(response) line. What is the right way to handle this?
Thanks!
Sounds like you might want a useEffect hook to take action when response is updated.
useEffect(() => {
setBoards(response);
}, [response]);
Note: if you have no need to ever change the boards state, then maybe it doesn’t need to be stateful at all, you could just use the returned value from your useFetch hook and be done with it.
const { response: boards } = useFetch(...);

Resources