How to handle async firebase calls with hooks - reactjs

I'd like to create a hook for adding data to a firestore database. I'm not sure if I'm misunderstanding how hooks work, or firestore works, I'm new to both.
Warning:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Firebase API
createTeam = newTeam => {
return this.db.collection("teams").add({
...newTeam
});
};
The hook
export default function useFetch(action) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
async function performAction(body) {
try {
setLoading(true);
setData(null);
setError(null);
const data = await action(body);
setData(data);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
return [{ loading, data, error }, performAction];
}
Component
const [state, runFetch] = useFetch(db.createTeam);
const { values, handleChange, isDirty, handleSubmit } = useForm({
initialValues: {
name: "",
location: ""
},
onSubmit({ values }) {
runFetch(values);
},
validate(e) {
return e;
}
});
The state.data is never set to the expected response, however, the logging after await in the fetch hook shows that I am receiving the response. Should I be doing this in useEffect? What is the appropriate way to accomplish this task via hooks/firebase?

See if that works for you.
That's a great idea for a custom hook.
Working example on CodeSandbox:
https://codesandbox.io/s/clever-joliot-ukr1t
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [state, runFetch] = useFetch(mockAPICall);
function mockAPICall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Some data from DB!");
}, 1000);
});
}
return (
<React.Fragment>
<div>Loading: {state.loading ? "True" : "False"}</div>
<div>Data: {state.data}</div>
<button onClick={() => runFetch(mockAPICall)}>Get Data</button>
</React.Fragment>
);
}
function useFetch(action) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
async function performAction(body) {
try {
setLoading(true);
setData(null);
setError(null);
const data = await action(body);
setData(data);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
return [{ loading, data, error }, performAction];
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Related

React JS: My view model is causing an endless loop of rendering

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 };
}

Calling a custom hook from an onClick by passing it arguments

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

useEffect dependency causes infinite loop

I created a custom hook which I use in App.js
The custom hook (relevant function is fetchTasks):
export default function useFetch() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [tasks, setTasks] = useState([]);
const fetchTasks = async (url) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("falied!");
}
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) {
loadedTasks.push({ id: taskKey, text: data[taskKey].text });
}
setTasks(loadedTasks);
} catch (err) {
console.log(err.message);
}
setLoading(false);
};
return {
loading,
setLoading,
error,
setError,
fetchTasks,
tasks,
};
}
Then in my App.js:
function App() {
const { loading, setLoading, error, setError, fetchTasks, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasks(
"https://.....firebaseio.com/tasks.json"
);
}, []);
My IDE suggests adding the fetchTasks function as a dependency to useEffect. But once I add it, an infinite loop is created. If I omit it from the dependencies as shown in my code, it will work as expected, but I know this is a bad practice. What should I do then?
Because that every time you call useFetch(). fetchTasks function will be re-created. That cause the reference to change at every render then useEffect() will detected that dependency fetchTasks is re-created and execute it again, and make the infinite loop.
So you can leverage useCallback() to memoize your fetchTasks() function so the reference will remains unchanged.
import { useCallback } from 'react'
export default function useFetch() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [tasks, setTasks] = useState([]);
const fetchTasks = useCallback(
async (url) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("falied!");
}
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) {
loadedTasks.push({ id: taskKey, text: data[taskKey].text });
}
setTasks(loadedTasks);
} catch (err) {
console.log(err.message);
}
setLoading(false);
};,[])
return {
loading,
setLoading,
error,
setError,
fetchTasks,
tasks,
};
}
function App() {
const { loading, setLoading, error, setError, fetchTasks, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasks(
"https://.....firebaseio.com/tasks.json"
);
}, [fetchTasks]);
instead of return fetchTasks function return this useCallback fetchTasksCallback function from useFetch hook which created only one instance of fetchTasksCallback.
const fetchTasksCallback = useCallback(
(url) => {
fetchTasks(url);
},
[],
);
function App() {
const { loading, setLoading, error, setError, fetchTasksCallback, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasksCallback(
"https://.....firebaseio.com/tasks.json"
);
}, [fetchTasksCallback]);
the problem is this fetchTasks every time create a new instance that way dependency list feels that there is a change and repeats the useEffect code block which causes the infinite loop problem

UseFetch returns Can't perform a React state update on an unmounted component

When I'm using this useFetch hook my application shows a blank screen and returns:
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
I don't understand why it goes wrong.
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
(async function () {
try {
setLoading(true);
const response = await fetch(url, {
method: "GET"
});
const data = await response.json();
setData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
})();
}, [url]);
return { data, error, loading };
}
export const useFetchCarById = (testId) =>
useFetch(
`https://localhost/cars/${testId}`
);
export default function Foo() {
const { cars, car } = useContext(Context);
const { data, error, loading } = useFetchCarById(car);
return (
<div>
{data && data.map((x) => <p>{x.startTime}</p>)}
</div>
);
}

Multiple fetch data axios with React Hooks

I would like to get global information from Github user and his repos(and get pinned repos will be awesome). I try to make it with async await but It's is correct? I've got 4 times reRender (4 times console log). It is possible to wait all component to reRender when all data is fetched?
function App() {
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(`https://api.github.com/users/${username}`);
const respRepos = await axios(`https://api.github.com/users/${username}/repos`);
setData(respGlobal.data);
setRepos(respRepos.data);
};
fetchData()
}, []);
if (data) {
console.log(data, repos);
}
return (<h1>Hello</h1>)
}
Multiple state updates are batched but but only if it occurs from within event handlers synchronously and not setTimeouts or async-await wrapped methods.
This behavior is similar to classes and since in your case its performing two state update cycles due to two state update calls happening
So Initially you have an initial render and then you have two state updates which is why component renders three times.
Since the two states in your case are related, you can create an object and update them together like this:
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
Working demo
Figured I'd take a stab at it because the above answer is nice, however, I like cleanliness.
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const Test = () => {
const [data, setData] = useState([])
useEffect(() => {
(async () => {
const data1 = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
const data2 = await axios.get('https://jsonplaceholder.typicode.com/todos/2')
setData({data1, data2})
})()
}, [])
return JSON.stringify(data)
}
export default Test
Using a self invoking function takes out the extra step of calling the function in useEffect which can sometimes throw Promise errors in IDEs like WebStorm and PHPStorm.
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
he made some mistake here:
setGitData({ data: respGlobal.data, repos: respGlobal.data(respRepos.data //it should be respRepos.data});
For other researchers (Live demo):
import React, { useEffect, useState } from "react";
import { CPromise, CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
function MyComponent(props) {
const [error, setError] = useState("");
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
console.log("mount");
const promise = CPromise.from(function* () {
try {
console.log("fetch");
const [respGlobal, respRepos] = [
yield cpAxios(`https://api.github.com/users/${props.username}`),
yield cpAxios(`https://api.github.com/users/${props.username}/repos`)
];
setData(respGlobal.data);
setRepos(respRepos.data);
} catch (err) {
console.warn(err);
CanceledError.rethrow(err); //passthrough
// handle other errors than CanceledError
setError(err + "");
}
}, []);
return () => {
console.log("unmount");
promise.cancel();
};
}, [props.username]);
return (
<div>
{error ? (
<span>{error}</span>
) : (
<ul>
<li>{JSON.stringify(data)}</li>
<li>{JSON.stringify(repos)}</li>
</ul>
)}
</div>
);
}

Resources