I have a react component that uses several states which are initialized in the same way useState(false), is there a way to combine all these states into a single useState(false)
const [loading, setLoading] = useState(false);
const [fields, setFields] = useState(false);
const [wrongImageType, setWrongImageType] = useState(false);
const [aboutError, setAboutError] = useState(false);
const [destinationError, setDestinationError] = useState(false)
You can do like this
const [states, setStates] = useState({ loading:false, fields:false, wrongImageType:false, aboutError:false, destinationError:false })
Update state like this
setStates((prevState) => ({ ...prevState, loading: true }))
I usually use the useReducer hook. That way, I can just slam properties to some state object and the reducer tidies it all up for me. It also makes it easy when I have to submit an object to an API, I just fling the entire state object to the server. So it's basically, "fetch data from the server, use form inputs to let the user update the values, fling the data back to the server when they click the save button."
import React, {
useReducer,
useEffect
} from "react";
const initialState = {
loading: false,
fields: false,
wrongImageType: false,
aboutError: false,
destinationError: false
};
const reducer = (prev, cur) => ({ ...prev, ...cur });
const MyComponent = () => {
const [state, setState] = useReducer(reducer, initialState);
useEffect(() => {
setState({
loading: true
});
// fetch something or do something...
setState({
loading: false
});
}, []);
return ({
state.loading ? <> Loading </> : <>{state.something}</>
});
}
Related
On first submit, it just shows the fields without values, while returns values on the second submit hit on console log.
import React, { useState } from "react";
const Movieform = ({ setMovArray }) => {
const [movieName, setMovieName] = useState("");
const [ratings, setRatings] = useState("");
const [duration, setDuration] = useState("");
const movArray = [
{
name: movieName,
rating: ratings,
duration: duration,
},
];
const [movieData, setMovieData] = useState(movArray);
const HandleSubmit = (e) => {
e.preventDefault();
setMovieData(movArray);
console.log(movieData);
};
I'm assuming you initially assign the state as an empty string, and during the first render of the component, the movArray constant takes empty strings into values.
You can do this for example:
const [movieName, setMovieName] = useState("");
const [ratings, setRatings] = useState("");
const [duration, setDuration] = useState("");
const [movieData, setMovieData] = useState([]);
const HandleSubmit = (evt) => {
evt.preventDefault();
// When submitting the form, we collect fresh values
const movieData = [{
name: movieName,
rating: ratings,
duration: duration,
}];
// And add a fresh array to the state
setMovieData(movieData);
};
I also hope that you made a controlled input in the form so that a fresh state is saved for each change
const [state, setState] = useState("");
<input
onChange={evt => setState(evt.target.value)} // fires on each character input and saves the new state
value={state} // always unique state
/>
The code above will help you )
And in addition you can make one state for your form
const [movieData, setMovieData] = useState([]);
// all the object you need is in the state
const [formState, setFormState] = useState({
name: '', rating: '', duration: ''
});
const HandleSubmit = (evt) => {
evt.preventDefault();
setMovieData([formState])
};
// In JSX
<input
name='duration'
onChange={evt => setFormState({ ...formState, duration: evt.target.value })} // overwrite the data by changing the field we need
value={formState.duration}
/>
In react, with useState, I can pass it an updater function to get the current state within the same function. Currently I update a state multiple times within the same function.
Is that possible with a reducer dispatch with useReducer?
Thanks.
const someFunc = () => {
// some other computation
const result = someOtherFunc();
setState((state)=>{
// do something with current state
return {...state, result}
})
const result2 = someOtherFunc2();
setState((state)=>{
// do something with current state
return {...state, result2}
})
}
Yes it is.
You can do this by grouping all your states in like an initialState variable:
import { useReducer, useEffect } from 'react';
const SampleComponent = () => {
const reducer = (state, action) => ({...state, ...action})
const initialState = {
someName: '',
someCounter: 0,
isLoading: false
}
const [{
someName, someCounter, isLoading
}, dispatch ] = useReducer(reducer, initialState )
useEffect(() => {
dispatch({ isLoading: true }
const res = await fetch('https://someapi')
const json = await res.json()
if (json) {
dispatch({
someName: json.someNameValue,
someCounter: someCounter + json.someCounterValue,
isLoading: false
})
}
}, [])
return (
<>
<h1>{someName}</h1>
<span>{someCounter}</span>
</>
)
}
There maybe other ways to do this, but the way works best for me is to:
Declare a reducer: const reducer = (state, action) => ({...state, action})
Declare all states group in an object state: const initialState = { someState: '' }
Use useReducer with the state variables and dispatch encapsulated with it: const [{ someState }, dispatch ] = useReducer(reducer, initialState)
Use dispatch() method to set new values of states: dispatch({ someState: newValue }) where newValue is any value you want to assign to someState.
Yes it is possible. Whatever that can be done with useState can also be done by useReducer. It is the context that makes us decide on which to use.
Suppose I just want to update my loading state, then useState is enough
const [isLoading, setIsLoading] = useState(true);
But let's suppose I have a input form,
if I decide to useState, then it will be kind of like this
const [name, setName] = useState('')
const [age, setAge] = useState(0);
const [address, setAddress] = useState('');
instead of using all these useState, I could directly using useReducer for this specific application
const initialFormValues = {
name: '',
age: 0,
address: '',
};
const reducer = (state, action) =>{
switch(action.type) {
case "NAME": return { ...state, name: action.value};
case "AGE": return { ...state, age: action.value};
case "ADDRESS": return { ...state, address: action.value};
}
}
const [formValues, dispatch] = useReducer(reducer, initialFormValues);
Now whenever you want to update your input fields in the form, just use dispatch.
Both can do the task. Depending upon the use-case, we can choose the more appropriate one.
I'm new to TypeScript and even React Hooks. I'm trying to fetch data continuously from a custom hook called useFetch(), which takes in a string (the URL) parameter:
import { useEffect, useState } from "react";
export interface Payload {
data: null | string,
loading: boolean
}
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
useEffect(() => {
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
}, [url, setState])
return state;
}
export default useFetch;
I import that hook into App():
import React, { useState, useEffect } from "react";
import useFetch from './utils/useFetch'
import {Payload} from './utils/useFetch';
const App = () => {
const [quote, setquote] = useState<Payload>({data: null, loading: true})
const handleClick = () => setquote(data)
const data = useFetch(
"/api/rand"
);
return (
<div>
<h1>Quotes</h1>
<button onClick={handleClick}>Get Quote</button>
<div>{quote.data}</div>
</div>
)
}
export default App;
On the first time the app loads, it works. I get data (a quote) when I click the button.
However, when I click it multiple times, the API isn't called again, and new data doesn't come through. I believe I'm supposed to use the useEffect() react hook and maintain the state (maybe I'm mistaken) but all my attempts so far have been to no avail. Any help would be much appreciated. If I figure it out, I'll definitely answer this question. Thanks!
It seems like you are mixing two ideas a little bit here.
Do you want to fetch data continuously? (Continuously meaning all the time, as in, periodically, at an interval)
Or do you want to fetch data upon mount of the component (currently happens) AND when the user taps the "Get Quote" button (not working currently)?
I will try to help you with both.
Continuously / periodically
Try changing the setup a bit. Inside the useFetch do something like:
import { useEffect, useState } from "react";
export interface Payload {
data: null | string,
loading: boolean
}
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
useEffect(() => {
const interval = setInterval(() => {
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
}, 2000); // Something like 2s
return () => {
clearInterval(interval); // clear the interval when component unmounts
}
}, [url])
return state;
}
export default useFetch;
This will fetch the endpoint every 2s and update the state variable, this will then be reflected in the App (or any place that uses the hook for that matter).
Therefore you do not need the quote state anymore in the App. Also, you don't need a button in the UI anymore.
App will looks something like:
import React, { useState, useEffect } from "react";
import useFetch from './utils/useFetch'
import {Payload} from './utils/useFetch';
const App = () => {
const quote = useFetch(
"/api/rand"
);
return (
<div>
<h1>Quotes</h1>
<div>{quote.data}</div>
</div>
)
}
export default App;
Fetch data upon mount of the component (currently happens) AND when the user taps the "Get Quote" button
Then the useFetch hook isn't really needed/suited in my opinion. A hook can be used for this application, but transforming it into a more simple function would make more sense to me. I suggest omitting the useFetch hook. The App component would look something like:
import React, { useState, useEffect, useCallback } from "react";
const App = () => {
const [quote, setQuote] = useState<Payload>({data: null, loading: true})
const handleGetQuote = useCallback(() => {
setQuote(state => ({ data: state.data, loading: true }));
fetch("/api/rand")
.then(x => x.text())
.then(y => {
setQuote({ data: y, loading: false });
});
}, []);
useEffect(() => {
handleGetQuote();
}, [handleGetQuote]);
return (
<div>
<h1>Quotes</h1>
<button onClick={handleGetQuote}>Get Quote</button>
<div>{quote.data}</div>
</div>
)
}
export default App;
The problem here is, that you are not refetching the quotes.
const useFetch = (url:string) => {
const [loading, setLoading] = useState(false);
const [quote, setQuote] = useState('');
const refetch = useCallback(() => {
setLoading(true);
fetch(url)
.then((x) => x.text())
.then((y) => {
//setQuote(y);
setQuote(String(Math.random()));
setLoading(false);
});
}, [url]);
return { quote, refetch, loading };
};
const App = () => {
const { quote, refetch, loading } = useFetch('/api/rand');
useEffect(() => {
refetch();
}, []);
return (
<div>
<h1>Quotes</h1>
<button onClick={refetch}>Get Quote</button>
{loading ? ' loading...' : ''}
<div>{quote}</div>
</div>
);
};
See this example
https://stackblitz.com/edit/react-ts-kdyptk?file=index.tsx
Here's another possibility: I added a reload function to the return values of useFetch. Also, since the useFetch hook already contains its own state, I removed the useState from App
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
const [reloadFlag, setReloadFlag] = useState(0)
useEffect(() => {
if(reloadFlag !== 0){ // remove this if you want the hook to fetch data initially, not just after reload has been clicked
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
}
}, [url, setState, reloadFlag])
return [state, ()=>setReloadFlag((curFlag)=>curFlag + 1)];
}
const App = () => {
const [data, reload] = useFetch(
"/api/rand"
);
return (
<div>
<h1>Quotes</h1>
<button onClick={reload}>Get Quote</button>
<div>{quote.data}</div>
</div>
)
}
Just change the code as below:
const useFetch = (url: string) => {
const [state, setState] = useState<Payload>({data: null, loading: true})
setState(state => ({ data: state.data, loading: true }));
fetch(url)
.then(x => x.text())
.then(y => {
setState({ data: y, loading: false });
});
return state;
}
export default useFetch;
How can I use LocalStorage in a functional component like this
I know how do this in a class component but can I solve this problem in this case?
ERROR: TypeError: repositories is not a function
export default function Main() {
const [newRepo, setNewRepo] = useState('');
const [repositories, setRepositories] = useState([]);
const [clearInput] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
repositories(localStorage.getItem('repositories'));
if (repositories) {
setRepositories(JSON.parse(repositories));
}
}, [repositories]);
useEffect((_, prevState) => {
if (prevState.repositories !== repositories) {
localStorage.setItem('repositories', JSON.stringify(repositories));
}
});
In your first useEffect, the repositories is your state which an array. Not a function.
Also, in your second useEffect you need to make correction to the way you access the prevState in hooks.
Fix for 1st useEffect
export default function Main() {
const [newRepo, setNewRepo] = useState('');
const [repositories, setRepositories] = useState([]);
const [clearInput] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const localRepoItems = localStorage.getItem('repositories');
if (localRepoItems) {
setRepositories(JSON.parse(localRepoItems));
}
}, []); // do not give the dependency as repositories as it will go to infinite loop
});
To obtain previous state in hooks, you can write a little custom hook:
Like this:
export const usePrevious = value => {
const ref = React.useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Usage in your component:
const prevRepositories = usePrevious(repositories);
useEffect(() => {
if (prevRepositories.length !== repositories.length) {
localStorage.setItem('repositories', JSON.stringify(repositories));
}
}, [repositories]);
I'm trying React hooks for the first time and all seemed good until I realised that when I get data and update two different state variables (data and loading flag), my component (a data table) is rendered twice, even though both calls to the state updater are happening in the same function. Here is my api function which is returning both variables to my component.
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
In a normal class component you'd make a single call to update the state which can be a complex object but the "hooks way" seems to be to split the state into smaller units, a side effect of which seems to be multiple re-renders when they are updated separately. Any ideas how to mitigate this?
You could combine the loading state and data state into one state object and then you could do one setState call and there will only be one render.
Note: Unlike the setState in class components, the setState returned from useState doesn't merge objects with existing state, it replaces the object entirely. If you want to do a merge, you would need to read the previous state and merge it with the new values yourself. Refer to the docs.
I wouldn't worry too much about calling renders excessively until you have determined you have a performance problem. Rendering (in the React context) and committing the virtual DOM updates to the real DOM are different matters. The rendering here is referring to generating virtual DOMs, and not about updating the browser DOM. React may batch the setState calls and update the browser DOM with the final new state.
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Alternative - write your own state merger hook
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
This also has another solution using useReducer! first we define our new setState.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
after that we can simply use it like the good old classes this.setState, only without the this!
setState({loading: false, data: test.data.results})
As you may noticed in our new setState (just like as what we previously had with this.setState), we don't need to update all the states together! for example I can change one of our states like this (and it doesn't alter other states!):
setState({loading: false})
Awesome, Ha?!
So let's put all the pieces together:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript Support.
Thanks to P. Galbraith who replied this solution,
Those using typescript can use this:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
where MyState is the type of your state object.
e.g. In our case it'll be like this:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
Previous State Support.
In comments user2420374 asked for a way to have access to the prevState inside our setState, so here's a way to achieve this goal:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction checks whether the passed argument is a function (which means you're trying to access the prevState) or a plain object. You can find this implementation of isFunction by Alex Grande here.
Notice. For those who want to use this answer a lot, I decided to turn it into a library. You can find it here:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Batching update in react-hooks https://github.com/facebook/react/issues/14259
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.
This will do:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
To replicate this.setState merge behavior from class components,
React docs recommend to use the functional form of useState with object spread - no need for useReducer:
setState(prevState => {
return {...prevState, loading, data};
});
The two states are now consolidated into one, which will save you a render cycle.
There is another advantage with one state object: loading and data are dependent states. Invalid state changes get more apparent, when state is put together:
setState({ loading: true, data }); // ups... loading, but we already set data
You can even better ensure consistent states by 1.) making the status - loading, success, error, etc. - explicit in your state and 2.) using useReducer to encapsulate state logic in a reducer:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
PS: Make sure to prefix custom Hooks with use (useData instead of getData). Also passed callback to useEffect cannot be async.
If you are using third-party hooks and can't merge the state into one object or use useReducer, then the solution is to use :
ReactDOM.unstable_batchedUpdates(() => { ... })
Recommended by Dan Abramov here
See this example
A little addition to answer https://stackoverflow.com/a/53575023/121143
Cool! For those who are planning to use this hook, it could be written in a bit robust way to work with function as argument, such as this:
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newState =>
typeof newState == "function"
? setState(prevState => ({ ...prevState, ...newState(prevState) }))
: setState(prevState => ({ ...prevState, ...newState }));
return [state, setMergedState];
};
Update: optimized version, state won't be modified when incoming partial state was not changed.
const shallowPartialCompare = (obj, partialObj) =>
Object.keys(partialObj).every(
key =>
obj.hasOwnProperty(key) &&
obj[key] === partialObj[key]
);
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newIncomingState =>
setState(prevState => {
const newState =
typeof newIncomingState == "function"
? newIncomingState(prevState)
: newIncomingState;
return shallowPartialCompare(prevState, newState)
? prevState
: { ...prevState, ...newState };
});
return [state, setMergedState];
};
In addition to Yangshun Tay's answer you'll better to memoize setMergedState function, so it will return the same reference each render instead of new function. This can be crucial if TypeScript linter forces you to pass setMergedState as a dependency in useCallback or useEffect in parent component.
import {useCallback, useState} from "react";
export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
const [state, setState] = useState(initialState);
const setMergedState = useCallback((newState: Partial<T>) =>
setState(prevState => ({
...prevState,
...newState
})), [setState]);
return [state, setMergedState];
};
You can also use useEffect to detect a state change, and update other state values accordingly