How to remove a functional dependancy from a state dispatch update - javascript

The issue I'm facing is that I have an item in state called foo and then I have a useCallBack that fires a series of dispatches and references the state item foo.
I'm also firing a UseEffect on page load to handle any changes from the database.
The issue I'm having is as follows
The page loads
useEffect triggers and loads data from server
The useEffect runs it's code and if required calls the useCallBack which is a dependancy of the useEffect
The useCallback fires a series of dispatches that update foo
State Item foo has changed
The useCallback is watching foo and updates
The useEffect is watching the useCallback and as it has changed it runs again
Steps 3-7 repeat infinitely
An example in code format:
const { state, dispatch } = React.useContext(AppContext};
const { foo } = state;
const handleBar = React.useCallBack((bar: IBarType) => {
const bar_exists = _.find(foo, {id: bar.id});
if (bar){
dispatch({ type: 'rainbows', payload: bar });
} else {
dispatch({ type: 'clouds', payload: bar });
}
}, [foo]}
React.useEffect(() => {
serverFunction...
if (x){
handleBar(data)
}
}, [handleBar]}
My assumption is that the dependancy on foo within the handleBar callback is causing the infinite loop, I know you can remove the dependancy from a reactHook by using something like setState(bar => bar ...).
However, I Just can't seem to apply the same rules to the handleBar function.
I've tried const bar_exists = _.find(foo: as IFooType => foo, { id: bar.id}); to no luck.

Related

React inputs stuck and not changing [duplicate]

