Cannot access react state from callback - javascript

I have the following components:
const ParentComponent: React.FC = () => {
// Somewhere in the code, I set this to some value
const [newType, setNewType] = useState<any>(undefined);
// Somewhere in the code, I set this to true
const [enableAddEdge, setEnableAddEdge] = useState(false);
const addEdge = (data: any) => {
console.log("newType", newType); // This is undefined
}
return (
...
<VisNetwork
newType={newType}
onAddEdge={addEdge}
enableAddEdge={enableAddEdge}
/>
...
)
}
export default ParentComponent;
interface Props {
newType: any;
onAddEdge: (data: any) => void;
enableAddEdge: boolean;
}
const VisNetwork: React.FC<Props> = (props: Props) => {
const options: any = {
// Some vis-network specific option configuration
manipulation: {
addEdge: (data: any, callback: any) => props.onAddEdge(data);
}
}
...
// Some code to create the network and pass in the options
const network = new vis.Network(container, networkData, options);
useEffect(() => {
if (props.enableAddEdge) {
// This confirms that indeed newType has value
console.log("newType from addEdge", props.newType);
// Call reference to network (I name it currentNetwork)
// This will enable the adding of edge in the network.
// When the user is done adding the edge,
// the `addEdge` method in the `options.manipulation` will be called.
currentNetwork.addEdgeMode();
}
}, [props.enableAddEdge])
useEffect(() => {
if (props.newType) {
// This is just to confirm that indeed I am setting the newType value
console.log("newType from visNetwork", props.newType); // This has value
}
}, [props.newType]);
}
export default VisNetwork;
When the addEdge method is called, the newType state becomes undefined. I know about the bind but I don't know if it's possible to use it and how to use it in a functional component. Please advise on how to obtain the newType value.
Also, from VisNetwork, I want to access networkData state from inside options.manipulation.addEdge. I know it's not possible but any workaround? I also need to access the networkData at this point.

You need to useRef in this scenario. It appears const network = new vis.Network(container, networkData, options); uses the options from the first render only. Or something similar is going on.
It's likely to do with there being a closure around newType in the addEdge function. So it has stale values: https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function
In order to combat this, you need to useRef to store the latest value of newType. The reference is mutable, so it can always store the current value of newType without re-rendering.
// Somewhere in the code, I set this to some value
const [newType, setNewType] = useState<any>(undefined);
const newTypeRef = useRef(newType);
useEffect(() => {
// Ensure the ref is always at the latest value
newTypeRef.current = newType;
}, [newType])
const addEdge = (data: any) => {
console.log("newType", newTypeRef.current); // This is undefined
}

Related

Functional component with React.memo() still rerenders

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);
});

Passing data between react context

