React-Spring: Invalid Hooks Call - javascript

I am trying to learn React-Spring. One of the codes provided in its documentation throws an error when I run it. Any idea what possibly is wrong? How to solve it?
The code I'm trying to run is-
const TextContent = (props) => {
const [items] = useState([
{ id: '0', title: 'Text1' },
{ id: '1', title: 'Text2' },
{ id: '2', title: 'Text1' }
])
const [index, setIndex] = useState(0);
const transitions = useTransition(items[index], index => index.id,
{
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: { tension: 220, friction: 120 }
}
)
useEffect(() => {
const interval = setInterval(() => {
setIndex((state) => (state + 1) % items.length);
}, 4000)
return () => clearInterval(interval);
}, []);
{
transitions.map(({ item, props, key }) => (
<animated.div
key={key}
style={{ ...props, position: 'absolute' }}
>
<p>
{item.title}
</p>
</animated.div>
))
}
}
export default TextContent;

Add a return statement to your functional component
const TextContent = (props) => {
const [items] = useState([
{ id: '0', title: 'Text1' },
{ id: '1', title: 'Text2' },
{ id: '2', title: 'Text1' }
])
const [index, setIndex] = useState(0);
const transitions = useTransition(items[index], index => index.id,
{
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: { tension: 220, friction: 120 }
}
)
useEffect(() => {
const interval = setInterval(() => {
setIndex((state) => (state + 1) % items.length);
}, 4000)
return () => clearInterval(interval);
}, []);
return (
<div>
{
transitions.map(({ item, props, key }) => (
<animated.div
key={key}
style={{ ...props, position: 'absolute' }}
>
<p>{item.title}</p>
</animated.div>
))
}
</div>
)
}
export default TextContent;
Here is a codesandbox where I got it working

In addition to Al Duncanson answer: My problem was in exporting React Fragment instead of actual tag:
return (
<>
{ /* springs.map() */ }
</>
)
Hook started working after I changed it to
return (
<div>
{ /* springs.map() */ }
</div>
)

In my NextJs app, I got the same issue. In my case, I think it was a cache-related issue. Run the project after removing the ".next" folder fixed the issue. I hope removing the build folder in React will do the same.
And there are two similar functions ("useSpring" and "useSprings"). Make sure to pick the right one for your use case.

Related

gain access of the variable that stores the useSelector in my handler function

I am trying to access the students variable by putting its value in a state then accessing that state in my handler function but it always returns an empty array.
const students = useSelector((state) => state.students);
useEffect(() => {
setdata(students);
console.log(data);
}, [])
const handleScanWebCam = (result, error) => {
if (result) {
console.log(data)
const list = data.filter(function (student) {
return student.id === result.text;
})
setScanResultWebCam({
id: list.id,
grade: list.grade,
name: list.name,
section: list.section
});
}
}
this is my full code
function QrScanner() {
const dispatch = useDispatch(getStudents())
useEffect(() => {
dispatch(getStudents());
}, [dispatch])
const students = useSelector((state) => state.students);
const [scanResultWebCam, setScanResultWebCam ] = useState({
id: '',
grade: '',
name: '',
section:''
});
const [openVerifier, setOpenVerifier] = useState(false);
const [data, setdata] = useState([]);
useEffect(() => {
setdata(students);
console.log(data);
}, [])
const handleScanWebCam = (result, error) => {
if (result) {
const list = data.filter(function (student) {
return student.id === result.text;
})
setScanResultWebCam({
id: list.id,
grade: list.grade,
name: list.name,
section: list.section
});
setOpenVerifier(true);
}
}
return (
<>
<NavBar />
<Container
sx={{
display: 'flex',
marginTop: '4rem',
flexWrap: 'wrap',
gap: '12px',
width: '90%'
}}
>
<Box
sx={{
width: '50%',
border: 'solid',
display: 'flex',
flex: '1'
}}
>
<QrReader
scanDelay={500}
containerStyle={{ width: '25rem', margin: 'auto'}}
onResult={handleScanWebCam}
// onError={handleErrorWebcam}
/>
</Box>
<PopupVerifier
details={scanResultWebCam}
verifier={openVerifier}
handleClose={() => handleClose()}
/>
</Container>
</>
)
}
If you need to cache a "duplicate" of the selected students state then I'd recommend caching students in a React ref that can have its current value read at any time during the component lifecycle, especially in stale closures.
Example:
function QrScanner() {
const dispatch = useDispatch(getStudents());
useEffect(() => {
dispatch(getStudents());
}, [dispatch]);
const students = useSelector((state) => state.students);
const studentsRef = React.useRef(students);
useEffect(() => {
studentsRef.current = students;
}, [students]);
...
const handleScanWebCam = (result, error) => {
if (result) {
const list = studentsRef.current.find(
(student) => student.id === result.text
);
if (list) {
setScanResultWebCam(list);
}
setOpenVerifier(true);
}
};
return (
...
);
}
You have using from react-redux. so you dont need to set student state.
and dispatch is used for change in store not getting data from it.
if you want to get an item from array use find instead of filter :
const list = students.find(student=>student.id===result.text);
i edit your code to :
function QrScanner() {
const students = useSelector((state) => state.students);
const [scanResultWebCam, setScanResultWebCam ] = useState({
id: '',
grade: '',
name: '',
section:''
});
const [openVerifier, setOpenVerifier] = useState(false);
const handleScanWebCam = (result, error) => {
if(result) {
const list = students.find(student=> student.id === result.text);
setScanResultWebCam({id: list.id, grade: list.grade, name: list.name, section: list.section});
setOpenVerifier(true);
}
}
return(
...
)
}