The useEffect React hook will run the passed-in function on every change. This can be optimized to let it call only when the desired properties change.
What if I want to call an initialization function from componentDidMount and not call it again on changes? Let's say I want to load an entity, but the loading function doesn't need any data from the component. How can we make this using the useEffect hook?
class MyComponent extends React.PureComponent {
componentDidMount() {
loadDataOnlyOnce();
}
render() { ... }
}
With hooks this could look like this:
function MyComponent() {
useEffect(() => {
loadDataOnlyOnce(); // this will fire on every change :(
}, [...???]);
return (...);
}
If you only want to run the function given to useEffect after the initial render, you can give it an empty array as second argument.
function MyComponent() {
useEffect(() => {
loadDataOnlyOnce();
}, []);
return <div> {/* ... */} </div>;
}
TL;DR
useEffect(yourCallback, []) - will trigger the callback only after the first render.
Detailed explanation
useEffect runs by default after every render of the component (thus causing an effect).
When placing useEffect in your component you tell React you want to run the callback as an effect. React will run the effect after rendering and after performing the DOM updates.
If you pass only a callback - the callback will run after each render.
If passing a second argument (array), React will run the callback after the first render and every time one of the elements in the array is changed. for example when placing useEffect(() => console.log('hello'), [someVar, someOtherVar]) - the callback will run after the first render and after any render that one of someVar or someOtherVar are changed.
By passing the second argument an empty array, React will compare after each render the array and will see nothing was changed, thus calling the callback only after the first render.
useMountEffect hook
Running a function only once after component mounts is such a common pattern that it justifies a hook of its own that hides implementation details.
const useMountEffect = (fun) => useEffect(fun, [])
Use it in any functional component.
function MyComponent() {
useMountEffect(function) // function will run only once after it has mounted.
return <div>...</div>;
}
About the useMountEffect hook
When using useEffect with a second array argument, React will run the callback after mounting (initial render) and after values in the array have changed. Since we pass an empty array, it will run only after mounting.
We have to stop thinking in component-life-cycle-methods (i.e. componentDidMount). We have to start thinking in effects. React effects are different from old-style class-life-cycle-methods.
By default effects run after every render cycle, but there are options to opt out from this behaviour. To opt out, you can define dependencies that mean that an effect is only carried out when a change to one of the dependencies is made.
If you explicitly define, that an effect has no dependecy, the effect runs only once, after the first render-cycle.
1st solution (with ESLint-complaint)
So, the first solution for your example would be the following:
function MyComponent() {
const loadDataOnlyOnce = () => {
console.log("loadDataOnlyOnce");
};
useEffect(() => {
loadDataOnlyOnce(); // this will fire only on first render
}, []);
return (...);
}
But then the React Hooks ESLint plugin will complain with something like that:
React Hook useEffect has missing dependency: loadDataOnlyOnce. Either include it or remove the dependency array.
At first this warning seems annoying, but please don't ignore it. It helps you code better and saves you from "stale closures". If you don't know what "stale closures" are, please read this great post.
2nd solution (the right way, if dependency is not dependent on component)
If we add loadDataOnlyOnce to the dependency array, our effect will run after every render-cycle, because the reference of loadDataOnlyOnce changes on every render, because the function is destroyed(garbarge-collected) and a new function is created, but that's exactly what we don't want.
We have to keep the same reference of loadDataOnlyOnce during render-cycles.
So just move the function-definition above:
const loadDataOnlyOnce = () => {
console.log("loadDataOnlyOnce");
};
function MyComponent() {
useEffect(() => {
loadDataOnlyOnce(); // this will fire only on first render
}, [loadDataOnlyOnce]);
return (...);
}
With this change you ensure that the reference of loadDataOnlyOnce will never change. Therefore you can also safely add the reference to the dependency array.
3rd solution (the right way, if dependency is dependent on component)
If the dependency of the effect (loadDataOnlyOnce), is dependent on the component (need props or state), there's React's builtin useCallback-Hook.
An elementary sense of the useCallback-Hook is to keep the reference of a function identical during render-cycles.
function MyComponent() {
const [state, setState] = useState("state");
const loadDataOnlyOnce = useCallback(() => {
console.log(`I need ${state}!!`);
}, [state]);
useEffect(() => {
loadDataOnlyOnce(); // this will fire only when loadDataOnlyOnce-reference changes
}, [loadDataOnlyOnce]);
return (...);
}
function useOnceCall(cb, condition = true) {
const isCalledRef = React.useRef(false);
React.useEffect(() => {
if (condition && !isCalledRef.current) {
isCalledRef.current = true;
cb();
}
}, [cb, condition]);
}
and use it.
useOnceCall(() => {
console.log('called');
})
or
useOnceCall(()=>{
console.log('Fetched Data');
}, isFetched);
Pass an empty array as the second argument to useEffect. This effectively tells React, quoting the docs:
This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
Here's a snippet which you can run to show that it works:
function App() {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUser(data.results[0]);
});
}, []); // Pass empty array to only run once on mount.
return <div>
{user ? user.name.first : 'Loading...'}
</div>;
}
ReactDOM.render(<App/>, document.getElementById('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>
I like to define a mount function, it tricks EsLint in the same way useMount does and I find it more self-explanatory.
const mount = () => {
console.log('mounted')
// ...
const unmount = () => {
console.log('unmounted')
// ...
}
return unmount
}
useEffect(mount, [])
leave the dependency array blank . hope this will help you understand better.
useEffect(() => {
doSomething()
}, [])
empty dependency array runs Only Once, on Mount
useEffect(() => {
doSomething(value)
}, [value])
pass value as a dependency. if dependencies has changed since the last time, the effect will run again.
useEffect(() => {
doSomething(value)
})
no dependency. This gets called after every render.
I had this issue with React 18. This is how I handled it:
import { useEffect, useRef } from "react";
export default function Component() {
const isRunned = useRef(false);
useEffect(() => {
if(isRunned.current) return;
isRunned.current = true;
/* CODE THAT SHOULD RUN ONCE */
}, []);
return <div> content </div>;
}
Check here how they explain why useEffect is called more than once.
Here is my version of Yasin's answer.
import {useEffect, useRef} from 'react';
const useOnceEffect = (effect: () => void) => {
const initialRef = useRef(true);
useEffect(() => {
if (!initialRef.current) {
return;
}
initialRef.current = false;
effect();
}, [effect]);
};
export default useOnceEffect;
Usage:
useOnceEffect(
useCallback(() => {
nonHookFunc(deps1, deps2);
}, [deps1, deps2])
);
This does not answer your question exactly but may have the same intended affect of only running a function once and after the first render. Very similar to the componentDidMount function. This uses useState instead of useEffect to avoid dependency lint errors. You simply pass a self-executing anonymous function as the first argument to useState. As an aside, I'm not sure why React doesn't simply provide a hook that does this.
import React, { useState } from "react"
const Component = () => {
useState((() => {
console.log('componentDidMountHook...')
}))
return (
<div className='component'>Component</div>
)
}
export default Component
I found out after some time spend on the internet. useEffect fires once on component mount, then componennt unmounts and mounts again, useEffect fires again. You have to check more on React docs, why they do that.
So, I used custom hook for that. On unmount you have to change your useRef state. In this case do not forget a return statement: when component unmounts, useEffect runs cleanup function after return.
import React, { useEffect, useRef } from "react"
const useDidMountEffect = (
func: () => void,
deps: React.DependencyList | undefined
) => {
const didMount = useRef(false)
useEffect(() => {
if (didMount.current) {
func()
}
return () => {
didMount.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
}
export default useDidMountEffect
Use it like normal useEffect:
useDidMountEffect(() => {
// your action
}, [])
window.onpageshow works even if the user presses the back button to navigate to the page, unlike passing an empty array as second argument of the use-effect hook which does not fire when returning to the page via the back button (thus not on every form of initial page load).
useEffect(() => {
window.onpageshow = async function() {
setSomeState(false)
let results = await AsyncFunction()
console.log(results, 'Fires on on first load,
refresh, or coming to the page via the back button')
};
};
I found that with the once function from lodash the problem may be solved concisely and elegantly.
import { once } from "lodash";
import { useEffect, useRef } from "react";
export const useEffectOnce = (cb: () => void) => {
const callBackOnce = useRef(once(cb)).current;
useEffect(() => callBackOnce(), [callBackOnce]);
};
Incase you just call the function in useeffect after render you add an empty array as the second argument for the useeffect
useEffect=(()=>{
functionName(firstName,lastName);
},[firstName,lastName])

React when pass data to child throw props data duplicate

I'm create Activities function component and call child function component called Categories when i send categories list to Categories function component and log "props" data send twice first one is empty and second has data as follow
Activies
function Activities() {
const [category, setCategory] = useState([]);
function handelChange({ target }) {
setCategory({
...category,
[target.name]: target.value,
});
}
useEffect(() => {
getCategories().then((_categories) => setCategory(_categories));
}, []);
return (<Categories category={category} onChange={handelChange} />)
}
and categories component
function Categories(props) {
console.log(props);
return (<div></div>)
}
i'm trying to log props in useEffect but problem still exist
This is happening because of how the life cycle in React works. This is correct and expected behavior. Before you load the categories, it is a blank array on the initial render. Then it gets the categories, updates the state, and re-renders, this time with categories.
renders with the initial state(empty)
goes and fetches categories
re-renders with the categories
This is entirely expected. That double log is the initial render and then the updated state render. Remember React is a heavily async library. The useEffect doesn't happen during render, it happens after render. Every state update will also cause an update and thus another log. It might be helpful to research what will cause a React render and how the life cycle behaves.
I think you handleChange function should update item in a array of object not change the state object completely
function handelChange({ target: {name, value} }) {
setCategory(categories => {
const categoryIndex = categories.findIndex(pr => pr.id === id);
const category = categories[categoryIndex];
return [
...categories.slice(0, categoryIndex)),
{
...category,
[name]: value,
},
...categories.slice(categoryIndex + 1))
]);
}

The react component renders several times when using useEffect

I use functional react component with hooks.
const [loaded, setLoaded] = React.useState(null);
const [title, setTitle] = React.useState(title);
React.useEffect(() => {
//...
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => {
setLoaded(true);
setTitle(title);
});
}, []);
In this case, the component is rendered twice. On the one hand, it seems to be logical.
But I also have a checkbox handler in this component
const changeHandler = event => {
//...
setTotal(new_total);
setError(false);
};
In this case, two rendering does not occur, although state also changes 2 times. I can’t understand why this is happening.
P.S.
There is no problem to solve this problem, I wonder why this is exactly what happens
UPD:
If I set in useEffect
setLoaded(true);
setTitle(title);
setTitle2(title);
setTitle3(title);
will be 4 re-renders, and if I set in changeHandler
setTotal(new_total);
setError(false);
setError2(false);
setError3(false);
will be 1 re-render
A side effect of calling setter method of the useState() hook, is that doing so triggers the component to re-render.
One solution to avoid the redundant re-render would be to merge your component state like this:
function functionalComponent() {
/* Merge both values into common "state object */
const [{ loaded, title }, setState] = React.useState({
loaded : null,
title : "inital title"
});
React.useEffect(() => {
/*
Single call to setState triggers on re-render only. The
value of "new title" for title could have been set in the
inital state, however I set it here to show how combined
state can be updated with a single call to setState()
*/
setState({ loaded : true, title : "new title" })
/*
setLoaded(true);
setTitle(title);
*/
}, []);
/* Use loaded and title variables as needed during render */
return <div>{ loaded } - { title }</div>
}
React wraps your event handlers in a call to
unstable_batchedUpdates(), so that your handler runs inside a
callback. Any state updates triggered inside that callback will be
batched. Any state updates triggered outside that callback will not be
batched. Timeouts, promises, and async functions will end up executing
outside that callback, and therefore not be batched.
https://github.com/facebook/react/issues/14259

Why is the cleanup function from `useEffect` called on every render?

I've been learning React and I read that the function returned from useEffect is meant to do cleanup and React performs the cleanup when the component unmounts.
So I experimented with it a bit but found in the following example that the function was called every time the component re-renders as opposed to only the time it got unmounted from the DOM, i.e. it console.log("unmount"); every time the component re-renders.
Why is that?
function Something({ setShow }) {
const [array, setArray] = useState([]);
const myRef = useRef(null);
useEffect(() => {
const id = setInterval(() => {
setArray(array.concat("hello"));
}, 3000);
myRef.current = id;
return () => {
console.log("unmount");
clearInterval(myRef.current);
};
}, [array]);
const unmount = () => {
setShow(false);
};
return (
<div>
{array.map((item, index) => {
return (
<p key={index}>
{Array(index + 1)
.fill(item)
.join("")}
</p>
);
})}
<button onClick={() => unmount()}>close</button>
</div>
);
}
function App() {
const [show, setShow] = useState(true);
return show ? <Something setShow={setShow} /> : null;
}
Live example: https://codesandbox.io/s/vigilant-leavitt-z1jd2
React performs the cleanup when the component unmounts.
I'm not sure where you read this but this statement is incorrect. React performs the cleanup when the dependencies to that hook changes and the effect hook needs to run again with new values. This behaviour is intentional to maintain the reactivity of the view to changing data. Going off the official example, let's say an app subscribes to status updates from a friends' profile. Being the great friend you are, you are decide to unfriend them and befriend someone else. Now the app needs to unsubscribe from the previous friend's status updates and listen to updates from your new friend. This is natural and easy to achieve with the way useEffect works.
useEffect(() => {
chatAPI.subscribe(props.friend.id);
return () => chatAPI.unsubscribe(props.friend.id);
}, [ props.friend.id ])
By including the friend id in the dependency list, we can indicate that the hook needs to run only when the friend id changes.
In your example you have specified the array in the dependency list and you are changing the array at a set interval. Every time you change the array, the hook reruns.
You can achieve the correct functionality simply by removing the array from the dependency list and using the callback version of the setState hook. The callback version always operates on the previous version of the state, so there is no need to refresh the hook every time the array changes.
useEffect(() => {
const id = setInterval(() => setArray(array => [ ...array, "hello" ]), 3000);
return () => {
console.log("unmount");
clearInterval(id);
};
}, []);
Some additional feedback would be to use the id directly in clearInterval as the value is closed upon (captured) when you create the cleanup function. There is no need to save it to a ref.
The React docs have an explanation section exactly on this.
In short, the reason is because such design protects against stale data and update bugs.
The useEffect hook in React is designed to handle both the initial render and any subsequent renders (here's more about it).
Effects are controlled via their dependencies, not by the lifecycle of the component that uses them.
Anytime dependencies of an effect change, useEffect will cleanup the previous effect and run the new effect.
Such design is more predictable - each render has its own independent (pure) behavioral effect. This makes sure that the UI always shows the correct data (since the UI in React's mental model is a screenshot of the state for a particular render).
The way we control effects is through their dependencies.
To prevent cleanup from running on every render, we just have to not change the dependencies of the effect.
In your case concretely, the cleanup is happening because array is changing, i.e. Object.is(oldArray, newArray) === false
useEffect(() => {
// ...
}, [array]);
// ^^^^^ you're changing the dependency of the effect
You're causing this change with the following line:
useEffect(() => {
const id = setInterval(() => {
setArray(array.concat("hello")); // <-- changing the array changes the effect dep
}, 3000);
myRef.current = id;
return () => {
clearInterval(myRef.current);
};
}, [array]); // <-- the array is the effect dep
As others have said, the useEffect was depending on the changes of "array" that was specified in the 2nd parameter in the useEffect. So by setting it to empty array, that'd help to trigger useEffect once when the component mounted.
The trick here is to change the previous state of the Array.
setArray((arr) => arr.concat("hello"));
See below:
useEffect(() => {
const id = setInterval(() => {
setArray((arr) => arr.concat("hello"));
}, 3000);
myRef.current = id;
return () => {
console.log("unmount");
clearInterval(myRef.current);
};
}, []);
I forked your CodeSandbox for demonstration:
https://codesandbox.io/s/heuristic-maxwell-gcuf7?file=/src/index.js
Looking at the code I could guess its because of the second param [array]. You are updating it, so it will call a re-render. Try setting an empty array.
Every state update will call a re-render and unmount, and that array is changing.
It seems expected. As per the documentation here, useEffect is called after first render, every update and unmount.
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Tip
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate and before
componentWillUnmount combined.
This is a Jest test that shows the render and effect order.
As you can see from the expect, once the dependency foo changes due to the state update it triggers a NEW render followed by the cleanup function of the first render.
it("with useEffect async set state and timeout and cleanup", async () => {
jest.useFakeTimers();
let theRenderCount = 0;
const trackFn = jest.fn((label: string) => { });
function MyComponent() {
const renderCount = theRenderCount;
const [foo, setFoo] = useState("foo");
useEffect(() => {
trackFn(`useEffect ${renderCount}`);
(async () => {
await new Promise<string>((resolve) =>
setTimeout(() => resolve("bar"), 5000)
);
setFoo("bar");
})();
return () => trackFn(`useEffect cleanup ${renderCount}`);
}, [foo]);
++theRenderCount;
trackFn(`render ${renderCount}`);
return <span data-testid="asdf">{foo}</span>;
}
const { unmount } = render(<MyComponent></MyComponent>);
expect(screen.getByTestId("asdf").textContent).toBe("foo");
jest.advanceTimersByTime(4999);
expect(screen.getByTestId("asdf").textContent).toBe("foo");
jest.advanceTimersByTime(1);
await waitFor(() =>
expect(screen.getByTestId("asdf").textContent).toBe("bar")
);
trackFn("before unmount");
unmount();
expect(trackFn.mock.calls).toEqual([
['render 0'],
['useEffect 0'],
['render 1'],
['useEffect cleanup 0'],
['useEffect 1'],
['before unmount'],
['useEffect cleanup 1']
])
});

How to wait for a Redux action to change state from a React component

I'm in a component which has a prop currentLineIndex passed by its parent container and coming from a Redux reducer.
In the same component's function I update currentLineIndex with an action creator and then I want to scroll to the new currentLineIndex. But it's not already updated, so I scroll to the same line.
I've tried using async / await as you'll see but it's not working.
In my component:
const { currentLineIndex, setCurrentLineIndex } = props; // passed by the parent container
const handlePlaybackEnd = async () => {
const nextIndex = currentLineIndex + 1;
// await don't wait until global state / component props gets updated
await setCurrentLineIndex(nextLineIndex);
// so when I scroll on next line, I scroll to the same line.
scrollToCurrentLine();
};
const scrollToCurrentLine = () => {
const currentLineEl = document.getElementById(currentLineIndex);
currentLineEl.scrollIntoView({ block: 'start', behaviour: 'smooth' });
};
in actions/index.js:
export function setCurrentLineIndex(index) {
return { type: SET_CURRENT_LINE_INDEX, payload: index };
}
in my reducer:
case SET_CURRENT_LINE_INDEX:
return {
...state,
currentLineIndex: action.payload,
};
Action and reducers are working good and my component state is successfully updated, but it's already too late.
I really need to rely on Redux state, not just to pass the currentLineIndex to scrollToCurrentLine(), that would be too easy :)
What would be the best solution to wait until my component state has been updated ?
One solution can be to define setCurrentLineIndex such that it receives a callback.
//assuming the parent container is a class
setCurrentLineIndex = (index, cb)=>{
//write the logic here then execute the callback
cb()
}
// Then in your child component, you could do something like this
setCurrentLineIndex(nextIndex, scrollToCurrentLine)
I finally solved this by making my component a class component so I could use componentDidUpdate
componentDidUpdate(prevProps) {
if (prevProps.currentLineIndex !== this.props.currentLineIndex) {
this.scrollToCurrentLine(this.props.currentLineIndex);
}
}
handlePlaybackEnd = () => this.props.setCurrentLineIndex(this.props.currentLineIndex + 1);
2020 UPDATE
Hooks made it a lot simpler, no need for a class component, just use an effect:
const scrollToCurrentLine = useCallback(() => {
const currentLineEl = document.getElementById(currentLineIndex);
currentLineEl.scrollIntoView({ block: 'start', behaviour: 'smooth' });
}, [currentLineIndex]);
useEffect(scrollToCurrentLine, [scrollToCurrentLine]);
I solved a similar problem by creating a little npm module. It allows you to subscribe to and listen for redux actions and executes the provided callback function as soon as the state change is complete. Usage is as follows. In your componentWillMount or componentDidMount hook:
subscribeToWatcher(this,[
{
action:"SOME_ACTION",
callback:()=>{
console.log("Callback Working");
},
onStateChange:true
},
]);
Detailed documentation can be found at this link

Categories