I'm having troubles passing data from one context to the other in React. I have some job data that is received from a SignalR connection and I need to pass it to a specific job context, but I'm not sure how to do this.
I have the following code:
SignalRContext
export interface SignalRContextProps {
connect: () => void;
}
export const SignalRContext = createContext<SignalRContextProps>(null!);
export const useSignalRContext = (): SignalRContextProps => {
const {onProgressReceived} = useContext(JobsContext);
const connect = () => {
//Removed a lot of connection setup code for readability
const connection = new HubConnectionBuilder().build();
connection.on('JobReportProgress', onProgressReceived);
connection.start();
};
return {
connect,
};
};
SignalRContextProvider
type Props = {
children: ReactElement | ReactElement[];
}
export const SignalRContextProvider = (props: Props) => {
const {children} = props;
const signalRContext = useSignalRContext();
return (
<SignalRContext.Provider value={signalRContext}>
{children}
</SignalRContext.Provider>
);
};
JobsContext
export const JobsContext = createContext<JobsContextProps>(null!);
export const useJobsContext = (): JobsContextProps => {
const [jobs, setJobs] = useState<Job[]>([]);
const load = async (): Promise<void> => {
const jobs = await getAllJobs();
setJobs(jobs);
};
const onProgressReceived = (progress: JobProgress) => {
console.log(jobs);
const currentJob = jobs.find((job) => job.id === progress.id);
console.log(currentJob); //currentJob will always be empty because jobs array is NULL on receiving progress.
}
};
return {
load, onProgressReceived, jobs
};
};
JobsContextProvider
interface Props {
children: ReactElement | ReactElement[];
}
export const JobsContextProvider = (props: Props): ReactElement => {
const {children} = props;
const jobsContext = useJobsContext();
return (
<JobsContext.Provider value={jobsContext}>
{children}
</JobsContext.Provider>
);
};
index
ReactDOM.render(
<JobsContextProvider>
<SignalRContextProvider>
<App />
</SignalRContextProvider>
</JobsContextProvider>,
document.getElementById('root'),
);
Flow
In my app.tsx I start the SignalR connection by calling signalRContext.connect() created by const signalRContext = useContext(SignalRContext);
I go to my job page where my 6 jobs are loaded from the backend via my context
const {load} = useContext(JobsContext);
await load();
I trigger a job and I see that the SignalR context is calling onProgressReceived on the jobContext. But for some reason the jobs array is empty so I can't update the correct job. Is seems that a new context is created instead of reusing the existing context.
Anyone has an idea how I can make my SignalRContext pass data to my JobContext? Or maybe there is a better system that using context for this?
UPDATE 1:
I have the feeling that there is something strange going on with the HubConnection instance. When I register my 'onProgressReceived' function as a callback function on the 'JobReportProgress' event then it doesn't work. But when I first save the progress with setState and trigger the 'onProgressReceived' function with useEffect it seems to be working. Small example:
const {jobs, onProgressReceived} = useContext(JobsContext);
const [progress, setProgress] = useState<JobProgress | null>(null);
//Change the connection.on line with the following
connection.on('JobReportProgress', onReceived);
//And then we trigger the function on change
const onReceived = (progress: JobProgress) => {
setProgress(progress);
};
useEffect(() => {
if (progress !== null) {
onProgressReceived(progress);
}
}, [progress]);
This seems to be working, but not sure why I first need to save my progress with useState.
I had very same issue a while ago. I even asked question about it Here
There are ways to connect nested context to each other, but then code gets messy.
At first I used event listeners and was dispatching events when some data received, but didn't worked well.
Then I tried passing callbacks in context value. Pass callback from JobsContextProvider ex: onSignalData and call this callback when data is updated in SignalRContextProvider and save in JobsContextProvider.
Combined these 2 contexts into one and it worked well, but didn't liked it as well. Code got messy.
Then I give up using contexts in this type of data and used redux with Redux Toolkit and RTK Query. It works very vell, code is well organized and this is the best solution I have found so far.
Let me know in comments which solution works best for you and I can write exact pseudocode for that solution.

Why is Gatsby arrow function not able to see a value passed in props where value is derived from promise?

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.

Comparing props on a custom doesn't work due to invalid hook call error

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.

Why isn't my React Hook updating when I setState?

I'm using react useState, where the state is an object with some nested properties. When I call setState on it, I'm not seeing a re-render or the state being updated. I assume react is seeing that the new state equals the old state and so no updates occur. So, I've tried cloning the state first, but still am not seeing any updates.
How can I get this function to cause the state to update?
export type TermEditorStateRecord = {
term: SolrTermType;
state: SolrTermEditorRecordState;
classifications: { [key: string]: string };
};
export type TermEditorStateRecordMap = {
[phrase: string]: TermEditorStateRecord;
};
const [records, setRecords] = useState({});
const setRecordClassification = (label, key, value) => {
const cloned = new Object(records) as TermEditorStateRecordMap;
cloned[label].classifications[key] = value;
setRecords(cloned);
};
I apologize for the TypeScript types, but I've included them here so that you can see the expected shape of the state.
Is it not updating because the changes are deeply nested? Is there a way to get this to work, or do I need to somehow pull the state that changes out into its own state?
new Object does not make a deep copy, so for setRecords it's the same instance and it won't trigger the re-render,
const obj = {
a: {
b: "b"
}
};
const copy = new Object(obj);
copy["c"] = "c";
console.log(obj);
You'll need to manually updated the nested property :
const setRecordClassification = (label, key, value) => {
setRecords(record => ({
...record,
[label]: {
...record[label],
classifications: {
...record[label].classifications,
[key]: value
}
}
}));
};
or to create a copy, use :
const cloned = JSON.parse(JSON.stringify(record));
cloned[label].classifications[key] = value;
setRecords(cloned);

Categories