I need to change the useState without rendering the page.
First is it possible?
const UsersComponent = ({valueProp}) => {
const [users, setUsers] = useState(valueProp);
const [oldUsers, setoldUsers] = useState(value);
const allUsers = useSelector((state) =>
state.users
);
useEffect(() => {
dispatch(getUsersData());
}, [dispatch]);
useEffect(() => {
// assign users to state oldUsers
}, [dispatch]);
const onClickMergeTwoArrayOfUsers = () => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
console.log("filteredUsers", filteredUsers); // not changed
};
I tried everything nothing helps me.
useEffect(() => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
}, [users]); // RETURN INFINITIVE LOOP
I am also try ->
useEffect(() => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
}, []);
Load only one and that doesn't mean anything to me..
I am try with useRef ,but that doesn't help me in this case.
I will try to explain the basis of the problem.
I need to get one get data. After that get on the click of a button, I need to merge oldUsers and users without rendering, change the state. That is problem.
If there is no solution to this problem, tell me what I could do to solve the problem?
I am googling but without succes ... I am also try this solution from interent ->
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
no work.
I am also try with ->
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
Here is problem because I need to asynchronous get only after that can I looping.
Using a ref is probably a better option for whatever it is you're ultimately trying to do.
Yes, it is possible, but it violates one of the core rules of React state: Do Not Modify State Directly.
React compares state values using Object.is equality, so if you simply mutate an object in state instead of replacing it with a new value that is not object-equal, then the state "update" will not cause a re-render (but this is considered a bug in your program!). Anyway, this is how you'd do it:
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.1/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useCallback, useState} = React;
function Example () {
const [state, setState] = useState([1]);
const logState = useCallback(() => console.log(state.join(', ')), [state]);
// Don't actually do this!!!
const mutateState = () => {
setState(arr => {
arr.push(arr.at(-1) + 1);
return arr;
});
};
return (
<>
<div>{state.join(', ')}</div>
<button onClick={mutateState}>Mutate state</button>
<button onClick={logState}>Log state</button>
</>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
Related
I want to obtain a value from a variable hosted in redux and memorize it, to later compare it with itself and verify if it is different.
I need, for example, to do the following : const value = useSelector(state => state.data.value); (suppose that here the value is 0) now, when value changes, i need to compare it with the value it had previously
If you want to check what the value was on the previous render, you can save it in a ref:
const SomeComponent = () => {
const value = useSelector(state => state.data.value)
const prevRef = useRef();
useEffect(() => {
// Once the render is complete, update the ref's value
prevRef.current = value;
});
// Do something comparing prevRef.current and value
}
If you're doing this a lot you might find it useful to make a custom hook:
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.curren;t
}
// used like:
const SomeComponent = () => {
const value = useSelector(state => state.data.value)
const prev = usePrevious(value);
// Do something comparing prev and value.
}
You have to use selectors(i use 'reselect' library for that), such as:
file: selectors.js
import { createSelector } from 'reselect';
const stateEnvironments = state => state.environments;
export const selectEnvironments = createSelector([stateEnvironments],
environments => environments.data);
so then in your component you can use mapStateToProps with reselect and connect
import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux';
// your component here
const EnvironmentCurrencies = props => {
const {
data,
} = props;
return (
<div>
...
</div.
);
};
const mapStateToProps = createStructuredSelector({
data: selectEnvironments,
});
// here you can update your values with actions
// mapDispatchToProps is especially useful for constraining our actions to the connected component.
// You can access these via `this.props`.
const mapDispatchToProps = dispatch => ({
setEnvironment: environment => dispatch(actions.setEnvironment(environment)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Environments);
this is not a full working example and the version that you might use, can have a bit different synaxis, but I hope this gives you a kick start. If not, let me know, I'll add more extensive example
I am using ResizeObserver to call a function when the screen is resized, but I need to get the updated value of a state within the observer in order to determine some conditions before the function gets invoked.
It's something like this:
let [test, setTest] = React.useState(true)
const callFunction = () => {
console.log('function invoked')
setTest(false) // => set 'test' to 'false', so 'callFunction' can't be invoked again by the observer
}
const observer = React.useRef(
new ResizeObserver(entries => {
console.log(test) // => It always has the initial value (true), so the function is always invoked
if (test === true) {
callFunction()
}
})
)
React.useEffect(() => {
const body = document.getElementsByTagName('BODY')[0]
observer.current.observe(body)
return () => observer.unobserve(body)
}, [])
Don't worry about the details or why I'm doing this, since my application is way more complex than this example.
I only need to know if is there a way to get the updated value within the observer. I've already spent a considerable time trying to figure this out, but I couldn't yet.
Any thoughts?
The problem is, you are defining new observer in each re render of the component, Move it inside useEffect will solve the problem. also you must change this observer.unobserve(body) to this observer..current.unobserve(body).
I have created this codesandbox to show you how to do it properly. this way you don't need external variable and you can use states safely.
import { useEffect, useState, useRef } from "react";
const MyComponent = () => {
const [state, setState] = useState(false);
const observer = useRef(null);
useEffect(() => {
observer.current = new ResizeObserver((entries) => {
console.log(state);
});
const body = document.getElementsByTagName("BODY")[0];
observer.current.observe(body);
return () => observer.current.unobserve(body);
}, []);
return (
<div>
<button onClick={() => setState(true)}>Click Me</button>
<div>{state.toString()}</div>
</div>
);
};
export default MyComponent;
I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);
i am trying to paginate the data from my rest server using CoreUI Table Component.
i have problem getting the updated data from redux store after dispatch request in useEffect, i am using redux thunk, i know that dispatch is async, but is there a way to wait for the dispatch to be completed? i tired making the dispatch a Promise but it did not work.
I successfully get the updated result from action and reducer but in ProductsTable its the previous one, i checked redux devtools extension and i can see the state being changed.
i never get the latest value from store.
Also the dispatch is being called so many times i can see in the console window, it nots an infinite loop, it stops after sometime.
const ProductsTable = (props) => {
const store = useSelector((state) => state.store);
const dispatch = useDispatch();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
useEffect(() => {
setLoading(true);
const payload = {
params: {
page,
},
};
if (page !== 0)
dispatch(getAllProducts(payload));
console.log("runs:" + page)
console.log(store.objects)
if(!(Object.keys(store.objects).length === 0)){
setItems(store.objects.results)
setPages(store.objects.total.totalPages)
setLoading(false)
} else{
console.log("error")
setFetchTrigger(fetchTrigger + 1);
}
}, [page, fetchTrigger]);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
The reason the ProductsTable always has the previous state data is because the effect you use to update the ProductsTable is missing the store as dependency or more specifically store.objects.results; when the page and the fetchTrigger change the effect becomes stale because it isn't aware that when those dependencies change the effect should change.
useEffect(() => {
// store.objects is a dependency that is not tracked
if (!(Object.keys(store.objects).length === 0)) {
// store.objects.results is a dependency that is not tracked
setItems(store.objects.results);
// store.objects.total.totalPages is a dependency that is not tracked
setPages(store.objects.total.totalPages);
setLoading(false);
}
// add these dependencies to the effect so that everything works as expected
// avoid stale closures
}, [page, fetchTrigger, store.objects, store.objects.results, store.objects.total.totalPages]);
The dispatch is being called many times because you have a recursive case where fetchTrigger is a dependency of the effect but you also update it from within the effect. By removing that dependency you'll see much less calls to this effect, namely only when the page changes. I don't know what you need that value for because I dont see it used in the code you've shared, but if you do need it I recommend using the callback version of setState so that you can reference the value of fetchTrigger that you need without needing to add it as a dependency.
useEffect(() => {
// code
if (!(Object.keys(store.objects).length === 0)) {
// code stuffs
} else {
// use the callback version of setState to get the previous/current value of fetchTrigger
// so you can remove the dependency on the fetchTrigger
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
}
// remove fetchTrigger as a dependency
}, [page, store.objects, store.objects.results, store.objects.totalPages]);
With those issues explained, you'd be better off not adding new state for your items, pages, or loading and instead deriving that from your redux store, because it looks like thats all it is.
const items = useSelector((state) => state.store.objects?.results);
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const loading = useSelector((state) => !Object.keys(state.store.objects).length === 0);
and removing the effect entirely in favor of a function to add to the onActivePageChange event.
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(getAllProducts({
params: {
page,
},
}));
};
return (
<CPagination
// other fields
onActivePageChange={onActivePageChange}
/>
);
But for initial results you will still need some way to fetch, you can do this with an effect that only runs once when the component is mounted. This should do that because dispatch should not be changing.
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
Together that would look like this with the recommended changes:
const ProductsTable = props => {
const items = useSelector(state => state.store.objects?.results);
const pages = useSelector(state => state.store.objects?.total?.totalPages);
const loading = useSelector(state => !Object.keys(state.store.objects).length === 0);
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(
getAllProducts({
params: {
page,
},
})
);
};
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={[
'title',
'slug',
{
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false,
},
]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
/>
<CPagination
pages={pages}
activePage={page}
onActivePageChange={onActivePageChange}
className={pages < 2 ? 'd-none' : ''}
/>
</CCard>
);
};
export default ProductsTable;
first of all you don't need any synchronous to achieve the results you want, you have to switch up your code so it doesn't use the state of react since you are already using some kind of global store ( i assume redux ); what you need to do is grab all the items straight from the store don't do an extra logic on the component (read for the separation of concerns); Also I would suggest to do the pagination on the server side not just paginate data on the front end. (getAllProducts() method to switch on fetching just a page of results and not all the products); Your code have alot of dispatches because you are using page and fetchTrigger as dependencies of useEffect hook that means every time the page or fetchTrigger value changes the code inside useEffect will run again resulting in another dispatch;
Here is a slightly modified part of your code, you need to add some extra stuff on your action and a loading param in your global state
const ProductsTable = (props) => {
const dispatch = useDispatch();
// PUT THE LOADING IN THE GLOBAL STATE OR HANDLE IT VIA A CALLBACK OR ADD A GLOBAL MECHANISM TO HANLE LOADINGS INSIDE THE APP
const loading = useSelector(() => state.store.loading)
const items = useSelector((state) => state.store.objects?.results); // ADD DEFAULT EMPTY VALUES FOR objects smthg like : { objects: { results: [], total: { totalPages: 0 } }}
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0); // I DONT UNDERSTAND THIS ONE
const fetchData = () => dispatch(getAllProducts({ params: { page }}));
useEffect(() => {
fetchData();
}, []);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
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