Using the Datagrid of material UI, How can I say that I don't want to trigger "onPageChange" after the call of "onPageSizeChange"?

In my DataGrid, I have a server-side pagination, for the events of this pagination, I call these functions :
onPageChange={handlePageChange}
onPageSizeChange={handleSizeChange}
The function "handleSizeChange" automatically trigger the function handlePageChage when I have multiple pages (example on images below), is it the default behavior ? How can I say to material UI I don't want to trigger "onPageChange" after the call of "onPageSizeChange" ? (this is my main question)
Here is my list with the pagination, and when I click on the number "15", two events are triggered instead of one, "onSizeChange" and "onPageChange". Instead, I want only "handleSizeChange" to be triggered. Quick example below :
[Step 1]
enter image description here
[Step 2]
enter image description here
Here are my default pagination values:
export const gridInitialState = {
paginationPageIndex: 1,
paginationPageSize: 10,
paginationRowsPerPageOptions: [10, 15, 20],
sort: [],
};
Here is my Code for handlePageChange:
const handlePageChange = useCallback(
(newPage) => {
const incrementPage = newPage + 1;
console.log(incrementPage);
// here is the bug
if (!isNaN(newPage)) {
if (
incrementPage < paginationData.totalRecords &&
newPage > params.page
) {
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: incrementPage,
size: params.pageSize,
}, "suivant")
);
}
if (incrementPage > 1 && newPage < params.page) {
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: incrementPage,
size: params.pageSize,
}, "précèdent")
);
}
}
// index: 1
// totalRecords: 15
},
[
paginationData,
apiRouteWithUserQuery,
dispatch,
params.pageSize,
reduxThunk,
params.page,
]
);
And this is my code for handle size change :
const handleSizeChange = useCallback(
(newPageSize) => {
if (!isNaN(newPageSize)) {
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: 1,
size: newPageSize,
}, "size changed")
);
}
},
[apiRouteWithUserQuery, dispatch, reduxThunk]
);
I give you the full componenent followed by his hook:
import { Box } from '#mui/material';
import React from 'react';
import useGenericDataGrid from './useGenericDataGrid';
import PropTypes from 'prop-types';
import { processTableColumns, gridInitialState } from './helper';
import { DataGrid } from '#mui/x-data-grid';
export default function GenericDataGrid(props) {
const {
apiRoute,
columns,
reduxSelectors,
reduxThunk,
apiRouteWithUserQuery,
onShowAction,
onEditAction
} = props;
const {
gridRows,
handlePageChange,
handleSizeChange,
handleSortChange,
params,
isLoading
} = useGenericDataGrid(
reduxSelectors,
apiRoute,
reduxThunk,
apiRouteWithUserQuery
);
return (
<Box
sx={{
'& .grey': {
backgroundColor: '#D3D3D3',
color: '#1a3e72'
},
marginLeft: 5,
width: '50%',
height: 500
}}
>
<DataGrid
rows={gridRows}
columns={processTableColumns(
columns,
apiRoute,
onShowAction,
onEditAction
)}
rowCount={params.rowCount}
pageSize={params.pageSize}
page={params.page}
initialState={{
pagination: {
pageSize: gridInitialState.paginationPageSize,
page: gridInitialState.paginationPageIndex - 1
},
sorting: {
sortModel: gridInitialState.sort
}
}}
componentsProps={{
pagination: {
labelRowsPerPage: 'Lignes par page',
labelDisplayedRows: ({ from, to, count }) =>
`${from}-${to} sur ${count} éléments`
}
}}
rowsPerPageOptions={gridInitialState.paginationRowsPerPageOptions}
sortingMode="server"
loading={false}
onPageChange={handlePageChange}
onPageSizeChange={handleSizeChange}
onSortModelChange={handleSortChange}
pagination
disableColumnMenu
isRowSelectable={() => false}
getRowClassName={(params) => {
if (params.row.isDisabled) {
return 'grey';
}
}}
isLoading={isLoading}
paginationMode="server"
/>
</Box>
);
}
GenericDataGrid.propTypes = {
apiRoute: PropTypes.string.isRequired,
columns: PropTypes.array.isRequired,
reduxSelectors: PropTypes.object.isRequired,
reduxThunk: PropTypes.func.isRequired,
apiRouteWithUserQuery: PropTypes.string,
onShowAction: PropTypes.func,
onEditAction: PropTypes.func
};
The hook:
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getUserQueryFromApiRouteWithUserQuery } from "../../../../helpers/uiHelpers";
import { gridInitialState } from "./helper";
const useGenericDataGrid = (
reduxSelectors,
apiRoute,
reduxThunk,
apiRouteWithUserQuery
) => {
let dispatch = useDispatch();
const listData = useSelector(reduxSelectors.dataSelector);
const paginationData = useSelector(reduxSelectors.paginationSelector);
const isLoading = useSelector(reduxSelectors.loadingSelector);
const gridRows = useMemo(() => listData, [listData]);
const params = useMemo(() => {
if (!Object.keys(paginationData).length) {
return {
pattern: getUserQueryFromApiRouteWithUserQuery(apiRouteWithUserQuery),
rowCount: 0,
pageSize: gridInitialState.paginationPageSize,
page: gridInitialState.paginationPageIndex,
sort: gridInitialState.sort,
};
} else {
return {
pattern: getUserQueryFromApiRouteWithUserQuery(apiRouteWithUserQuery),
rowCount: paginationData.totalRecords,
pageSize: paginationData.pageSize,
page: paginationData.pageIndex - 1,
};
}
}, [paginationData, apiRouteWithUserQuery]);
const handlePageChange = useCallback(
(newPage) => {
const incrementPage = newPage + 1;
console.log(incrementPage);
// here is the bug
if (!isNaN(newPage)) {
if (
incrementPage < paginationData.totalRecords &&
newPage > params.page
) {
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: incrementPage,
size: params.pageSize,
}, "suivant")
);
}
if (incrementPage > 1 && newPage < params.page) {
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: incrementPage,
size: params.pageSize,
}, "précèdent")
);
}
}
// index: 1
// totalRecords: 15
},
[
paginationData,
apiRouteWithUserQuery,
dispatch,
params.pageSize,
reduxThunk,
params.page,
]
);
const handleSizeChange = useCallback(
(newPageSize) => {
if (!isNaN(newPageSize)) {
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: 1,
size: newPageSize,
}, "size changed")
);
}
},
[apiRouteWithUserQuery, dispatch, reduxThunk]
);
const handleSortChange = useCallback(
(newSortModel) => {
if (newSortModel.length > 0) {
let sortItemsListOutput = [];
newSortModel.map((sortItem) =>
pushSortItemsArray(sortItemsListOutput, sortItem)
);
dispatch(
reduxThunk({
pattern: getUserQueryFromApiRouteWithUserQuery(
apiRouteWithUserQuery
),
index: params.page + 1,
size: params.pageSize,
sort: sortItemsListOutput,
})
);
}
},
[apiRouteWithUserQuery, dispatch, params.page, params.pageSize, reduxThunk]
);
function pushSortItemsArray(sortItemsListOutput, sortItem) {
const sortItemOutput = {
property: sortItem.field,
direction: sortItem.sort.toUpperCase(),
};
sortItemsListOutput.push(sortItemOutput);
}
return {
gridRows,
handlePageChange,
handleSizeChange,
handleSortChange,
params,
isLoading,
};
};
export default useGenericDataGrid;
Can you help me with that behavior material UI behavior? Thanks a lot for your reading. I appreciate this.
When I change the Page Size of my data grid, I expect to have the new page size with the index 1. I have this behavior, but the following behavior is occuring, "onPageChange" is called right after the event "onPageSizeChange" and I don't understand how and why. Can you help me with this?

