I am going through one tutorial of REST API and react, but I want to move a step ahead and write clean code. Therefore, I want to ask you for some help, so I could reuse hooks for different API requests.
With the tutorial, I've written this example where Hooks are used for saving API request statuses and think this is a good pattern I could reuse. Basically everything except const data = await API.getItems(token['my-token']) could be used for all/most API request I want to make. How should I reuse code with these technologies when building a clean API framework?
import { useState, useEffect } from 'react';
import { API } from '../api-service';
import { useCookies } from 'react-cookie';
function useFetch() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const [token] = useCookies(['my-token']);
useEffect( ()=> {
async function fetchData() {
setLoading(true);
setError();
const data = await API.getItems(token['my-token'])
.catch( err => setError(err))
setData(data)
setLoading(false)
}
fetchData();
}, []);
return [data, loading, error]
}
export { useFetch }
BIG Thanks!
Looks like you already have a custom hook. You can just use it in whichever component you want.
Basically everything except const data = await
API.getItems(token['my-token']) could be used for all/most API request
I want to make.
You can send some arguments to your custom hook and dynamically send requests based on those parameters.
function useFetch(someArgument) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const [token] = useCookies(["my-token"]);
useEffect(() => {
async function fetchData() {
setLoading(true);
setError();
if (someArgument === 'something') {
const data = await API.getItems(token["my-token"]).catch((err) =>
setError(err)
);
} else {
// do something else
}
setData(data);
setLoading(false);
}
fetchData();
}, []);
return [data, loading, error];
}
Using the custom hook in a component:
import React from "react";
export default ({ name }) => {
const [data, loading, error] = useFetch('something');
return <h1>Hello {name}!</h1>;
};
I think you've done pretty good job, but you can improve the code by parameterizing it a bit, maybe some thing like (in case you use cookies and tokens always the same way):
import { useState, useEffect } from 'react';
import { API } from '../api-service';
import { useCookies } from 'react-cookie';
function useFetch(cookieName, promiseFactory) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const [token] = useCookies([cookieName]);
useEffect( ()=> {
async function fetchData() {
setLoading(true);
setError();
const data = await promiseFactory(token[cookieName])
.catch( err => setError(err))
setData(data)
setLoading(false)
}
fetchData();
}, []);
return [data, loading, error]
}
export { useFetch }
You can add cookieName or array of cookie names and also the promise, which obviously could be different. promiseFactory will accept the token and do its specific job (token) => Promise in order to encapsulate the token retrieval logic in the hook, that is the token[cookieName] part.
Usage would be:
function SomeComponent {
const promiseFactory = (token) => API.getItems(token);
const [data, loading, error] = useFetch('my-token', promiseFactory );
}
If the cookies logic and token retrieval is also specific only to the case you provided, you can move it outside of the hook like:
function SomeComponent {
const [token] = useCookies(['my-token']);
const promise = API.getItems(token['my-token']);
const [data, loading, error] = useFetch(promise);
}
That second approach would totally abstract away the details about how the request is constructed.
Related
I am new to React JS, coming from iOS and so trying to implement MVVM. Not sure it's the right choice, but that's not my question. I would like to understand how react works and what am I doing wrong. So here's my code:
const ViewContractViewModel = () => {
console.log('creating view model');
const [value, setValue] = useState<string | null>(null);
const [isLoading, setisLoading] = useState(false);
async function componentDidMount() {
console.log('componentDidMount');
setisLoading(true);
// assume fetchValueFromServer works properly
setValue(await fetchValueFromServer());
console.log('value fetched from server');
setisLoading(false);
}
return { componentDidMount, value, isLoading };
};
export default function ViewContract() {
const { componentDidMount, value, isLoading } = ViewContractViewModel();
useEffect(() => {
componentDidMount();
}, [componentDidMount]);
return (
<div className='App-header'>
{isLoading ? 'Loading' : value ? value : 'View Contract'}
</div>
);
}
So here's what I understand happens here: the component is mounted, so I call componentDidMount on the view model, which invokes setIsLoading(true), which causes a re-render of the component, which leads to the view model to be re-initialised and we call componentDidMount and there's the loop.
How can I avoid this loop? What is the proper way of creating a view model? How can I have code executed once after the component was presented?
EDIT: to make my question more general, the way I implemented MVVM here means that any declaration of useState in the view model will trigger a loop every time we call the setXXX function, as the component will be re-rendered, the view model recreated and the useState re-declared.
Any example of how to do it right?
Thanks a lot!
A common pattern for in React is to use{NameOfController} and have it completely self-contained. This way, you don't have to manually call componentDidMount and, instead, you can just handle the common UI states of "loading", "error", and "success" within your view.
Using your example above, you can write a reusable controller hook like so:
import { useEffect, useState } from "react";
import api from "../api";
export default function useViewContractViewModel() {
const [data, setData] = useState("");
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
(async () => {
try {
const data = await api.fetchValueFromServer();
setData(data);
} catch (error: any) {
setError(error.toString());
} finally {
setLoading(false);
}
})();
}, []);
return { data, error, isLoading };
}
Then use it within your view component:
import useViewContractViewModel from "./hooks/useViewContractViewModel";
import "./styles.css";
export default function App() {
const { data, error, isLoading } = useViewContractViewModel();
return (
<div className="App">
{isLoading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error}</p>
) : (
<p>{data || "View Contract"}</p>
)}
</div>
);
}
Here's a demo:
On a side note, if you want your controller hook to be more dynamic and can control the initial data set, then you can pass it props, which would then be added to the useEffect dependency array:
import { useEffect, useState } from "react";
import api from "../api";
export default function useViewContractViewModel(id?: number) {
const [data, setData] = useState("");
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
(async () => {
try {
const data = await api.fetchValueFromServer(id);
setData(data);
} catch (error: any) {
setError(error.toString());
} finally {
setLoading(false);
}
})();
}, [id]);
return { data, error, isLoading };
}
Or, you return a reusable callback function that allows you to refetch data within your view component:
import { useCallback, useEffect, useState } from "react";
import api from "../api";
export default function useViewContractViewModel() {
const [data, setData] = useState("");
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState("");
const fetchDataById = useCallback(async (id?: number) => {
setLoading(true);
setData("");
setError("");
try {
const data = await api.fetchValueFromServer(id);
setData(data);
} catch (error: any) {
setError(error.toString());
} finally {
setLoading(false);
}
}, [])
useEffect(() => {
fetchDataById()
}, [fetchDataById]);
return { data, error, fetchDataById, isLoading };
}
I created a custom hook to make my api calls with axios.
When I call this hook passing it different parameters, it returns 3 states.
I created a page with a form.
When I submit this form I call a function "onSubmitform"
I would like to be able to execute this custom hook in this function.
How can I do ?
Maybe a custom hook is not suitable in this case?
-- file useAxios.js --
import { useState, useEffect } from "react";
import axios from "axios";
const useAxios = (axiosParams) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
handleData(axiosParams);
}, []);
return { response, error, loading };
};
export default useAxios;
-- file Page.js --
import useAxios from "../hooks/useAxios";
function Page() {
const { response } = useAxios();
const onSubmitForm = () => {
// Here I want to call the custom hook by passing it different parameters.
}
}
You can add an option to execute the request manually and avoid the fetch on mount:
const useAxios = (axiosParams, executeOnMount = true) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (executeOnMount) handleData(axiosParams);
}, []);
return { response, error, loading, execute: handleData };
};
Then use it:
const { response, execute } = useAxios(undefined, false);
const onSubmitForm = (data) => {
execute(params) // handleData params
}
A hook is a function (returning something or not) which should be called only when the components (re)renders.
Here you want to use it inside a callback responding to an event, which is not the same thing as the component's render.
Maybe you are just looking for a separate, "simple", function? (for example something similar to what you have in your "useEffect")
I want to set a loader to my button while I call createUserWithEmailAndPassword using email & password. How can I do that?
For example in Apollo Client GraphQL, there is a loading state provided out of the box like:
const {data, loading, error} = <API call>
The given loading state automatically sets to true if the data fetching is in the works.
Is there any similar such way we can do it in firebase?
There isn't a built in function that's tied in with react state, so you would create the states yourself:
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Whatever function is going to do the loading:
const onClick = async () => {
try {
setLoading(true);
const userCredential = await createUserWithEmailAndPassword(email, password);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
}
I'm using a custom hook to get pull some data in from an API for use across a set of React function components. However, esLint throws up a lovely warning:
React Hook useEffect has a missing dependency: 'fetchFromAPI'. Either
include it or remove the dependency array.
I didn't think it's a dependency, as it's inside useFetch() itself. I need to do it as I'm using await. What am I doing wrong? Is it ok to just turn off the warning for this line? Or is there a more canonical syntax I should be using?
function useFetch (url) {
const [data, setData] = useState(null);
async function fetchFromAPI() {
const json = await( await fetch(url) ).json();
setData(json);
}
useEffect(() => {fetchFromAPI()},[url]);
return data;
};
export {
useFetch
};
I suggest you to define async function inside useEffect itself:
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchFromAPI() {
const json = await( await fetch(url) ).json();
setData(json);
}
fetchFromAPI()
},[url]);
return data;
};
You can take a look at valid example from doc faqs which uses async function inside useEffect itself:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// By moving this function inside the effect,
// we can clearly see the values it uses.
async function fetchProduct() {
const response = await fetch('http://myapi/product' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ Valid because our effect only uses productId
// ...
}
Declare it outside your custom effect passing url as parameter and return the json to be setted inside useEffect
async function fetchFromAPI(url) {
const json = await( await fetch(url) ).json();
return json
}
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
setData(fetchFromAPI(url))
},[url]);
return data;
};
Or directly inside useEffect
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchFromAPI() {
const json = await( await fetch(url) ).json();
return json
}
setData(fetchFromAPI())
},[url]);
return data;
};
Just move your function inside the useEffect, and everything will be fine:
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchFromAPI() {
const json = await (await fetch(url)).json();
setData(json);
}
fetchFromAPI();
}, [url]);
return data;
}
export { useFetch };
https://codesandbox.io/s/clever-cdn-8g0me
async function fetchFromAPI(url) {
return ( await fetch(url) ).json();
}
function useFetch (url) {
const [data, setData] = useState(null);
useEffect(() => {
fetchFromAPI(url).then(setData);
}, [url, setData, fetchFromAPI]);
return data;
};
export {
useFetch
};
You can change a little and then extract fetchFromAPI, for not being created every time useFetch calls, also it's good for single responsibility.
If you understand this code very well of course you can either turn off linting for this current line or you can add the rest setData and fetchFromAPI params. And exactly in this order. Because on re-render params comparison start from first param to last, and it's better place most changed param in first place, for not checking not changed param every time the next one is changed, so if first changed, useEffect don't need to check the others and call passed function earlier
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.