Yes, I know this question have been asked zillion times, but none of the answers are fit to my code.
My useEffect() calls an outside function (showIncrement()) that logs my increment state value. The problem is showIncrement() is also used by a button, so I can't move it inside the useEffect() scope.
I know a few solutions to this:
re-create the function inside useEffect(), but then I have two identical functions
use the React useCallback() function, React documentation call it the last resort, and other answer in another question also don't recommend using it, so I'm not really sure
The question is, what is the best way to solve this problem? Is it safe to use useCallback()?
Here's my code:
const App = () => {
const [increment, setIncrement] = React.useState(2);
const showIncrement = React.useCallback(() => console.log(increment), [
increment,
]);
React.useEffect(() => {
showIncrement();
}, [showIncrement]);
return (
<div className="App">
<button type="button" onClick={showIncrement}>
Show Increment
</button>
</div>
);
};
If showIncrement doesn't need access to update the state variables, the easiest way to fix this is to move it outside the component, so it won't be recreated on every render and have a stable reference:
const showIncrement = (increment) => console.log(increment);
const App = () => {
const [increment, setIncrement] = React.useState(2);
React.useEffect(() => {
showIncrement(increment);
}, [increment]);
return (
<div className="App">
<button type="button" onClick={() => showIncrement(increment)}>
Show Increment
</button>
</div>
);
};
Another way is to use the useCallback hook:
const App = () => {
const [increment, setIncrement] = React.useState(2);
const showIncrement = React.useCallback(() => console.log(increment), [
increment
]);
React.useEffect(() => {
showIncrement();
}, [showIncrement]);
return (
<div className="App">
<button type="button" onClick={showIncrement}>
Show Increment
</button>
</div>
);
};
As I understand this situation, you want to:
Re-use showIncrement logic.
Run showIncrement on component's mount.
Not violate linter warnings.
The problem here is that showIncrement depends on state value increment, so either way, your useEffect has to include increment in dep array.
Usually, you go for useCallback when its dep array ([increment] in this case) not frequently changes.
So depending on your use case, since showIncrement triggered only onClick it seems like a good choice.
const App = () => {
const [increment] = React.useState(2);
const isMounted = useRef(false);
const showIncrement = useCallback(() => {
console.log(increment);
}, [increment]);
React.useEffect(() => {
if (!isMounted.current) {
showIncrement();
}
}, [showIncrement]);
return (
<div className="App">
<button type="button" onClick={showIncrement}>
Show Increment
</button>
</div>
);
};
Related
I understand (somewhat) how to use useEffect to update state, but I struggle with situations like when you need current state inside of another function, before the "nextTick" as it were.
Here is a simple Codepen with the exact issue. Make sure the Pen console is open.
https://codepen.io/kirkbross/pen/vYRNpqG?editors=1111
const App = () => {
const [state, setState] = React.useState(null);
// how can I make sure the below function knows what the current state really is?
const handleAppend = (state) => {
console.log("click");
console.log(state?.text + " foobar");
};
return (
<div class="app">
<div className="row">
<span>Text: </span>
<input
type="text"
onChange={() => setState({ text: e.target.value })}
/>
</div>
<div className="row">
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
</div>
);
};
You're shadowing your state variable in your handleAppend function. You don't need to pass in an argument since state is available in scope of the component
const handleAppend = () => {
console.log("click");
console.log(state?.text + " foobar");
};
I did some changes. You dont need to use ur state as a parameter, since your textState lives inside your app component and there for you can reach it within your function.
Also, i changed the state and setState to textState, setTextState to make it less confusing. Also after clicking on the button and console logging, i cleared the textState so the next value wont be effected. Check it out below.
function App() {
const [textState, setTextState] = React.useState(null);
const handleAppend = () => {
console.log("click");
console.log(textState + " foobar");
setTextState('')
//also, you could make the input box clear after each press on button by adding value={textState} in the input.
};
return (
<div className="App">
<input
type="text"
onChange={(e) => setTextState(e.target.value)}
/>
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
);
}
My real world case was more complicated than the Pen. The actual function needing state was a useCallback function and I had forgotten to add state to the dep array of the useCallback function.
const handleDragEnd = useCallback(
async (result) => {
const { source, destination, draggableId } = result;
console.log(state); // shows up now.
},
[state], // I had forgotten to add state to the useCallback dep array
);
I have some issue on understandsing the lifecycle in React, so iam using useEffects() since i do understand that it was the right way to call a method before the component rendered (the replacement for componentDidMount ).
useEffect(() => {
tagSplit = tagArr.split(',');
});
And then i call tagSplit.map() function on the component, but it says that tagSplit.map is not a function
{tagSplit.map((item, index) => (
<div className="styles" key={index}>
{item}
</div>
))}
Is there something wrong that i need to fix or was it normal ?
useEffect runs AFTER a render and then subsequently as the dependencies change.
So yes, if you have tagSplit as something that doesn't support a map function initially, it'll give you an error from the first render.
If you want to control the number of times it runs, you should provide a dependency array.
From the docs,
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
This article from Dan Abramov's blog should also help understand useEffect better
const React, { useState, useEffect } from 'react'
export default () => {
const [someState, setSomeState] = useState('')
// this will get reassigned on every render
let tagSplit = ''
useEffect(() => {
// no dependencies array,
// Runs AFTER EVERY render
tagSplit = tagArr.split(',');
})
useEffect(() => {
// empty dependencies array
// RUNS ONLY ONCE AFTER first render
}, [])
useEffect(() => {
// with non-empty dependency array
// RUNS on first render
// AND AFTER every render when `someState` changes
}, [someState])
return (
// Suggestion: add conditions or optional chaining
{tagSplit && tagSplit.map
? tagSplit.map((item, index) => (
<div className='styles' key={index}>
{item}
</div>
))
: null}
)
}
you can do something like this .
function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
let tagSplit = tagArr.split(',');
setArr(tagSplit);
}, []);
return (
<>
{arr.map((item, index) => (
<div className="styles" key={index}>
{item}
</div>
))}
</>
)
}
Answering the question's title:
useEffect runs after the first render.
useMemo runs before the first render.
If you want to run some code once, you can put it inside useMemo:
const {useMemo, Fragment} = React
const getItemsFromString = items => items.split(',');
const Tags = ({items}) => {
console.log('rendered')
const itemsArr = useMemo(() => getItemsFromString(items), [items])
return itemsArr.map((item, index) => <mark style={{margin: '3px'}} key={index}>{item}</mark>)
}
// Render
ReactDOM.render(<Tags items='foo, bar, baz'/>, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
For your specific component, it's obvious there is no dilema at all, as you can directly split the string within the returned JSX:
return tagArr.split(',').map((item, index) =>
<div className="styles" key={index}>
{item}
</div>
)
But for more complex, performance-heavy transformations, it is best to run them only when needed, and use a cached result by utilizing useMemo
I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
useEffect(() => {
//Calling this function
const handleClick = () => {
const lazyApp = import("firebase/app")
const lazyStore = import("firebase/firestore")
//Some code here
}
}, [])
//Calling the function here
<MDBBtn
to=""
className="btn p-0 btn-white bg-transparent"
title="Add To Cart"
onClick={handleClick}
>
<FontAwesomeIcon
icon={faShoppingCart}
style={{ fontSize: "1.3rem" }}
/>
</MDBBtn>
I am trying to call a function after a button click event. But, I want that function to be defined inside useEffect() due to some reasons. I get the error that handleClick is not defined. What is the solution for this?
Because you can't. That's not how it works. useEffect — without a second param— works like componentDidMount lifecycle. The function you're calling has already been called when the component is mounted.
I don't know for what reason you're trying to call an onclick function inside of an useEffect, I've never seen anything like that.
I found this article describing how to implement in a class component:
componentDidMount() {
const lazyApp = import('firebase/app')
const lazyDatabase = import('firebase/database')
Promise.all([lazyApp, lazyDatabase]).then(([firebase]) => {
const database = getFirebase(firebase).database()
// do something with `database` here,
// or store it as an instance variable or in state
// to do stuff with it later
})
}
In your case you will need to mimic that componentDidMount via useEffect and implement some click handler.
You could try to do the following:
let lazyApp, lazyDatabase;
useEffect(() => {
lazyApp = import("firebase/app");
lazyDatabase = import("firebase/firestore");
}, []);
const handleClick = (e) => {
e.preventDefault();
Promise.all([lazyApp, lazyDatabase]).then(([firebase]) => {
const database = getFirebase(firebase).database()
// do something with `database` here,
// or store it as an instance variable or in state
// to do stuff with it later
})
};
This is untested, but maybe that will help.
Even though this seems weird and like an anti-pattern, this is how you could implement it. Using a useRef to keep the reference for your function.
Please check if you really need to do this.
function App() {
console.log("Rendering App...");
const click_ref = React.useRef(null);
React.useEffect(()=>{
function handleClick() {
console.log("Running handleClick defined inside useEffect()...");
}
console.log("Updating click_ref...");
click_ref.current = handleClick;
},[]);
return(
<React.Fragment>
<div>App</div>
<button onClick={()=>click_ref.current()}>Click</button>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};