ReactJS, useEffect without depts vs const - reactjs

I've just see this code:
const defaultState = { // some object };
const SomeComponent = ({
complete
}) => {
const [state, setState] = useState(defaultState);
const {
color,
icon
} = state;
useEffect(() => {
if (complete) {
setState({
...state,
color: '#61a60e',
});
} else {
setState({
...state,
color: 'red',
});
}
}, []);
// etc
I wonder, is that really necesary that useEffect?
The if statement inside the useEffect has a control value that comes as prop on the componente (complete), so in my opinion that code would be the same than this:
const defaultState = { // some object };
const SomeComponent = ({
complete
}) => {
const [state, setState] = useState(defaultState);
const {
color,
icon
} = state;
setState({
...state,
color: complete ? '#61a60e' : 'red'
});
I'm ok? Or there's something about useEffect that I'm not considering.

First, You need to understand about useEffect: https://reactjs.org/docs/hooks-effect.html
In your example, useEffect was call without dependencies. So it likes componentDidMout and only calls one time. Base on the complete props. It will update color for state. It works fine.
About your update without useEffect. setState call directly in the component. So every component render (state or props change). The setState will be called => state change => component render => ... => infinite rerender. It's terrible. Never do that.
Note: If you use setState in useEffect, you need to add [dependencies] to avoid infinite loops. dependencies maybe have value or not(like example above). Make sure you understand dependencies in useEffect before using it.

Related

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

how to prevent re-render if fetch from many sources in react hooks?

I'm using react hooks in React Native.
My problem is that the function of useState which to initialize state makes re-render.
So if I set state like below
const [A, setA] = useState(false);
const [B, setB] = useState(false);
const [C, setA] = useState(false);
// ...
const testFunc = () => {
setA(true);
setB(true);
setC(true);
}
EDITED
I think examples were wrong.
Here's another example.
const useFetch(coords) {
const [example, setExample] = useState([])
const [checker, setChecker] = useState(false);
const fetchData = () => {
axios.fetch(`url+${coords.latitue}+${coords.longitude}`).then(){
setExample(res.data());
setChecker(true);
}
}
useEffect(() => {
fetchData();
}, [coords])
return example;
}
const useLocation = () => {
...
return coords;
}
const App = () => {
const coords = useLocation();
const example = useFetch(coords); // example is undefined.
const [data, setData] = useState(example); // data is undefined.
}
It causes many re-render as many as I use the set function.
Is this natural thing?
If I don't want to make this re-render, can't use the set function multiple times?
You can not do it in straightforward way. I will suggest you the two solutions for it.
Solution 1: Combine states in one object.
const [value, setValue] = useState({A: false, B: false, C: false});
// ...
const testFunc = () => {
setValue({A: true, B: true, C: true});
}
Solution 2: Another solution is useReducer.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{A: false, B: false, C: false}
);
// ...
const testFunc = () => {
setState({A: true, B: true, C: true});
}
Here I have implemented your another example: https://stackblitz.com/edit/react-usestate-wcjshg
Hope this will help for you!
React does not batch state updates if they are triggered outside React-based event. That means, if you want your state updates to be batched you need to wrap it on an event handle such as onClick.
If your local component state is non-trival and/or using an event handler is not an option, I'd recommend you to use useReducer as you can batch your state updates within that.
This appears to be normal React behavior. It works the exact same way if you were to call setState() in a class component multiple times.
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 a setTimeout().
I think there's plans long-term to always batch events, but not sure on the details
Sources:
https://github.com/facebook/react/issues/14259#issuecomment-439632622
https://github.com/facebook/react/issues/14259#issuecomment-468937068
As stated in the other answers, React does not batch state updates if they are triggered outside React-based events (in then for example), one of the solutions is to merge your state in one object and call setState one time. But if you like to keep your state separated, the solution is to use ReactDOM.unstable_batchedUpdates like this :
const fetchData = () => {
axios.fetch(`url+${coords.latitue}+${coords.longitude}`).then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setExample(res.data());
setChecker(true);
});
});
}
Recommended by Dan Abramov here

React hook useEffect causes initial render every time a component mounts

