Using two useeffect react hooks - reactjs

I try to use react with hooks. I have this state:
const [state, setState] = useState<State>({
titel: "",
somethingElse: "",
tabledata: [],
});
I have two useeffect:
// componentDidMount, Initial variable deklaration
useEffect(() => {
//Do something
//Set initial states
setState({
...state,
titel: props.titel,
somethingElse: props.somethingElse,
})
}, []);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
//Do something and generate tabledata
let tabledata ....
//Set tabledata
setState({
...state,
tabledata: tabledata,
})
}, [props.taenzer]);
Now I have the behavior, that the second useeffect is overwriting the first useeffect setState command.
My variable titel and somethingElse is always empty.
Now I could change my deklaration of state, something in this way:
const [titel, setTitel] = useState<>({
titel = ""
});
const [somethingElse, setSomethingElse] = useState<>({
somethingElse = ""
});
But this makes the whole unclear and it is not so easy to set the state of several variables in one time like we could with setState(...state, titel="Hello", somethingElse="Else")
Any other possibility?

the second useeffect is overwriting the first useeffect setState
useState function doesn't automatically merge the state. So you would need to make use of the previous state accessible via callback.
useEffect(
() => {
const tabledata = ...
setState(prevState => ({ // use previous state
...prevState,
tabledata
}))
}, [props.taenzer]
)
Any other possibility?
You can have lazy initialization of your state via props and remove the first effect (without dependency)
const [{ title, somethingElse, tabledata }, setState] = useState<State>({
...props,
tabledata: [],
});

Related

React component does not re-render when I use a component for building forms

So I have this component which is used for forms handling:
import React, { useState } from "react";
export const useForm = (callback: any, initialState = {}) => {
const [values, setValues] = useState(initialState);
const onChange = (event: React.ChangeEvent<any>) => {
setValues({ ...values, [event.target.name]: event.target.value });
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
await callback();
};
return {
onChange,
onSubmit,
setValues,
values,
};
}
in my component, I use it like this:
const { id } = useParams();
const initialState = {
field1: "",
field2: "",
....
}
const { onChange, onSubmit, setValues, values } = useForm(
submitData,
initialState
);
useEffect(
() => {
if(id) {
getData().then((response) => {
setValues(response);
console.log(response);
console.log(values);
});
}
}
,[]);
async function getData() {
return await (await instance.get(url)).data;
}
The two console.logs in the useEffect hook are different and I don't understand why.
I used setValues so I expect the components to be re-drawn and the "values" variable to be updated with the values from the response, but it still has the values from the initialState for some reason.
Why is that happening and how can I fix it?
The change of state using setState hook will not be reflected synchronously.
This is taken from react docs:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.

ReactJS, props between child and parent in render loop

I have this passing props code between child and its parent:
Parent.js
const defaultState = {
canOperate: false,
// among other states
};
const [state, setState] = useState(defaultState);
// this technique is for avoiding some re-renders in some occasions, I'm using a lot of this way to set the
// state in this Parent component, but isn't working on this case
const handleCanOperate = (value: boolean) => {
setState(state => ({
...state,
canOperate: value
}));
};
<ChildComponent
onCanOperate={handleCanOperate}
/>
Child.js
// from props I have: onCanOperate
useEffect(() => {
const handleCanOperate = (canOperate: boolean) => onCanOperate(canOperate);
if (data) {
handleCanOperate(false);
setState(state => ({
...state,
isDisabled: true
}));
} else {
setState(s => ({
...state,
isDisabled: false
}));
handleCanOperate(true);
}
}, [data, onCanOperate]);
With this approach I get a loop, but if in the parent I do this change there is no problem:
Parent that works:
const defaultState = {
// other states
};
const [state, setState] = useState(defaultState);
const [canOperate, setCanOperate] = useState(false);
<ChildComponent
onCanOperate={setCanOperate}
/>
I don't want to use two setters for state in the Parent component, that's why I put a handler to call the already working setState
Any idea?
I believe your loop is caused by the handleCanOperate function which gets redefined on re-render (when the state changes). This gets passed down to your child, which reruns the logic in your useEffect hook because it thinks onCanOperate has changed.
This also explains why it works when you place your handler in it's own state. React state is memoized and doesn't reinitialise on re-renders.
To fix this you could memoize your handleCanOperate so it doesn't reinitialise every re-render by wrapping it around a useCallback hook. This hook works similarly to the useEffect hook and will only re-initialise your callback when a value in it's dependency array changes.
Your parent would look something like this:
const defaultState = {
canOperate: false,
// among other states
};
const [state, setState] = useState(defaultState);
const handleCanOperate = useCallback((value: boolean) => {
setState(state => ({
...state,
canOperate: value
}));
}, []);
<ChildComponent
onCanOperate={handleCanOperate}
/>
I haven't tested this, but I do believe this would fix your issue.

React useEffect hook register callback with access to state

I´m creating a new instacne of a map component in the useEffect hook with second parameter [] so it only runs once.
After creating the instance I want to register a callback, which is fired if the user interacts with the map. Inside this callback I want to access the state of my component. How can I do this, without causing a loop? If I don´t add my state to the second parameter the state stays the same with every run of the callback (as expected), but if I add it, I cause a loop.
export const Map = (props: any) => {
const [state, setState]: [MapState, any] = useGlobalState({
id: 'MAP',
initialValue: getInitialValue(initialized),
});
useEffect(() => {
mapInstance = new mapFramework.Map(});
mapInstance.on('move', () => {
console.log(state);
});
}, []);
}
You can use useRef
export const Map = (props: any) => {
const [state, setState]: [MapState, any] = useGlobalState({
id: 'MAP',
initialValue: getInitialValue(initialized),
});
const stateRef = useRef(state)
useEffect(()=>{
stateRef.current = state
}, [state])
useEffect(() => {
mapInstance = new mapFramework.Map(});
mapInstance.on('move', () => {
console.log(stateRef.current)
});
}, []);
}

React custom hook not receiving current state of value from another custom hook

I have an event handler that sets a selected value in a list to [values, setValues] state hook. After doing this, I have a useEffect hook that calls an [options, setOptions] state hook to set that value to the options value. However, although setValues is able to set the state (which is shown when I console.log, setOptions is not able to set the new values state to the options state
UPDATE
Moving the console.log(options.data) out of the effect solves the problem partially because it logs out the current value, however, returning that value and using it in another custom hook returns the default value of options. I have updated the code.
I have tried using useRef to get the current value but my implementation is not working. Maybe I am doing that bit wrong.
const useFirstCustomHook = () => {
const [options, setOptions] = useState({
data: null
})
const [values, setValues] = useState({
name: "",
myid: "my-id"
});
//state changer
function handleChange(event, id) {
setValues(oldValues => ({
...oldValues,
name: event.target.value,
myid: id.props.id,
}));
}
useEffect(() => {
setOptions({
data: values
})
},[values])
console.log(options.data) //not printing the current state
return {
options
}
}
export default useFirstCustomHook
const [waitState, setWait] = useState(0)
const { values, options } = useFirstCustomHook ()
useEffect(() => {
console.log(options)
let wait = () => {
setTimeout(() => setWait(waitState+1), 10000)
}
async function fetchData() {
//fetch some data then wait
wait()
}
},[wait])
export default useFetchedData
I expect the new state of options to be the same as the current value of values but it is only reflecting the default value.

React Hooks: updating multiple hook states atomically [duplicate]

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

Resources