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 };
}
Related
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")
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>
);
}
https://reactjs.org/docs/hooks-custom.html
Building your own Hooks lets you extract component logic into reusable functions.
Thats what I want to do: extract my component logic into reusable functions for other components.
My functional component:
//React
import React from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';
//Local
import UserPreview from './UserPreview';
import defaultContainer from '../../shared/styles/defaultContainer';
import useFetchUsers from './../../../handler/useFetchUsers';
export default function UserList(props) {
const { users } = props;
const dispatch = useDispatch();
//State
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState(null);
return (
<View style={defaultContainer}>
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <UserPreview user={item} />}
ListEmptyComponent={() => <Text style={styles.listEmpty}>Keine Benutzer gefunden!</Text>}
ItemSeparatorComponent={() => <View style={styles.listSeperator} />}
onRefresh={useFetchUsers}
refreshing={isLoading}
contentContainerStyle={styles.container}
/>
</View>
);
}
my reusable function:
import React from 'react';
import * as userActions from '../store/actions/user';
import { useDispatch } from 'react-redux';
export default async function useFetchUsers() {
const [error, setError] = React.useState(null);
const dispatch = useDispatch();
const [isLoading, setIsLoading] = React.useState(false);
console.log('StartupScreen: User laden');
setIsLoading(true);
setError(null);
try {
await dispatch(userActions.fetchUsers());
console.log('StartupScreen: User erfolgreich geladen');
} catch (err) {
setError(err.message);
}
setIsLoading(false);
}
How should I use my function in the onRefresh prop in my Userlist?
I'm getting this error: Invalid hook call
You are using useFetchUsers as a callback. Rules of Hooks forbid this.
useFetchUsers should return some function that can be used as callback:
export default function useFetchUsers() {
const [error, setError] = React.useState(null);
const dispatch = useDispatch();
const [isLoading, setIsLoading] = React.useState(false);
return async function() {
console.log('StartupScreen: User laden');
setIsLoading(true);
setError(null);
try {
await dispatch(userActions.fetchUsers());
console.log('StartupScreen: User erfolgreich geladen');
} catch (err) {
setError(err.message);
}
setIsLoading(false);
}
}
function UserList(props) {
...
const handleRefresh = useFetchUsers();
...
return <FlatList onRefresh={handleRefresh} />;
}
React hooks can't be an async function. So according to this redux workflow:
You have to dispatch fetch user's action and then your loading and error states should be in your reducer and if you have any side effect manager such as redux-saga alongside your redux, you have to call all HTTP methods right there and your components just should dispatch and present the results. The other way is to call and fetch users into your hook and put them into your redux store via an action which you dispatch.
In this way, loading and error can be in your hook(in your local hook state, not into the redux-store).
So let's try this code(I've implemented the second way):
import React from 'react';
import * as userActions from '../store/actions/user';
import { useDispatch } from 'react-redux';
export default function useFetchUsers() {
const [error, setError] = React.useState(null);
const dispatch = useDispatch();
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
(async () => {
console.log('StartupScreen: User laden');
setIsLoading(true);
setError(null);
try {
const res = await fetchUsers();
dispatch(setUsers(res.data));
console.log('StartupScreen: User erfolgreich geladen');
setIsLoading(false);
} catch (err) {
setIsLoading(false);
setError(err.message);
}
})()
}, [])
}
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.
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);