I am new in react. can anyone explain why the loading is not updating its value. on console the loading is 1
import React, { useState, useEffect } from "react";
function App() {
useEffect(() => {
hai();
}, []);
const [loading, setLoading] = useState(1);
const hai = () => {
console.log("............");
setLoading(2);
console.log(loading);
};
return <></>;
}
export default App;
Also if there are two state variables, and rearrange the set function, whole application breaks
const [loading, setLoading]=useState(false)
const [datas, setDatas]=useState([])
//works fine if loading is set second
const hai = () => {
setDatas(res) //fetched from external api
setLoading(true)
};
//throws error in applicaton
const hai = () => {
setLoading(true)
setDatas(res) //fetched from external api
};
console.log(datas)
You are testing your loading value in a wrong way,this is how you should be doing it:
useEffect(() => {
console.log(loading);
}, [loading]);
and remove console.log(loading) from the function hai
Whenever you want to access an updated value of some variable then put it inside an useEffect and put the value which you want to check inside the useEffect's dependency array.
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 };
}
What I'm trying to do is fetch a single random quote from a random quote API every 5 seconds, and set it's contents to a React component.
I was able to fetch the request successfully and display it's contents, however after running setInterval method with the fetching method fetchQuote, and a 5 seconds interval, the contents are updated multiple times in that interval.
import { Badge, Box, Text, VStack, Container} from '#chakra-ui/react';
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const RandomQuotes = () => {
const [quote, setQuote] = useState<Quote>(quoteObject);
const [error, setError]: [string, (error: string) => void] = React.useState("");
const [loading, setLoading] = useState(true);
const fetchQuote = () => {
axios.get<Quote>(randomQuoteURL)
.then(response => {
setLoading(false);
setQuote(response.data);
})
.catch(ex => {
setError(ex);
console.log(ex)
});
}
setInterval(() => setLoading(true), 5000);
useEffect(fetchQuote, [loading, error]);
const { id, content, author } = quote;
return (
<>
<RandomQuote
quoteID={id}
quoteContent={content}
quoteAuthor={author}
/>
</>
);
}
When any state or prop value gets updated, your function body will re-run, which is called a re-render.
And you've put setInterval call in the main function(!!!), so each time the component re-renders, it will create another interval again and again. Your browser will get stuck after a few minutes.
You need this interval definition once, which is what useEffect with an empty second parameter is for.
Also, using loading flag as a trigger for an API call works, but semantically makes no sense, plus the watcher is expensive and not needed.
Here's a rough correct example:
useEffect(() => {
const myInterval = setInterval(fetchQuote, 5000);
return () => {
// should clear the interval when the component unmounts
clearInterval(myInterval);
};
}, []);
const fetchQuote = () => {
setLoading(true);
// your current code
};
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
I have the following case:
export default function Names() {
const dispatch = useDispatch();
const [names, setNames] = useState([]);
const stateNames = useSelector(state => state.names);
const fetchNames = async () => {
try {
const response = await nameService.getNames();
dispatch(initNames(response.body));
setNames(response.body);
} catch (error) {
console.error('Fetch Names: ', error);
}
};
useEffect(() => {
fetchNames();
}, []);
return (
{ names.map((name, index) => (
<Tab label={ budget.label} key={index}/>
)) }
);
}
When my component is rendered in the browser console I get a warning: "React Hook useEffect has a missing dependency: 'fetchBudgets'. Either include it or remove the dependency array react-hooks / exhaustive-deps".
If I comment the line in which I write the names in Redux state, the warning does not appear.
I need the list of names in the state so that I can update the list when a new name is written to the list from the outside.
export default function AddNameComponent() {
const dispatch = useDispatch();
const [label, setLabel] = useState('');
const [description, setDescription] = useState('');
const onLabelChange = (event) => { setLabel(event.target.value); };
const onDescriptionChange = (event) => { setDescription(event.target.value); };
const handleSubmit = async (event) => {
try {
event.preventDefault();
const newName = {
label: label
description: description
};
const answer = await budgetService.postNewName(newName);
dispatch(add(answer.body)); // Adding new Name in to Redux state.names
} catch (error) {
setErrorMessage(error.message);
console.error('Create Name: ', error);
}
};
return (
<div>
// Create name form
</div>
)
}
This is how everything works, but I don't understand why I have a warning.
I tried to add a flag to the array with dependencies of usеЕffect.
I tried to pass the function 'fetchNames' through the parent component - in props and to add it as a dependency, but it is executed twice ...
Can you advise please!
It's just an eslint warning so you don't have to fix it. But basically any variables which are used in the useEffect function are expected to be included in the dependency array. Otherwise, the effect will never be re-run even if the function fetchBudgets were to change.
It is expecting your hook to look like
useEffect(() => {
fetchBudgets();
}, [fetchBudgets]);
Where the effect will run once when the component is mounted and run again any time that the fetchBudgets function changes (which is probably never).
If it's executing more than once, that means that fetchBudgets has changed and you should try to figure our where and why it has been redefined. Maybe it needs to be memoized?
Here are the docs on putting functions in the dependency array.
Thanks for your attention! I tried many options and finally found one solution.
useEffect(() => {
async function fetchNames() {
const response = await nameService.getNames();
dispatch(init(response.body));
setNames(response.body);
}
fetchNames();
}, [dispatch, props]);
I put 'props' in an array of dependencies for one useEffect execution.
So I have a Route that loads a dashboard component, and a sidebar with different links to this dashboard component I'm attempting to use useEffect to load the appropriate data from the backend when the component is loaded
const Dashboard = ({match}) => {
const dispatch = useDispatch()
const [loading, setLoading] = useState(true)
const thing = useSelector(state => state.things)[match.params.id]
const fetchData = async () => {
setLoading(true)
await dispatch(loadStuff(match.params.id))
setLoading(false)
}
useEffect(() => {
fetchData()
}, [match]);
return loading
? <div>Loading</div>
: <div>{thing.name}</div>
}
This works well enough for the first load. However when I click the NavLink on the sidebar to change { match }, thing.name blows up. I would expect, since match is a dependency on useEffect, that it would restart the load cycle and everything would pause until the load is complete, instead it appears to try to render immediately and the API call is not made at all. If I remove thing.name, I see the api call is made and everything works.
I keep running into this, so I appear to have a fundamental misunderstanding of how to predictably load data with redux when a component is mounted. What am I doing wrong here?
Have you wrapped your component with withRouter of react router dom?
import React, { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
const Dashboard = ({ match }) => {
const dispatch = useDispatch()
const [loading, setLoading] = useState(true)
const thing = useSelector(state => state.things)[match.params.id]
const fetchData = async () => {
setLoading(true)
await dispatch(loadStuff(match.params.id))
setLoading(false)
}
useEffect(() => {
fetchData()
}, [match.params.id]);
return loading
? <div>Loading</div>
: <div>{thing.name}</div>
}
export default withRouter(Dashboard);
Use match.params.id in the useEffect as comparison of object(match) should not be done.
{} === {} /* will always be false and useEffect will be called every time
irrespective of match.params.id change. */