I am trying to pass a onSelection function to a component:
const GenderScreen = (props) => {
const onSelection = () => {console.log('clicked')};
const buttons = ['one','two', 'three'];
const breadcrumb = `${i18n.t('stage 1')} > ${i18n.t('stage 2')} > ${i18n.t('stage 3')}`;
const sceneConfig = {
centredButtons: ['one','two', 'three'],
question: {
text: [breadcrumb, i18n.t('What is your ethnicity?')],
},
selectNumber: {},
onSelection: this.onSelection
};
return <SceneCentredButtons { ...props} sceneConfig={sceneConfig} />;
};
Child component:
const SceneCentredButtons = props => (
<LYDSceneContainer goBack={props.goBack} subSteps={props.subSteps}>
<Flexible>
<LYDSceneQuestion {...props.sceneConfig.question} />
<LYDCentredButtons
{...props.sceneConfig}
onSelection={props.sceneConfig.onSelection}
/>
</Flexible>
</LYDSceneContainer>
);
function LYDCentredButtons(props) {
const buttons = this.props;
return (
<View style={styles.main}>
{props.centredButtons.map((button, i) => {
const isLast = i + 1 === props.centredButtons.length;
const marginBottomStyle = !isLast && {
marginBottom: theme.utils.em(1.5),
};
return (
<LYDButton
style={[styles.button, marginBottomStyle]}
label={button.text}
onPress={() => props.onSelection(button.value)}
/>
);
})}
</View>
);
}
However, when in my component I get the error:
props.onSelection is not a function
How can I pass a function to use when my buttons are clicked?
GenderScreen is a stateless functional component, you don't need to use this keyword (this will be undefined).
So instead of this.onSelection use onSelection.
Like this:
const sceneConfig = {
centredButtons: ['one','two', 'three'],
question: {
text: [breadcrumb, i18n.t('What is your ethnicity?')],
},
selectNumber: {},
onSelection: onSelection
};
Related
Here's are a few snippets from my code. I have a function component defined as:
function AssociationViewer(props) {
const core = React.useRef<GraphCore | null>(null);
const dataService = React.useRef<DataService>(new DataService(props.graphApi));
const selectionBoxDimensions = React.useRef(null);
const initialGraphRender = React.useRef<boolean>(false);
const filterRef = React.useRef(null);
const getElementRef = React.useCallback((el) => {
if (el && !core.current) {
core.current = new GraphCore({ container: el });
// TODO: Change data service to accept core as an argument and initialize it here?
dataService.current.addGraphDataFn = (opts) => getRksGraph().addData(opts);
dataService.current.setGraphDataFn = (opts) => getRksGraph().setData(opts);
onMount();
return onUnmount;
}
}, []);
.
.
.
return (
<>
{props.enableSearch && <div style={{zIndex: 10000, position: 'absolute', marginTop: 10, right: 15}}>
<button onClick={flashSearchedNodes}>Search</button>
<input
value={searchText}
placeholder='Find node by text'
onKeyDown={(e) => e.key == 'Enter' && flashSearchedNodes()}
onChange={(e) => setSearchText(e.target.value)}
/>
<input readOnly style={{width: '60px', textAlign: 'center'}} type="text" value={searchedElesDisplayText} />
<button onClick={prevFoundNode}>Prev</button>
<button onClick={nextFoundNode}>Next</button>
<button onClick={cancelFlashNodes}>Clear</button>
</div>}
<div
style={{ height: '100%', width: '100%' }}
id={props.componentId || 'kms-graph-core-component'}
ref={getElementRef}
></div>
{core.current && (
<GraphTooltip
tooltip={props.tooltipCallback}
tooltipHoverHideDelay={props.tooltipHoverHideDelay}
tooltipHoverShowDelay={props.tooltipHoverShowDelay}
tippyOptions={props.tippyOptions}
core={core.current}
/>
)}
{props.loadingMask && !hasCustomLoadMask() && (
<DefaultLoadMask
active={showLoading}
loadingClass={getLoadingClass()}
onClick={() => {
setShowLoading(false);
}}
/>
)}
{props.loadingMask && showLoading && hasCustomLoadMask() && props.customLoadingMask()}
</>
);
}
export default AssociationViewer;
I have an angular app that uses a service to call this component as follows:
ReactService.render(AssociationViewer,
{
ref: function (el) {
reactElement = el;
},
component: 'association-viewer-1',
getImageUrl: getImageUrl,
graphApi: graphApi,
pairElements: $ctrl.pairElements,
theme: theme,
view: 'testView'
}
'miniGraphContainer',
() => {
if ($ctrl.pairElements.edges) {
reactElement.core.select($ctls.pairElements.edges($ctrl.currEdge).id);
}
}
Here's GraphTooltip:
import React from 'react';
import GraphCore from '../GraphCore';
function useForceUpdate() {
const [value, setValue] = React.useState(0);
return () => setValue((value) => ++value);
}
interface IGraphTooltipProps{
tooltipHoverShowDelay: number;
tooltipHoverHideDelay: number;
core: GraphCore;
tooltip: Function;
tippyOptions: any;
}
const GraphTooltip = (props: IGraphTooltipProps) => {
const tooltipRef = React.useRef<React.Element>(null);
const forceUpdate = useForceUpdate();
const setRef = (ref) => {
tooltipRef.current = ref;
};
const [currentElement, setCurrentElement] = React.useState<React.Element>();
React.useEffect(() => {
props.core.getAllSubGraphs().forEach((subgraph) => {
subgraph.setOptions({
tooltip: {
domElementCallback: (e) => {
// this isn't changing so it's not picking up a render loop
setCurrentElement(e);
forceUpdate();
return tooltipRef.current;
},
hoverShowDelay: props.tooltipHoverShowDelay,
hoverHideDelay: props.tooltipHoverHideDelay,
options: props.tippyOptions
}
});
});
}, []);
return <>{props.tooltip(setRef, currentElement, props.core)}</>;
};
export default GraphTooltip;
When triggering the event that causes this ReactService to render the AssociationViewer component, I get the warning: Function component cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? Also, reactElement is undefined since the ref cannot be accessed. How can I use React.forwardRef() in the AssociationViewer component to forward the ref to the calling component?
So, here you just have to wrap your functional component inside React.forwardRef()
<GraphTooltip
tooltip={props.tooltipCallback}
tooltipHoverHideDelay={props.tooltipHoverHideDelay}
tooltipHoverShowDelay={props.tooltipHoverShowDelay}
tippyOptions={props.tippyOptions}
core={core.current} // Due to this you are getting warning
/>
Go to your functional component "GraphTooltip" and wrap your export statement in forwardRef().
export const React.forwardRef(GraphTooltip)
Hope it helps!
I have multiple child components (component generate through recursion) inside my main component. Now the problem is when I updated the context variable in the parent component the child component doesn't render the updated value
here is my context provider
function MainLayoutProvider({ children }) {
const [mainJson, setMainJson] = useState([
{
component:'section',
id:'1111',
content:null,
type:'section',
cmType:'normal',
class:'',
style:{},
props:{}
}
]);
return (
<MainLayout.Provider value={mainJson}>
<MainDispatchLayout.Provider value={setMainJson}>
{children}
</MainDispatchLayout.Provider>
</MainLayout.Provider>
);
}
Here I included it on my main component
function App() {
return (
<DndProvider backend={HTML5Backend}>
<MainLayoutProvider>
<PlayGround></PlayGround>
</MainLayoutProvider>
</DndProvider>
);
}
inside the PlayGround component, there is another component name DropSection, where I am updating the 'mainJson' value
function DropSection() {
const board = useContext(MainLayout);
const setBoard = useContext(MainDispatchLayout);
const [{ isOver }, drop] = useDrop(() => ({
accept: "image",
drop(item, monitor) {
const didDrop = monitor.didDrop();
if (!didDrop ) {
addItemToBoard(item.sectionName)
}
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));
const addItemToBoard = (sectionName) => {
let newJson = {
component:sectionName,
id:IdGenerator(),
content:null,
type:'text',
cmType:'normal',
class:'',
style:{},
props:{}
}
setBoard((board) => [...board, newJson]);
};
return (
<div ref={drop}>
<h4 className="text-center">DropZone</h4>
{board.map((config,index) => <RenderCard key={config.id} config={config} />)}
</div>
);
}
but in the RenderCard component, the value of 'mainJson' is not updating or rendering, I am getting the old value which initializes in MainLayoutContext
function RenderCard({config}) {
const board =useContext(MainLayout);
const setBoard = useContext(MainDispatchLayout);
const [{ isOver }, drop] = useDrop(() => ({
accept: "image",
drop(item, monitor) {
const didDrop = monitor.didDrop();
if (!didDrop ) {
addItemToBoard(item.sectionName)
}
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));
const addItemToBoard = async (sectionName) => {
let newJson = {
component:sectionName,
id:IdGenerator(),
content:null,
type:'text',
cmType:'normal',
class:'',
style:{},
props:{}
}
setBoard((board) => [...board, newJson]);
};
if(config.cmType == 'complex'){
return RenderComplexCard(config)
}
var configProperty = {
style: config.style,
id:config.id,
};
return React.createElement(
config.component,
configProperty,
config.content &&
( config.type == "section" && Array.isArray(config.content) ? config.content.map(c => <RenderCard key={c.id} config={c} />) : config.content )
);
}
I had similar issue and solved by using useRef hook, if you do not want to use useState.
Do not forget to reference the variable by using .current when accessing it.
I'm using function component to create a MUI dataGrid, and trying to add a button in a column, and I have a onRowClick function to open a side pane when user clicking row. The problem is, once I click row, react will report error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Here is the code:
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
};
const formatDates = (columns): GridColDef[] => {
return columns;
};
const addTooltipsToData = (columns: GridColDef[]): GridColDef[] => {
console.log('render tool bar called');
return columns.map((column) => {
const { description, field, headerName } = column;
console.log('inside map');
if (field === ID) {
console.log('直接return');
return column;
}
return {
...column,
renderCell: (): JSX.Element => {
console.log('render run');
return (
<Tooltip arrow title={description || ''} >
<span className={classes.headerCell}>{headerName}</span>
</Tooltip>
);
},
};
});
};
const formatColumns = (columns: GridColDef[]): GridColDef[] => {
const dateFormatted = formatDates(columns);
return addTooltipsToData(dateFormatted);
};
console.log('generic table rendered');
return (
<MuiThemeProvider theme={theme}>
<DataGrid
columns={formatColumns(columns)}
rows={rows}
autoHeight
className={classes.table}
components={{
Toolbar: CustomToolbar,
}}
density={GridDensityTypes.Compact}
filterMode={tableMode}
hideFooterSelectedRowCount
loading={loading}
onFilterModelChange={handleFilterChange}
onSortModelChange={handleSortChange}
sortModel={sortModel}
sortingMode={tableMode}
onRowClick={openViewPane}
/>
</MuiThemeProvider>
);
However, if I change the renderCell to renderHeader, it will work fine.
setRightSlidePlaneContent
setRightSlidePlaneOpen
Above are two state passed by parent component in props. it will open a slide pane.
After I comment setRightSliePlaneOpen, it will work well. But no slide pane show.
Please help me slove it. Or do you know how can I add a button in column not using renderCell?
const PageFrame: FC<IProps> = (props: IProps) => {
const classes = useStyles();
const dispatch = useAppDispatch();
const { Component, userInfo } = props;
const [navBarOpen, setNavBarOpen] = useState(false);
const [rightSlidePlaneOpen, setRightSlidePlaneOpen] = useState(false);
const [rightSlidePlaneContent, setRightSlidePlaneContent] = useState(
<Fragment></Fragment>,
);
const [rightSlidePlaneWidthLarge, setRightSlidePlaneWidthLarge] = useState(
false,
);
useEffect(() => {
dispatch({
type: `${GET_USER_LOGIN_INFO}_${REQUEST}`,
payload: {
empId: userInfo.empId,
auth: { domain: 'GENERAL_USER', actionType: 'GENERAL_USER', action: 'VIEW', empId: userInfo.empId},
},
meta: { remote: true },
});
}, []);
return (
<div className={classes.root}>
<HeaderBar
navBarOpen={navBarOpen}
toggleNavBarOpen={setNavBarOpen}
/>
<NavigationBar open={navBarOpen} toggleOpen={setNavBarOpen} />
<Component
setRightSlidePlaneContent={setRightSlidePlaneContent}
setRightSlidePlaneOpen={setRightSlidePlaneOpen}
setRightSlidePlaneWidthLarge={setRightSlidePlaneWidthLarge}
/>
<PersistentDrawerRight
content={rightSlidePlaneContent}
open={rightSlidePlaneOpen}
rspLarge={rightSlidePlaneWidthLarge}
/>
</div>
);
};
export default PageFrame;
The component that calls setRightSidePlaneOpen
interface IProps {
setRightSlidePlaneContent: React.Dispatch<React.SetStateAction<JSX.Element>>;
setRightSlidePlaneOpen: React.Dispatch<React.SetStateAction<boolean>>;
setRightSlidePlaneWidthLarge: React.Dispatch<SetStateAction<boolean>>;
}
const TagDashboard = (props: IProps): JSX.Element => {
const { setRightSlidePlaneContent, setRightSlidePlaneOpen, setRightSlidePlaneWidthLarge } = props;
const employeeId = useAppSelector((store) => store.userInfo.info.employeeNumber);
const rows = useAppSelector((state) => state.tag.rows);
const accountId = useAppSelector(store => store.userInfo.accountId);
const updateContent = useAppSelector(state => state.tag.updateContent);
const numOfUpdates = useAppSelector(state => state.tag.numOfUpdates);
const dispatch = useAppDispatch();
const closeAddForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
};
const openAddForm = (): void => {
setRightSlidePlaneContent(
<AddForm
category={'tag'}
close={closeAddForm}
title={ADD_FORM_TITLE}
createFunction={createTag}
/>);
setRightSlidePlaneOpen(true);
};
const closeForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
setRightSlidePlaneWidthLarge(false);
};
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
setRightSlidePlaneWidthLarge(true);
};
// to the RSP.
return (
<GenericDashboard
addFunction={openAddForm}
description={DESCRIPTION}
title={TITLE}
columns={columns}
handleRowClick={openViewPane}
rows={rows}
numOfUpdates={numOfUpdates}
updateContent={updateContent}
/>
);
};
This is the component of the right slide pane
const { content, open, rspLarge } = props;
const classes = useStyles();
const drawerClass = rspLarge ? classes.drawerLarge : classes.drawer;
const drawerPaperClass = rspLarge ? classes.drawerPaperLarge : classes.drawerPaper;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
className={drawerClass}
variant='temporary'
anchor='right'
open={open}
classes={{
paper: drawerPaperClass,
}}
>
<Fragment>{content}</Fragment>
</Drawer>
</div>
);
I am new to this jest+enzyme testing and I am stuck at how to cover the lines and functions such as onClick(), the useState variables and also useffect(). Can anyone with any experience in such scenerios please give me some direction on how to do that efficiently.
Below is the code:
export interface TMProps {
onClick: (bool) => void;
className?: string;
style?: object;
}
export const TM: React.FC<TMProps> = (props) => {
const {onClick} = props;
const [isMenuOpen, toggleMenu] = useState(false);
const handleUserKeyPress = (event) => {
const e = event;
if (
menuRef &&
!(
(e.target.id && e.target.id.includes("tmp")) ||
(e.target.className &&
(e.target.className.includes("tmp-op") ||
e.target.className.includes("tmp-option-wrapper")))
)
) {
toggleMenu(false);
}
};
useEffect(() => {
window.addEventListener("mousedown", handleUserKeyPress);
return () => {
window.removeEventListener("mousedown", handleUserKeyPress);
};
});
return (
<React.Fragment className="tmp">
<Button
className={props.className}
style={props.style}
id={"lifestyle"}
onClick={() => toggleMenu((state) => !state)}>
Homes International
<FontAwesomeIcon iconClassName="fa-caret-down" />{" "}
</Button>
<Popover
style={{zIndex: 1200}}
id={`template-popover`}
isOpen={isMenuOpen}
target={"template"}
toggle={() => toggleMenu((state) => !state)}
placement="bottom-start"
className={"homes-international"}>
<PopoverButton
className={
"template-option-wrapper homes-international"
}
textProps={{className: "template-option"}}
onClick={() => {
onClick(true);
toggleMenu(false);
}}>
Generic Template{" "}
</PopoverButton>
/>
}
Here is the test I have written but it isn't covering the onClick(), useEffect() and handleUserKeyPress() function.
describe("Modal Heading", () => {
React.useState = jest.fn().mockReturnValueOnce(true)
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Button)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Popover)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(PopoverButton)).toHaveLength(1);
});
What you're looking for is enzyme's:
const btn = wrapper.find('lifestyle');
btn.simulate('click');
wrapper.update();
Not sure if it'd trigger the window listener, it's possible you'll have to mock it.
I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};