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;
}
Related
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);
});
A have two files, with two functional components A and B, in the first component A, i have a specialFunction that gets called with onClick, what i want to do is raise an event in specialFunction when it's called, and then in component B add a Listener for the event in specialFunction.
Component A:
function specialFunction(){
//raise the event and send some data
}
Component B:
//contains a listener that does some work when specialFunction is called, example:
(data) => {console.log("am called:",data)};
1. Create notifier class using observer pattern
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
2. ComponentA receives notifier as a prop and used to notify all subscribers
const ComponentA = ({ notifier }) => {
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div>{/** Some content */}</div>
}
3. ComponentB receives notifier and subscribes to it to receive data sent by from ComponentB
const ComponentB = ({ notifier }) => {
useEffect(() => {
const callbackFn = data => {/** Do whatever you want with received data */ }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [])
}
4. App holds both component. Create instance of notifier there and pass as a props
const App = () => {
const dataNotifier = new ChangeNotifier();
return <div>
<ComponentA notifier={dataNotifier} />
<ComponentB notifier={dataNotifier} />
</div>
}
If you have components on different levels deeply nested and it is hard to pass notifier as a prop, please read about React Context which is very helpful when you want to avoid property drilling
React Context
Here's implementation with context
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
return this.unsubscribe.bind(this, callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
const NotifierContext = React.createContext();
const ComponentA = () => {
const { notifier } = useContext(NotifierContext);
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div><button onClick={triggerNotifier}>Notify</button></div>
}
const ComponentB = () => {
const { notifier } = useContext(NotifierContext);
useEffect(() => {
const callbackFn = data => { console.log(data) }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [notifier])
}
Now all components wrapped in NotifierContext.Provider (no matter how deep they are nested inside other components) will be able to use useContext hook to receive context value passed as value prop to NotifierContext.Provider
const App = () => {
const dataNotifier = useMemo(() => new ChangeNotifier(), []);
return <NotifierContext.Provider value={{ notifier: dataNotifier }}>
<ComponentA />
<ComponentB />
</NotifierContext.Provider>
}
export default App;
Last but not least, I guess you can avoid context or properties drilling and just create instance of ChangeNotifier in some utility file and export it to use globally...
Andrius posted a really good answer, but my problem was that the two components, one of them is used as an API, and the other had a parent component, am a beginner so maybe there is a way to use them but i just didn't know how.
The solution that i used, (maybe not the best) but did the job was to dispatch a custom event in a Promise from the specialFunction:
function specialFunction(){
new Promise((resolve) => {
console.log("am the promise");
document.dispatchEvent(event);
resolve();
});
And add a Listener in the other component using a useEffect hook:
useEffect(() => {
let handlePreview = null;
new Promise((resolve) => {
document.addEventListener(
"previewImg",
(handlePreview = (event) => {
event.stopImmediatePropagation();
//Stuff...
})
);
return () =>
window.removeEventListener("previewImg", handlePreview, false);
});
}, []);
Thank you for your help.
Can someone please explain why the value of key within the arrow function is undefined:
// in parent component
const Parent = () => {
const [key, setKey] = useState<string>();
// this contains an expensive function we only wish to execute once on first load
useEffect(() => {
// has some promise that will call within a `then()`
setKey(someVal);
}, []};
// within render
< MyComponent key={key}/>
}
// within child component
interface Props {
key: string;
}
const MyComponent = ({key}: Props) => {
// this works, I can see the correct value `someVal`
console.log("value when rendered: " + key);
const callback = () => {
// this isn't working, key is undefined
console.log("value within callback: " + key);
}
// within render, when something calls callback(), key is undefined, why?
}
I can see that key has a value when the render is called, but key is undefined. I've tried using let callback = instead of const, but no luck. How do I access key please?
In React, key is a reserved prop name.
[...] attempting to access this.props.key from a component (i.e., the render function or propTypes) is not defined
https://reactjs.org/warnings/special-props.html
Which is probably the reason why it doesn't work in subsequent renders — I'm surprised that it worked in the first render at all!
This works fine:
// https://codepen.io/d4rek/pen/PoZRWQw
import { nanoid } from 'https://cdn.jsdelivr.net/npm/nanoid/nanoid.js'
const Child = ({ id }) => {
console.log(`within component: ${id}`)
const cb = () => console.log(`in callback: ${id}`)
return <button onClick={cb}>Click me</button>
}
const Parent = () => {
const [id, setId] = React.useState(null)
React.useEffect(() => {
setId(nanoid(6))
}, [])
return (<Child id={id} />)
}
ReactDOM.render(<Parent />, document.body)
import React, { useCallback } from 'react';
const callback = useCallback(() => {
// this isn't working, key is undefined
console.log("value within callback: " + key);
}, [key]);
The reason why yours is not working: props are bound to this but the callback as you defined it has its own scope and hence has its own this. So you need to provide the value to that scope. You can do it by using local state of the component. Since there are some nice hooks in React to make that easy you should use them to memorize the callback:
React.useCallback(() =>{console.log(key);}, [key]);
Note the dependency array that updates the callback when key changes. Here the scoping is fine.
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 have a functional component which I pass a function addEvent to, which takes an event parameter. However, when I call the function from props inside a functional component, the function doesn't execute:
const onOk = () => {
const { title, description, start_time, end_time, remind_time } = formStates;
const event = {
title:title[0],
description:description[0],
start_time:start_time.toISOString(),
end_time: end_time.toISOString(),
remind_time: remind_time.toISOString()
}
props.addEvent(event);
props.hideModal();
};
const ModalConductor = props => {
switch(props.modal.currentModal) {
case EVENT_FORM_MODAL:
return <EventsFormModal {...props} title="New Event" addEvent={addEvent}/>
default:
return null;
}
};
Passed Function:
export const addEvent = (event) => dispatch => {
console.log(event);
axios
.post('/api/events/', event)
.then(res => {
dispatch({
type: ADD_EVENT,
payload: res.data
});
}).catch(err => console.log(err));
}
I have read on React docs that passing functions to components requires a this.function = this.function.bind(this);. However, there is no this in a functional component and there is no example in the docs. How would I fix this issue? Any help would be appreciated!
in your code ModalConductor is your functional component which takes props as input.
you are accessing addEvent directly, instead of props.addEvent.
const ModalConductor = props => {
switch(props.modal.currentModal) {
case EVENT_FORM_MODAL:
return <EventsFormModal {...props} title="New Event" addEvent={props.addEvent}/>
default:
return null;
}
};
also as long as your function definition doesn't contain 'this', you dont have to worry about 'this' binding.