I have a button component that has a button inside that has a state passed to it isActive and a click function. When the button is clicked, the isActive flag will change and depending on that, the app will fetch some data. The button's parent component does not rerender. I have searched on how to force stop rerendering for a component and found that React.memo(YourComponent) must do the job but still does not work in my case. It also make sense to pass a check function for the memo function whether to rerender or not which I would set to false all the time but I cannot pass another argument to the function. Help.
button.tsx
interface Props {
isActive: boolean;
onClick: () => void;
}
const StatsButton: React.FC<Props> = ({ isActive, onClick }) => {
useEffect(() => {
console.log('RERENDER');
}, []);
return (
<S.Button onClick={onClick} isActive={isActive}>
{isActive ? 'Daily stats' : 'All time stats'}
</S.Button>
);
};
export default React.memo(StatsButton);
parent.tsx
const DashboardPage: React.FC = () => {
const {
fetchDailyData,
fetchAllTimeData,
} = useDashboard();
useEffect(() => {
fetchCountry();
fetchAllTimeData();
// eslint-disable-next-line
}, []);
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
return (
<S.Container>
<S.Header>
<StatsButton
onClick={handleClick}
isActive={statsButtonActive}
/>
</S.Header>
</S.Container>
)
}
fetch functions are using useCallback
export const useDashboard = (): Readonly<DashboardOperators> => {
const dispatch: any = useDispatch();
const fetchAllTimeData = useCallback(() => {
return dispatch(fetchAllTimeDataAction());
}, [dispatch]);
const fetchDailyData = useCallback(() => {
return dispatch(fetchDailyDataAction());
}, [dispatch]);
return {
fetchAllTimeData,
fetchDailyData,
} as const;
};
You haven't posted all of parent.tsx, but I assume that handleClick is created within the body of the parent component. Because the identity of the function will be different on each rendering of the parent, that causes useMemo to see the props as having changed, so it will be re-rendered.
Depending on if what's referenced in that function is static, you may be able to use useCallback to pass the same function reference to the component on each render.
Note that there is an RFC for something even better than useCallback; if useCallback doesn't work for you look at how useEvent is defined for an idea of how to make a better static function reference. It looks like that was even published as a new use-event-callback package.
Update:
It sounds like useCallback won't work for you, presumably because the referenced variables used by the callback change on each render, causing useCallback to return different values, thus making the prop different and busting the cache used by useMemo. Try that useEventCallback approach. Just to illustrate how it all works, here's a naive implementation.
function useEventCallback(fn) {
const realFn = useRef(fn);
useEffect(() => {
realFn.current = fn;
}, [fn]);
return useMemo((...args) => {
realFn.current(...args)
}, []);
}
This useEventCallback always returns the same memoized function, so you'll pass the same value to your props and not cause a re-render. However, when the function is called it calls the version of the function passed into useEventCallback instead. You'd use it like this in your parent component:
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
Related
I'm learning React and have a custom Context accessible with a use method:
// In a Provider file.
export const useUsefulObject(): UsefulObject {
return useContext(...)
}
I want to use UsefulObject in a click callback in another file. For convenience, I have that callback wrapped in a method someLogic.
const PageComponent: React.FC = () => {
return <Button onClick={(e) => someLogic(e)} />;
}
function someLogic(e: Event) {
const usefulObject = useUsefulObject();
usefulObject.foo(e, ...);
}
However, VS Code alerts that calling useUsefulObject in someLogic is a violation of the rules of hooks since it's called outside of a component method.
I can pass the object down (the below code works!) but it feels like it defeats the point of Contexts to avoid all the passing down. Is there a better way than this?
const PageComponent: React.FC = () => {
const usefulObject = useUsefulObject();
return <Button onClick={(e) => someLogic(e, usefulObject)} />;
}
function someLogic(e: Event, usefulObject) {
usefulObject.foo(e, ...);
}
The hooks need to be called while the component is rendering, so your last idea is one of the possible ways to do so. Another option is you can create a custom hook which accesses the context and creates the someLogic function:
const PageComponent: React.FC = () => {
const someLogic = useSomeLogic();
return <Button onClick={(e) => someLogic(e)} />
}
function useSomeLogic() {
const usefulObject = useUsefulObject();
const someLogic = useCallback((e: Event) => {
usefulObject.foo(e, ...);
}, [usefulObject]);
return someLogic;
}
I have a very simple todo app built with React.
The App.js looks like this
const App = () => {
const [todos, setTodos] = useState(initialState)
const addTodo = (todo) => {
todo.id = id()
todo.done = false
setTodos([...todos, todo])
}
const toggleDone = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo
return { ...todo, done: !todo.done }
})
)
}
return (
<div className="App">
<NewTodo onSubmit={addTodo} />
<Todos todos={todos} onStatusChange={toggleDone} />
</div>
)
}
export default App
where <NewTodo> is the component that renders the input form to submit new todo item and <Todos /> is the component that renders the list of the todo items.
Now the problem is that when I toggle/change an existing todo item, the <NewTodo> will get re-rendered since the <App /> gets re-rendered and the prop it passes to <NewTodo>, which is addTodo will also change. Since it is a new <App /> every render the function defined in it will also be a new function.
To fix the problem, I first wrapped <NewTodo> in React.memo so it will skip re-renders when the props didn't change. And I wanted to use useCallback to get a memoized addTodo so that <NewTodo> will not get unnecessary re-renders.
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos([…todos, todo])
},
[todos]
)
But I realized that obviously addTodo is dependent upon todos which is the state that holds the existing todo items and it is changing when you toggle/change an existing todo item. So this memoized function will also change.
Then I switched my app from using useState to useReducer, I found that suddenly my addTodo is not dependent upon the state, at least that's what it looks like to me.
const reducer = (state = [], action) => {
if (action.type === TODO_ADD) {
return [...state, action.payload]
}
if (action.type === TODO_COMPLETE) {
return state.map((todo) => {
if (todo.id !== action.payload.id) return todo
return { ...todo, done: !todo.done }
})
}
return state
}
const App = () => {
const [todos, dispatch] = useReducer(reducer, initialState)
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
const toggleDone = (id) => {
dispatch({
type: TODO_COMPLETE,
payload: {
id,
},
})
}
return (
<div className="App">
<NewTodo onSubmit={addTodo} />
<Todos todos={todos} onStatusChange={toggleDone} />
</div>
)
}
export default App
As you can see here addTodo is only announcing the action that happens to the state as opposed to doing something directly related to the state. So this would work
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
My question is, does this mean that useCallback never plays nicely with functions that contain useState? Is this ability to use useCallback to memoize the function considered a benefit of switching from useState to useReducer? If I don't want to switch to useReducer, is there a way to use useCallback with useState in this case?
Yes there is.
You need to use the update function syntax of the setTodos
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos((todos) => […todos, todo])
},
[]
)
You've dived down a bit of a rabbit hole! Your original problem was that your addTodo() function depended on the state todos, therefore whenever todos changed you needed to create a new addTodo function and pass that to NewTodo, causing a re-render.
You discovered useReducer, which could help with this since the reducer is passed the current state, and so does not need to capture it in the closure, so it can be stable over changes of todos. However, the React authors have already thought of this situation, and you don't need useReducer (which is really provided as a concession to those who like the Redux style of state updating!). As Gabriele Petrioli pointed out, you can just use the update usage of the state setter. See the docs.
This allows you to write the callback function that Gabriele has provided.
So to answer your final questions:
does this mean that useCallback always does not play nicely with function that contains useState?
useCallback can play perfectly nicely, but you need to be aware of what you are capturing in the closure you pass to useCallback, and if you are using a variable from useState in your callback you need to pass that variable in the list of deps to ensure that your closure is refreshed and it won't be called with out-of-date state.
And then you have to realize that the callback will be a new function thus causing re-renders to components that take it as an argument.
Is this ability to use useCallback to memoize the function considered a benefit of switching from useState to useReducer?
No, not really. useCallback does not prefer useState or useReducer. As I said, useReducer is really there to support a different style of programming, not because it provides functionality that is not available through other means.
If I don't want to switch to useReducer, is there a way to use useCallback with useState in this case?
Yes, as outlined above.
As mentioned by Gabriele Petrioli, You can use the callback syntax or you can keep the value of dependencies of your callback in a ref and use that ref in your callback instead of the state as mentioned here.
In your example, this approach would look something like this:
const [todos, setTodos] = useState([]);
const todosRef = useRef(todos);
useEffect(() => {
todosRef.current = todos;
},[todos]);
const addTodo = useCallback(
todo => {
todo.id = id()
todo.done = false
setTodos([…todosRef.current, todo])
},
[todosRef]
)
I am trying to compare some props, prev props and new props. I created a hook:
export const RelatedArticles: FC<RelatedArticlesProps> = props => {
const { content, currentArticle, fetchRelatedContent } = props;
const usePrevious = <T extends unknown>(value: T): T | undefined => {
const ref = React.useRef<T>();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
};
React.useEffect(() => {
if (currentArticle) {
const prevContent = usePrevious(content);
}
fetchRelatedContent!(paramSet || {}, RELATED_ARTICLE_LIMIT);
}, [currentArticle.drupal_id]);
return(...)
};
And I am getting this error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
What am I doing wrong?
You need to 'hoist' your custom hook declaration outside of the component.
Then invoke it inside.
export const RelatedArticles: FC<RelatedArticlesProps> = props => {
const { content, currentArticle, fetchRelatedContent } = props;
const prevContent = usePrevious(content);
Exactly as it says, you can't use hook calls anywhere else but your function component. Your hook call
React.useEffect(() => {
ref.current = value;
});
is being called from an anonymous function which is not, in fact, a component.
You can either extract the functionality out of the anonymous function or use a class component. As stated on the link you sent us, it looks like you need to define the function out of your function component.
I'm trying to challenge myself to convert my course project that uses hooks into the same project but without having to use hooks in order to learn more about how to do things with class components. Currently, I need help figuring out how to replicate the useCallback hook within a normal class component. Here is how it is used in the app.
export const useMovieFetch = movieId => {
const [state, setState] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetchData = useCallback(async () => {
setError(false);
setLoading(true);
try{
const endpoint = `${API_URL}movie/${movieId}?api_key=${API_KEY}`;
const result = await(await fetch(endpoint)).json();
const creditsEndpoint = `${API_URL}movie/${movieId}/credits?api_key=${API_KEY}`;
const creditsResult = await (await fetch(creditsEndpoint)).json();
const directors = creditsResult.crew.filter(member => member.job === 'Director');
setState({
...result,
actors: creditsResult.cast,
directors
});
}catch(error){
setError(true);
console.log(error);
}
setLoading(false);
}, [movieId])
useEffect(() => {
if(localStorage[movieId]){
// console.log("grabbing from localStorage");
setState(JSON.parse(localStorage[movieId]));
setLoading(false);
}else{
// console.log("Grabbing from API");
fetchData();
}
}, [fetchData, movieId])
useEffect(() => {
localStorage.setItem(movieId, JSON.stringify(state));
}, [movieId, state])
return [state, loading, error]
}
I understand how to replicate other hooks such as useState and useEffect but I'm struggling to find the answer for the alternative to useCallback. Thank you for any effort put into this question.
TL;DR
In your specific example useCallback is used to generate a referentially-maintained property to pass along to another component as a prop. You do that by just creating a bound method (you don't have to worry about dependencies like you do with hooks, because all the dependencies are maintained on your instance as props or state.
class Movie extends Component {
constructor() {
this.state = {
loading:true,
error:false,
}
}
fetchMovie() {
this.setState({error:false,loading:true});
try {
// await fetch
this.setState({
...
})
} catch(error) {
this.setState({error});
}
}
fetchMovieProp = this.fetchMovie.bind(this); //<- this line is essentially "useCallback" for a class component
render() {
return <SomeOtherComponent fetchMovie={this.fetchMovieProp}/>
}
}
A bit more about hooks on functional vs class components
The beautiful thing about useCallback is, to implement it on a class component, just declare an instance property that is a function (bound to the instance) and you're done.
The purpose of useCallback is referential integrity so, basically, your React.memo's and React.PureComponent's will work properly.
const MyComponent = () => {
const myCallback = () => { ... do something };
return <SomeOtherComponent myCallback={myCallback}/> // every time `MyComponent` renders it will pass a new prop called `myCallback` to `SomeOtherComponent`
}
const MyComponent = () => {
const myCallback = useCallback(() => { ... do something },[...dependencies]);
return <SomeOtherComponent myCallback={myCallback}/> // every time `MyComponent` renders it will pass THE SAME callback to `SomeOtherComponent` UNLESS one of the dependencies changed
}
To replicate useCallback in class components you don't have to do anything:
class MyComponent extends Component {
method() { ... do something }
myCallback = this.method.bind(this); <- this is essentially `useCallback`
render() {
return <SomeOtherComponent myCallback={this.myCallback}/> // same referential integrity as `useCallback`
}
}
THE BIG ONE LINER
You'll find that hooks in react are just a mechanism to create instance variables (hint: the "instance" is a Fiber) when all you have is a function.
You can replicate the behavior ofuseCallback by using a memorized function for the given input(eg: movieId)
You can use lodash method
for more in-depth understanding check here
How can the useEffect hook (or any other hook for that matter) be used to replicate componentWillUnmount?
In a traditional class component I would do something like this:
class Effect extends React.PureComponent {
componentDidMount() { console.log("MOUNT", this.props); }
componentWillUnmount() { console.log("UNMOUNT", this.props); }
render() { return null; }
}
With the useEffect hook:
function Effect(props) {
React.useEffect(() => {
console.log("MOUNT", props);
return () => console.log("UNMOUNT", props)
}, []);
return null;
}
(Full example: https://codesandbox.io/s/2oo7zqzx1n)
This does not work, since the "cleanup" function returned in useEffect captures the props as they were during mount and not state of the props during unmount.
How could I get the latest version of the props in useEffect clean up without running the function body (or cleanup) on every prop change?
A similar question does not address the part of having access to the latest props.
The react docs state:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
In this case however I depend on the props... but only for the cleanup part...
You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
DEMO
However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
useLayoutEffect() is your answer in 2021
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
This is equivalent to ComponentWillUnmount.
99% of the time you want to use useEffect, but if you want to perform any actions before unmounting the DOM then you can use the code I provided.
useLayoutEffect is great for cleaning eventListeners on DOM nodes.
Otherwise, with regular useEffect ref.current will be null on time hook triggered
More on react docs https://reactjs.org/docs/hooks-reference.html#uselayouteffect
import React, { useLayoutEffect, useRef } from 'react';
const audioRef = useRef(null);
useLayoutEffect(() => {
if (!audioRef.current) return;
const progressEvent = (e) => {
setProgress(audioRef.current.currentTime);
};
audioRef.current.addEventListener('timeupdate', progressEvent);
return () => {
try {
audioRef.current.removeEventListener('timeupdate', progressEvent);
} catch (e) {
console.warn('could not removeEventListener on timeupdate');
}
};
}, [audioRef.current]);
Attach ref to component DOM node
<audio ref={audioRef} />
useEffect(() => {
if (elements) {
const cardNumberElement =
elements.getElement('cardNumber') || // check if we already created an element
elements.create('cardNumber', defaultInputStyles); // create if we did not
cardNumberElement.mount('#numberInput');
}
}, [elements]);