useState hook behaves differently than expected - reactjs

I have a simple language selector (en, cs) in my React app using i18next. The change of the language (applying all the translations and re-rendering the app) takes around 2 seconds.
In the meantime, I want to display a loader, but that doesn't seem to work as expected.
I have two scenarios. The first one does not display Loader:
const [isLoading, setIsLoading] = useState(false)
const [language, setLanguage] = useState(userPreferences.lang);
const handleChangeLanguage = (lang) => {
setIsLoading(true);
setLanguage(lang);
}
useEffect(() => {
i18n.changeLanguage(language).then(() => setIsLoading(false) );
},[language])
return (
<>
{isLoading ? <Loader /> : <div>lang selector here</div> }
</>
)
But when I use setTimeout (even with zero time) on setLanguage the loader is displayed until the lang changes:
const [isLoading, setIsLoading] = useState(false)
const [language, setLanguage] = useState(userPreferences.lang);
const handleChangeLanguage = (lang) => {
setIsLoading(true);
setTimeout(() => setLanguage(lang), 0); // <= change here
}
useEffect(() => {
i18n.changeLanguage(language).then(() => setIsLoading(false) );
},[language])
return (
<>
{isLoading ? <Loader /> : <div>lang selector here</div> }
</>
)
Why does it behave like that, and can I set it somehow to avoid setTimeout?
Thanks.

try this code instead of your function and remove useEffect
const handleChangeLanguage = async(lang) => {
setIsLoading(true);
try {
const res = await i18n.changeLanguage(lang);
setIsLoading(false)
} catch (error) {
console.log(error)
setIsLoading(false)
}
}

Try using setImmediate() instead of setTimeout()

Related

React not updating state?

I´m new to react. I´m trying to fetch an endpoints array. and I want to update the api's status every 15 seconds. I´m trying to do this
export const endpoints: string[] = [
"accounts/health/status",
"assets/health/status",
"customers/health/status",
"datapoints/health/status",
"devices/health/status",
"documents/health/status",
"forms/health/status",
"invites/health/status",
"media/health/status",
"messages/health/status",
"namespaces/health/status",
"orders/health/status",
"patients/health/status",
"relationships/health/status",
"rules/health/status",
"templates/health/status",
"users/health/status",
"workflows/health/status",
];
and I have this proxy in my package.json
"proxy": "https://api.factoryfour.com/",
Here the rest of my code
const [data, setData] = useState<Response[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string[] | null[]>([]);
const effectRan = useRef(false);
const fetching = async () => {
setLoading(true);
endpoints.map(async (endpoint) => {
return await axios
.get(endpoint)
.then((res) => {
setData((prev) => [...prev, res.data]);
})
.catch((err) => {
setError([...error, err.message]);
});
});
setLoading(false);
};
useEffect(() => {
if (!effectRan.current) {
fetching();
}
return () => {
effectRan.current = true;
};
});
useEffect(() => {
setTimeout(async () => {
setData([]);
setLoading(true);
setError([]);
await fetching();
}, 15000);
}, []);
but when the seTimeout runs every card duplicates and the state gets more data than before. even though I´m reseting the state to setData([]) I just want to update the api's status. What can i do?
if (loading) return <Spinner />;
return (
<div className="card-container">
{data.length ? (
data.map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{error.length
? error.map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>
```
Theres a few things wrong here and one or more probably fixes it:
You keep a ref around to track the first fetch but theres no need as you can do that by virtue of using [] in an effects deps array, which you already have.
The loading state does not wait until all requests in flight finished.
The 15 second interval does not wait until all requests launched are finished.
You dont clear down the timer if the component unmounts and remounts.
The data is not keyed against the endpoint which could land you in trouble if using React strictmode that runs affects twice in dev mode.
Your code, by design it seems, does append data each time one of the requests comes back -- but I think that was intentional?
const [data, setData] = useState<Record<string, Response>>({});
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Record<string, string | null>>({});
const fetching = async () => {
setLoading(true);
await Promise.all(
endpoints.map((endpoint) => {
return axios
.get(endpoint)
.then((res) => {
setData((prev) => ({...prev, [endpoint]: res.data}));
})
.catch((err) => {
setError((prev) => ({...prev, [endpoint]: err.message}));
});
})
);
setLoading(false);
};
useEffect(() => {
let timer: number | null = null;
const intervalFetch = async () => {
await fetching();
timer = setTimeout(async () => {
setError({});
setData({});
intervalFetch();
}, 15000);
};
intervalFetch();
return () => timer !== null && clearTimeout(timer);
}, []);
if (loading) return <Spinner />;
return (
<div className="card-container">
{Object.values(data).length ? (
Object.values(data).map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{Object.values(error).length
? Object.values(error).map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>)
I think this piece of code might be adding additional data instead of overwriting the existing one. Is that what you're trying to do?
setData((prev) => [...prev, res.data]);

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).

Can not change state of object [duplicate]

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>

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.

useState(new Map()) is not working, but object does

I honestly have no idea what is going on here. I have this code, on first render it should fetch popular repos and set them to the repos state, which should cause a re-render and paint the new repos on the DOM. The reason I use Map/obj is because I'm caching the repos to avoid re-fetch.
The code doesn't work as expected, it's not setting any new state, and I can verify it in the react dev tools. For some reason if I click around on Components in the devtools, the state updates(?!), but the DOM is still not painted (stuck on Loading), which is a very strange behavior.
export default () => {
const [selectedLanguage, setSelectedLanguage] = useState('All');
const [error, setError] = useState(null);
const [repos, setRepos] = useState(() => new Map());
useEffect(() => {
if (repos.has(selectedLanguage)) return;
(async () => {
try {
const data = await fetchPopularRepos(selectedLanguage);
setRepos(repos.set(selectedLanguage, data));
} catch (err) {
console.warn('Error fetching... ', err);
setError(err.message);
}
})();
}, [selectedLanguage, repos]);
const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);
const isLoading = () => !repos.has(selectedLanguage) && !error;
return (
<>
<LanguagesNav
selected={selectedLanguage}
updateLanguage={updateLanguage}
/>
{isLoading() && <Loading text="Fetching repos" />}
{error && <p className="center-text error">{error}</p>}
{repos.has(selectedLanguage)
&& <ReposGrid repos={repos.get(selectedLanguage)} />}
</>
);
};
However, if I change up the code to use object instead of a Map, it works as expected. What am I missing here? For example, this works (using obj instead of a Map)
const Popular = () => {
const [selectedLanguage, setSelectedLanguage] = useState('All');
const [error, setError] = useState(null);
const [repos, setRepos] = useState({});
useEffect(() => {
if (repos[selectedLanguage]) return;
(async () => {
try {
const data = await fetchPopularRepos(selectedLanguage);
setRepos(prev => ({ ...prev, [selectedLanguage]: data }));
} catch (err) {
console.warn('Error fetching... ', err);
setError(err.message);
}
})();
}, [selectedLanguage, repos]);
const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);
const isLoading = () => !repos[selectedLanguage] && !error;
return (
<>
<LanguagesNav
selected={selectedLanguage}
updateLanguage={updateLanguage}
/>
{isLoading() && <Loading text="Fetching repos" />}
{error && <p className="center-text error">{error}</p>}
{repos[selectedLanguage]
&& <ReposGrid repos={repos[selectedLanguage]} />}
</>
);
};
repos.set() mutates the current instance and returns it. Since setRepos() sees the same reference, it doesn't trigger a re-render.
Instead of
setRepos(repos.set(selectedLanguage, data));
you can use:
setRepos(prev => new Map([...prev, [selectedLanguage, data]]));

Resources