React. How to make function with state? - reactjs

I'm learning React Native and can't understand how to make function or component instead of repeated part of my code:
export const PostScreen = () => {
const postId = 1
/* THIS PART IS REPEATED IN MANY FILES */
const [data, setData] = useState([]);
const [loader, setLoader] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
setError(null)
setLoader(true)
try {
const data = await Api.get(`http://localhost/api/posts/${postId}`)
if (data.success == 1) {
setData(data.data)
}
else {
setError(data.error)
}
}
catch(e) {
console.log(e)
setError('Some problems')
}
finally {
setLoader(false)
}
}
useEffect(() => {
fetchData()
}, [])
if (loader) {
return <Loader />
}
if (error) {
return <View><Text>{error}</Text></View>
}
/*END>>> THIS PART IS REPEATED IN MANY FILES */
return (
<View><Text>{data.title}</Text></View>
)
}
The problem is that fetchData is working with state. I found, how to do it with Context, but I don't wont to use it. Is there any way to do clear code without Context?

So, why not make your own hook, then?
Write the hook in a dedicated module:
function useMyOwnHook(props) {
const {
postId,
} = props;
const [data, setData] = useState([]);
const [loader, setLoader] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
setError(null)
setLoader(true)
try {
const data = await Api.get(`http://localhost/api/posts/${postId}`)
if (data.success == 1) {
setData(data.data)
}
else {
setError(data.error)
}
}
catch(e) {
console.log(e)
setError('Some problems')
}
finally {
setLoader(false)
}
}
useEffect(() => {
fetchData()
}, [])
const render = loader
? <Loader />
: error
? <View><Text>{error}</Text></View>
: null;
return {
data,
render,
}
}
At that point, the component will be written as follows:
export const PostScreen = () => {
const postId = 1
const {
render,
data,
} = useMyOwnHook({ postId });
return render ?? (
<View><Text>{data.title}</Text></View>
)
}

Related

Stop invoking custom hook on first render