problem with useEffect and useState in fake chat app

My program is fake chat App, using useEffect, useState, it will emit a comment per 2 second and display all comments on each lesson. In the first render, it run correctly(emit a comment per 2 second), but in each re-render, it emit 2 comments per 2 second.
I try to console.log(newLessonComments[lessonId - 1].comments.length) after push a comment. And it only push 1 comment to the array.
What is the problem?
import { useState, useEffect } from 'react';
function Content() {
let lessons = [
{
id: 1,
title: 'Bài học 1'
},
{
id: 2,
title: 'Bài học 2'
},
{
id: 3,
title: 'Bài học 3'
}
];
const [lessonId, setLessonId] = useState(1);
const [lessonComment, setLessonComment] = useState(() => {
return lessons.map(lesson => {
return {
id: lesson.id,
comments: []
}
});
})
useEffect(() => {
const handleComment = ({ detail }) => {
setLessonComment(prev => {
const newLessonComments = prev.slice(0);
newLessonComments[lessonId - 1].comments.push(detail);
return newLessonComments;
});
}
window.addEventListener(`lesson${lessonId}`, handleComment);
return () => {
window.removeEventListener(`lesson${lessonId}`, handleComment);
}
}, [lessonId])
return (
<div>
<ul>
{lessons.map(lesson => {
return (
<li
key={lesson.id}
style={{
color: lessonId == lesson.id ? 'red' : '#333',
cursor: 'pointer'
}}
onClick={() => { setLessonId(lesson.id) }}
>
{lesson.title}
</li>
)
})}
</ul>
<div className="comments">
<ul>
{lessonComment[lessonId - 1].comments.map((comment, index) => {
return (
<li
key={index}
>
{comment}
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Content;
function emitComments(id) {
setInterval(() => {
console.log('emit comments')
window.dispatchEvent(
new CustomEvent(`lesson${id}`, {
detail: `Nội dung comments của lesson ${id}`
})
)
}, 2000)
}
emitComments(1);
emitComments(2);
emitComments(3);

Better way to access property in React.useState and update it?

I have a simple component that renders a menu with items. What I am trying to do is have a value called isLoggedIn that accesses the value of the Italian Food item, change it's value to true and later hide the Italian Food item. Currenly my code works, the Italian Restaurant item gets hidden, but is there a better way to access the available property, change it based on a condition and to hide the element? Here is my code:
import React, { useState, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import {
Drawer,
DrawerContent,
DrawerItem,
} from '#progress/kendo-react-layout';
import { Button } from '#progress/kendo-react-buttons';
const CustomItem = (props) => {
const { visible, ...others } = props;
const [isLoggedIn, setIsLoggedIn] = React.useState(
props.available ? false : true
);
const arrowDir = props['data-expanded']
? 'k-i-arrow-chevron-down'
: 'k-i-arrow-chevron-right';
React.useEffect(() => {
setIsLoggedIn(props.available);
}, [props.available]);
return (
<React.Fragment>
{isLoggedIn === false ? null : (
<DrawerItem {...others}>
<span className={'k-icon ' + props.icon} />
<span className={'k-item-text'}>{props.text}</span>
{props['data-expanded'] !== undefined && (
<span
className={'k-icon ' + arrowDir}
style={{
position: 'absolute',
right: 10,
}}
/>
)}
</DrawerItem>
)}
</React.Fragment>
);
};
const DrawerContainer = (props) => {
const [drawerExpanded, setDrawerExpanded] = React.useState(true);
const [items, setItems] = React.useState([
{
text: 'Education',
icon: 'k-i-pencil',
id: 1,
selected: true,
route: '/',
},
{
separator: true,
},
{
text: 'Food',
icon: 'k-i-heart',
id: 2,
['data-expanded']: true,
route: '/food',
},
{
text: 'Japanese Food',
icon: 'k-i-minus',
id: 4,
parentId: 2,
route: '/food/japanese',
},
{
text: 'Italian Food',
icon: 'k-i-minus',
id: 5,
parentId: 2,
route: '/food/italian',
available: false,
},
{
separator: true,
},
{
text: 'Travel',
icon: 'k-i-globe-outline',
['data-expanded']: true,
id: 3,
route: '/travel',
},
{
text: 'Europe',
icon: 'k-i-minus',
id: 6,
parentId: 3,
route: '/travel/europe',
},
{
text: 'North America',
icon: 'k-i-minus',
id: 7,
parentId: 3,
route: '/travel/america',
},
]);
const handleClick = () => {
setDrawerExpanded(!drawerExpanded);
};
const onSelect = (ev) => {
const currentItem = ev.itemTarget.props;
const isParent = currentItem['data-expanded'] !== undefined;
const nextExpanded = !currentItem['data-expanded'];
const newData = items.map((item) => {
const {
selected,
['data-expanded']: currentExpanded,
id,
...others
} = item;
const isCurrentItem = currentItem.id === id;
return {
selected: isCurrentItem,
['data-expanded']:
isCurrentItem && isParent ? nextExpanded : currentExpanded,
id,
...others,
};
});
props.history.push(ev.itemTarget.props.route);
setItems(newData);
};
const data = items.map((item) => {
const { parentId, ...others } = item;
if (parentId !== undefined) {
const parent = items.find((parent) => parent.id === parentId);
return { ...others, visible: parent['data-expanded'] };
}
return item;
});
return (
<div>
<div className="custom-toolbar">
<Button icon="menu" look="flat" onClick={handleClick} />
<span className="title">Categories</span>
</div>
<Drawer
expanded={drawerExpanded}
mode="push"
width={180}
items={data}
item={CustomItem}
onSelect={onSelect}
>
<DrawerContent>{props.children}</DrawerContent>
</Drawer>
</div>
);
};
export default withRouter(DrawerContainer);
If I understood your request properly you want to calculate the isLoggedIn property based on props.available, right? If this is correct then you may just use the useMemo hook in the following way:
const CustomItem = (props) => {
const { visible, ...others } = props;
const isLoggedIn = React.useMemo(() => {
return !props.available
});
const arrowDir = props['data-expanded']
? 'k-i-arrow-chevron-down'
: 'k-i-arrow-chevron-right';
return (
<React.Fragment>
{isLoggedIn === false ? null : (
<DrawerItem {...others}>
<span className={'k-icon ' + props.icon} />
<span className={'k-item-text'}>{props.text}</span>
{props['data-expanded'] !== undefined && (
<span
className={'k-icon ' + arrowDir}
style={{
position: 'absolute',
right: 10,
}}
/>
)}
</DrawerItem>
)}
</React.Fragment>
);
};
Here the doc of the hook if you want to go deeper.

Adding drag`n`drop(react-dnd) to Material-UI component [TreeView]

I have a question. I created a TreeView and tried to bind the drag'n'drop, everything works, the TreeItem can be moved. BUT. If you expand any TreeItem and try to drag it, then all its child TreeItems will move with it.
How to make only one TreeItem drag'n'drop, without its child TreeItems????
My guess is I need to access the inner component of the item tree. I also don't know how to do this.
My Code:
export const PathTreeItem = (props: PathTreeItemProps) => {
const [obj, setObj] = useState<TreeItemType[] | undefined>(undefined)
const [isCurrentRequest, setIsCurrentRequest] = useState(false)
const [{ isDragging }, drag] = useDrag({
item: { type: props.obj.description || 'asd' },
canDrag: true,
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging(),
}),
})
const treeItemStyle = useMemo(
() => ({
opacity: isDragging ? 0.4 : 1,
}),
[isDragging]
)
useEffect(() => {
if (isCurrentRequest && props.obj.parameter_type === 'ABC') {
APIs.get(props.obj.name)
.then(res => {
setObj(res.data)
})
.catch(err => {=
console.error('Error ', err)
})
}
}, [isCurrentRequest])
const handleLabelCLick = useCallback(event => {
console.log(event)
setIsCurrentRequest(!isCurrentRequest)
}, [])
return (
<TreeItem
ref={drag}
style={treeItemStyle}
nodeId={props.index}
label={props.obj.description}
onLabelClick={handleLabelCLick}
>
{props.obj.parameter_type === 'ABC' ? (
obj ? (
obj.map((value, index) => (
<PathTreeItem
key={props.keyIndex * 100 + index}
keyIndex={index}
index={`${props.index}.${index}`}
obj={value}
/>
))
) : (
<div></div>
)
) : null}
</TreeItem>
)
}
I have solved that problem by not dragging the TreeItem itself, but a custom component attached to it as its label attribute. Unfortunately, the solution currently only works in Firefox, not in Chrome or Safari:
const CustomItem = () => {
return (
// custom item components (Box, Typography, etc.)
);
}
const DraggableCustomItem = () => {
const [{ isDragging }, drag] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
type: 'CustomItem'
})
return (
<div ref={drag} style={{ opacity: isDragging ? 0.5 : 1}}>
<CustomItem/>
</div>
)
}
const TreeViewDraggableCustomItem = () => {
return (
<TreeView>
<TreeItem key = { '1' }
nodeId = { '1' }
label = { <DraggableCustomItem/> }>
</TreeView>
);
}
See also related SO question, example sandbox and github comment.

Categories