I am new to React hooks. So, I wanted to implement componentWillReceiveProps with React hooks.
I used React.useEffect() like so:
React.useEffect(() => {
console.log(props.authLoginSuccess); // initially called every time, the component renders
}, [props.authLoginSuccess]);
return ( //JSX...)
onst mapStateToProps = (state: any): StateProps => {
return {
authLoginSuccess: selectAuthLoginSuccess(state) //used selector to select authLoginSuccess
};
};
export default connect(
mapStateToProps,
// mapDispatchToProps
{ authLogin, toggleLoadingStatus }
)(Auth);
The problem is, useEffect is called each time the component renders initially, which I don't want. I only want it to render, when "props.authLoginSuccess" changes.
Since you want the effect to not run on initial render, you can do that by making use of useRef
const initialRender = useRef(true);
React.useEffect(() => {
if(initialRender.current) {
initialRender.current = false;
} else {
console.log(props.authLoginSuccess); // initially called every time, the component renders
}
}, [props.authLoginSuccess]);
Wrap it in an if condition like this:
React.useEffect(() => {
if (props.authLoginSuccess) {
console.log(props.authLoginSuccess);
}
}, [props.authLoginSuccess]);
Note that the effect will still run though - both initially and every time props.authLoginSuccess changes (which is okay!).
The if block will prevent running console.log(props.authLoginSuccess) when props.authLoginSuccess is falsy. So if you don't want it running initially i.e. when component mounts, just make sure that props.authLoginSuccess is false initially.
You could add another state that would monitor whether or not the component has been mounted.
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
if (isMounted) {
console.log(props.authLoginSuccess);
} else {
setIsMounted(true);
}
}, [props.authLoginSuccess]);
That way, it will only execute when the component has been mounted.

React: Trying to rewrite ComponentDidUpdate(prevProps) with react hook useEffect, but it fires when the app starts

I'm using a componentDidUpdate function
componentDidUpdate(prevProps){
if(prevProps.value !== this.props.users){
ipcRenderer.send('userList:store',this.props.users);
}
to this
const users = useSelector(state => state.reddit.users)
useEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
but it I get the message 'users changed' when I start the app. But the user state HAS NOT changed at all
Yep, that's how useEffect works. It runs after every render by default. If you supply an array as a second parameter, it will run on the first render, but then skip subsequent renders if the specified values have not changed. There is no built in way to skip the first render, since that's a pretty rare case.
If you need the code to have no effect on the very first render, you're going to need to do some extra work. You can use useRef to create a mutable variable, and change it to indicate once the first render is complete. For example:
const isFirstRender = useRef(true);
const users = useSelector(state => state.reddit.users);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log('users changed')
console.log({users})
}
}, [users]);
If you find yourself doing this a lot, you could create a custom hook so you can reuse it easier. Something like this:
const useUpdateEffect = (callback, dependencies) => {
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
return callback();
}
}, dependencies);
}
// to be used like:
const users = useSelector(state => state.reddit.users);
useUpdateEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
As from: Using the Effect Hook
This, it will be invoked as the component is painted in your DOM, which is likely to be closer to componentDidMount.

Call a Redux Action inside a useEffect

My objective here is to call an action inside a useEffect.
const ShowTodos = (props) =>{
useEffect(()=>{
props.fetchTodos()
},[])
}
const mapStateToProps = (state)=>{
return {
todos:Object.values(state.todos),
currentUserId:state.authenticate.userId
}
}
export default connect(mapStateToProps,{fetchTodos})(ShowTodos)
It works fine but I got a warning
React Hook useEffect has a missing dependency: 'props'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
But if I'm going to add props as my second parameter in my useEffects then it will run endlessly.
My first workaround here is to use the useRef but it seems that it will always re-render thus re-setup again the useRef which I think is not good in terms of optimization.
const ref = useRef();
ref.current = props;
console.log(ref)
useEffect(()=>{
ref.current.fetchTodos()
},[])
Is there any other workaround here ?
That is an eslint warning that you get if any of the dependency within useEffect is not part of the dependency array.
In your case you are using props.fetchTodos inside useEffect and the eslint warning prompts you to provide props as a dependency so that if props changes, the useEffect function takes the updated props from its closure.
However since fetchTodos is not gonna change in your app lifecycle and you want to run the effect only once you can disable the rule for your case.
const ShowTodos = (props) =>{
const { fetchTodos } = props
useEffect(()=>{
fetchTodos()
// eslint-disable-next-line import/no-extraneous-dependencies
},[])
}
const mapStateToProps = (state)=>{
return {
todos:Object.values(state.todos),
currentUserId:state.authenticate.userId
}
}
export default connect(mapStateToProps,{fetchTodos})(ShowTodos)
You however can solve the problem without disabling the rule like
const ShowTodos = (props) =>{
const { fetchTodos } = props
useEffect(()=>{
fetchTodos()
},[fetchTodos])
}
I however will recommend that you know when exactly should you disable the rule or pass the values to the dependency array.
You have to add fetchTodos to dependencies.
const ShowTodos = ({ fetchTodos }) => {
useEffect(() => {
fetchTodos();
}, [fetchTodos])
...
}
or like this.
const ShowTodos = (props) => {
const { fetchTodos } = props;
useEffect(() => {
fetchTodos();
}, [fetchTodos])
...
}

Resources