I started having fun with custom hooks recently. I am mostly using them to fetch from api. The thing is that since I cannot really put useFetchLink inside of functions or useEffect i dont know how to prevent it from fetching after website first render. I could put some ifs in the hook but isn't there any other way?
***component***
export default function LinkShortener({ setLinkArr }) {
const [nextLink, setNextLink] = useState();
const inputRef = useRef(null);
const handleClick = () => {
setNextLink(inputRef.current.value);
};
const { shortLink, loading, error } = useFetchLink(nextLink);
useEffect(() => {
setLinkArr((prev) => [
...prev,
{
id: prev.length === 0 ? 1 : prev[prev.length - 1].id + 1,
long: nextLink,
short: shortLink,
},
]);
inputRef.current.value = "";
}, [shortLink, error]);
return (
<LinkShortenerContainer>
<InputContainer>
<LinkInput ref={inputRef} type="text" />
</InputContainer>
<Button
size={buttonSize.medium}
text={
loading ? (
<Loader />
) : (
<FormattedMessage
id="linkShortener.shortenItBtn"
defaultMessage="Shorten It !"
/>
)
}
onClick={handleClick}
></Button>
</LinkShortenerContainer>
);
}
***hook***
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchLink = async () => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${linkToShorten}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLink(linkToShorten);
}, [linkToShorten]);
const value = { shortLink, loading, error };
return value;
};```
Why not using directly fetchLink function and calling it whenever you need inside the component? I would change the hook in this way without useEffect inside
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchLink = async () => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${linkToShorten}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const value = { shortLink, loading, error, fetchLink };
return value;
};
Generally speaking - the standard way to avoid useEffect from running of 1st render is to use a boolean ref initialized with false, and toggled to true after first render - see this answer.
However, in your case, you don't want to call the function if linkToShorten is empty, even if it's not the 1st render, so use an if inside useEffect.
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchLink = useCallback(async (linkToShorten) => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${linkToShorten}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if(linkToShorten) fetchLink(linkToShorten);
}, [fetchLink, linkToShorten]);
const value = { shortLink, loading, error };
return value;
};

How to display ActivityIndicator untill all elements are mapped

I have this screen in which I want to see ActivityIndicator untill all devices are mapped (not fetched):
const MyScreen = () => {
const [devices, setDevices] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
getDevices();
}, []);
const getDevices = async () => {
const pulledDevices = await fetchDevices();
setDevices(pulledDevices)
setIsLoading(false)
};
if (isLoading)
return (
<ActivityIndicator />
);
return (
<View >
{devices?.map((device) => {
return (
<View>
<Text>{device.name}</Text>
</View>
);
})}
</View>
);
};
Mapping these devices takes some time.
How could I implement here ActivityIndicator untill all devices are mapped.
I suggest you to use a bit more sophisticated async await hook to handle this.
useAsyncHook.js
const useAsync = asyncFunction => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(async () => {
setLoading(true);
setResult(null);
setError(null);
try {
const response = await asyncFunction();
setResult(response);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
}, [asyncFunction]);
useEffect(() => {
execute();
}, [execute]);
return { loading, result, error };
};
This is a raw use async hook that can be enhanced many way but it handles the loading state correctly in this state.
Usage:
const { loading, result, error } = useAsync(yourFunction);
if (loading) return null;
return <Component />;

TypeError: Cannot read property 'map' of undefined React Hooks

I need some help understanding why I'm getting the error from the title: 'TypeError: Cannot read property 'map' of undefined'. I need to render on the page (e.g state & country here) some data from the API, but for some reason is not working.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const APIFetch = () => {
const [user, setUser] = useState('');
const [info, setInfo] = useState([]);
const fetchData = async () => {
const data = await axios.get('https://randomuser.me/api');
return JSON.stringify(data);
}
useEffect(() => {
fetchData().then((res) => {
setUser(res)
setInfo(res.results);
})
}, [])
const getName = user => {
const { state, country } = user;
return `${state} ${country}`
}
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>
})}
</div>
)
}
Can you guys provide me some help? Thanks.
Try this approach,
const APIFetch = () => {
const [user, setUser] = useState("");
const [info, setInfo] = useState([]);
const fetchData = async () => {
const data = await axios.get("https://randomuser.me/api");
return data; <--- Heres is the first mistake
};
useEffect(() => {
fetchData().then((res) => {
setUser(res);
setInfo(res.data.results);
});
}, []);
const getName = (user) => {
const { state, country } = user.location; <--- Access location from the user
return `${state} ${country}`;
};
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>;
})}
</div>
);
};
Return data without stringify inside the fetchData.
Access user.location inside getName.
Code base - https://codesandbox.io/s/sharp-hawking-6v858?file=/src/App.js
You do not need to JSON.stringify(data);
const fetchData = async () => {
const data = await axios.get('https://randomuser.me/api');
return data.data
}
Do it like that
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const APIFetch = () => {
const [user, setUser] = useState('');
const [info, setInfo] = useState([]);
useEffect(() => {
const fetchData = async () => {
const res = await axios.get('https://randomuser.me/api');
setUser(res.data);
setInfo(res.data.results);
}
featchData();
}, [])
const getName = user => {
const { state, country } = user;
return `${state} ${country}`
}
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>
})}
</div>
)
}
Codesandbox: https://codesandbox.io/s/vigorous-lake-w52vj?file=/src/App.js

Array split React Hooks

I want to split an array into two arrays.
The problem is main array which I want to split into two is coming from server.And I need to wait until it loads.
Here my code.
This is useSafeFetch custom Hook which is responsible to fetch data (by the way this is working fine just paste here to show you all code)
const useSafeFetch = (url) => {
const [data, setData] = useState([]);
const [customUrl] = useState(url);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(_ => {
let didCancel = false;
const fetchData = async () => {
if(didCancel === false){
setIsError(false);
setIsLoading(true);
}
try {
const result = await axios(customUrl);
if(didCancel === false){
setData(result.data);
}
} catch (error) {
if(didCancel === false){
setIsError(true);
}
}
if(didCancel === false){
setIsLoading(false);
}
};
fetchData();
return () => {
didCancel = true;
};
}, []);
return {
data,
isLoading,
isError,
}
}
I try to write a function which return a two independent array
export default _ => {
const {data,isLoading,isError} = useSafeFetch(`my api`);
useEffect(_ => {
console.log(data); // length 11
const mainA = splitTwoArrays(data);
console.log("progress",mainA.progressHalf); //length 5
console.log("circle", mainA.circleHalf); //length 1
});
const splitTwoArrays = mainArr => {
const half = mainArr.length >>> 1;
let progressHalf = mainArr.splice(0, half);
let circleHalf = mainArr.splice(half, mainArr.length);
console.log(mainArr);
return {
progressHalf,
circleHalf,
}
}
return (
//do something with data
)
}
This is not worked correctly.
As you can see main data length is 11 but function splitTwoArrays split arrays with wrong way. progressHalf length is 5 another circleHalf is 1.But circleHalf need to 6.
Next try:
using useEffect
export default _ => {
const {data,isError,isLoading} = useSafeFetch(`my api`);
const [progressHalf,setProgressHalf] = useState([]);
const [newArr,setNewArr] = useState([]);
const [half,setHalf] = useState(0);
useEffect(_ => {
setHalf(data.length >>> 1);
setNewArr(data);
const partArr = newArr.slice(0, half);
setProgressHalf([...progressHalf, ...partArr]);
})
return (
//do something with data
)
}
This gets into infinity loop when I uncomment this part setProgressHalf([...progressHalf, ...partArr]);.
I try to give useEffect some dependency but unfortunately this also won't work.
I solve this on my own.
const { data } = useSafeFetch("https://jsonplaceholder.typicode.com/users");
const [copiedData, setCopiedData] = useState([]);
const [halfArr, setHalfArr] = useState([]);
const [secHalf, setSecHalf] = useState([]);
const [half, setHalf] = useState(0);
useEffect(
_ => {
setCopiedData([...data]);
setHalf(data.length >>> 1);
setHalfArr([...copiedData.slice(0, half)]);
setSecHalf([...copiedData.slice(half, copiedData.length)]);
},
[data, half]
);
console.log(halfArr);
console.log(secHalf);
And in the end you get two array which created from main data you get from server.
Codesandbox

Does this make sense as a useApi-type hook? How do I properly type where it's used?

This is a hook I wrote to consume an existing "data loader" class that handles the authentication and data fetching itself, in order to expose things like loading flags and errors better.
export const useApi = (endpoint: string, options: object = {}) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const loader = DataLoader.instance
useEffect((): (() => void) | undefined => {
if (!endpoint) {
console.warn('Please include an endpoint!')
return
}
let isMounted = true // check component is still mounted before setting state
const fetchUserData = async () => {
isMounted && setIsLoading(true)
try {
const res = await loader.load(endpoint, options)
if (!res.ok) {
isMounted && setData(res)
} else {
throw new Error()
}
} catch (error) {
isMounted && setError(error)
}
isMounted && setIsLoading(false)
}
fetchUserData()
return () => (isMounted = false)
}, [endpoint])
return { data, isLoading, error }
}
Does it make sense? I then use it like so:
const { data, isLoading, error } = useApi(Endpoints.user.me)
Assuming it's OK, how do I properly type the consumer? When I try to use a particular property on data, TypeScript will complain that "the Object is possibly null".
Many thanks in advance.
At glance this hook seems reasonable. As for the proper typing here is when the TS Generics comes into play
import { useEffect, useState } from "react";
// This is the least example, what I assume the DataLoader should looks like.
// Here you need to declare the proper return type of the *load* function
const DataLoader = {
instance: {
load: (endpoint, options) => {
return {
ok: true
};
}
}
};
type ApiData = {
ok: boolean;
};
export const useApi = <T extends ApiData>(
endpoint: string,
options: object = {}
) => {
const [data, setData] = useState<T>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const loader = DataLoader.instance;
useEffect((): (() => void) | undefined => {
if (!endpoint) {
console.warn("Please include an endpoint!");
return;
}
let isMounted = true;
const fetchUserData = async () => {
isMounted && setIsLoading(true);
try {
// I used here *as* construction, because the *res* type should match with your *useState* hook type of data
const res = (await loader.load(endpoint, options)) as T;
if (!res.ok) {
isMounted && setData(res);
} else {
throw new Error();
}
} catch (error) {
isMounted && setError(error);
}
isMounted && setIsLoading(false);
};
fetchUserData();
return () => (isMounted = false);
}, [endpoint]);
return { data, isLoading, error };
};
But if you want to do this in the right way, you should also declare a DataLoader class (or whatever you have) with an ability to pass the Generic of the data type

Resources